// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; /// The HTTP verb for a [FakeRequest]. enum HttpMethod { get, put, delete, post, patch, head, } HttpMethod _fromMethodString(String value) { final String name = value.toLowerCase(); switch (name) { case 'get': return HttpMethod.get; case 'put': return HttpMethod.put; case 'delete': return HttpMethod.delete; case 'post': return HttpMethod.post; case 'patch': return HttpMethod.patch; case 'head': return HttpMethod.head; default: throw StateError('Unrecognized HTTP method $value'); } } String _toMethodString(HttpMethod method) { return switch (method) { HttpMethod.get => 'GET', HttpMethod.put => 'PUT', HttpMethod.delete => 'DELETE', HttpMethod.post => 'POST', HttpMethod.patch => 'PATCH', HttpMethod.head => 'HEAD' }; } /// Create a fake request that configures the [FakeHttpClient] to respond /// with the provided [response]. /// /// By default, returns a response with a 200 OK status code and an /// empty response. If [responseError] is non-null, will throw this instead /// of returning the response when closing the request. class FakeRequest { const FakeRequest(this.uri, { this.method = HttpMethod.get, this.response = FakeResponse.empty, this.responseError, this.body, }); final Uri uri; final HttpMethod method; final FakeResponse response; final Object? responseError; final List? body; @override String toString() => 'Request{${_toMethodString(method)}, $uri}'; } /// The response the server will create for a given [FakeRequest]. class FakeResponse { const FakeResponse({ this.statusCode = HttpStatus.ok, this.body = const [], this.headers = const >{}, }); static const FakeResponse empty = FakeResponse(); final int statusCode; final List body; final Map> headers; } /// A fake implementation of the HttpClient used for testing. /// /// This does not fully implement the HttpClient. If an additional method /// is actually needed by the test script, then it should be added here /// instead of in another fake. class FakeHttpClient implements HttpClient { /// Creates an HTTP client that responses to each provided /// fake request with the provided fake response. /// /// This does not enforce any order on the requests, but if multiple /// requests match then the first will be selected; FakeHttpClient.list(List requests) : _requests = requests.toList(); /// Creates an HTTP client that always returns an empty 200 request. FakeHttpClient.any() : _any = true, _requests = []; bool _any = false; final List _requests; @override bool autoUncompress = true; @override Duration? connectionTimeout; @override Duration idleTimeout = Duration.zero; @override int? maxConnectionsPerHost; @override String? userAgent; @override void addCredentials(Uri url, String realm, HttpClientCredentials credentials) { throw UnimplementedError(); } @override void addProxyCredentials(String host, int port, String realm, HttpClientCredentials credentials) { throw UnimplementedError(); } @override Future> Function(Uri url, String? proxyHost, int? proxyPort)? connectionFactory; @override Future Function(Uri url, String scheme, String realm)? authenticate; @override Future Function(String host, int port, String scheme, String realm)? authenticateProxy; @override bool Function(X509Certificate cert, String host, int port)? badCertificateCallback; @override Function(String line)? keyLog; @override void close({bool force = false}) { } @override Future delete(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return deleteUrl(uri); } @override Future deleteUrl(Uri url) async { return _findRequest(HttpMethod.delete, url, StackTrace.current); } @override String Function(Uri url)? findProxy; @override Future get(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return getUrl(uri); } @override Future getUrl(Uri url) async { return _findRequest(HttpMethod.get, url, StackTrace.current); } @override Future head(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return headUrl(uri); } @override Future headUrl(Uri url) async { return _findRequest(HttpMethod.head, url, StackTrace.current); } @override Future open(String method, String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return openUrl(method, uri); } @override Future openUrl(String method, Uri url) async { return _findRequest(_fromMethodString(method), url, StackTrace.current); } @override Future patch(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return patchUrl(uri); } @override Future patchUrl(Uri url) async { return _findRequest(HttpMethod.patch, url, StackTrace.current); } @override Future post(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return postUrl(uri); } @override Future postUrl(Uri url) async { return _findRequest(HttpMethod.post, url, StackTrace.current); } @override Future put(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return putUrl(uri); } @override Future putUrl(Uri url) async { return _findRequest(HttpMethod.put, url, StackTrace.current); } int _requestCount = 0; _FakeHttpClientRequest _findRequest(HttpMethod method, Uri uri, StackTrace stackTrace) { // Ensure the fake client throws similar errors to the real client. if (uri.host.isEmpty) { throw ArgumentError('No host specified in URI $uri'); } else if (uri.scheme != 'http' && uri.scheme != 'https') { throw ArgumentError("Unsupported scheme '${uri.scheme}' in URI $uri"); } final String methodString = _toMethodString(method); if (_any) { return _FakeHttpClientRequest( FakeResponse.empty, uri, methodString, null, null, stackTrace, ); } FakeRequest? matchedRequest; for (final FakeRequest request in _requests) { if (request.method == method && request.uri.toString() == uri.toString()) { matchedRequest = request; break; } } if (matchedRequest == null) { throw StateError( 'Unexpected request for $method to $uri after $_requestCount requests.\n' 'Pending requests: ${_requests.join(',')}' ); } _requestCount += 1; _requests.remove(matchedRequest); return _FakeHttpClientRequest( matchedRequest.response, uri, methodString, matchedRequest.responseError, matchedRequest.body, stackTrace, ); } } class _FakeHttpClientRequest implements HttpClientRequest { _FakeHttpClientRequest(this._response, this._uri, this._method, this._responseError, this._expectedBody, this._stackTrace); final FakeResponse _response; final String _method; final Uri _uri; final Object? _responseError; final List _body = []; final List? _expectedBody; final StackTrace _stackTrace; @override bool bufferOutput = true; @override int contentLength = 0; @override late Encoding encoding; @override bool followRedirects = true; @override int maxRedirects = 5; @override bool persistentConnection = true; @override void abort([Object? exception, StackTrace? stackTrace]) { throw UnimplementedError(); } @override void add(List data) { _body.addAll(data); } @override void addError(Object error, [StackTrace? stackTrace]) { } @override Future addStream(Stream> stream) async { final Completer completer = Completer(); stream.listen(_body.addAll, onDone: completer.complete); await completer.future; } @override Future close() async { final Completer completer = Completer(); Timer.run(() { if (_expectedBody != null && !const ListEquality().equals(_expectedBody, _body)) { completer.completeError(StateError( 'Expected a request with the following body:\n$_expectedBody\n but found:\n$_body' ), _stackTrace); } else { completer.complete(); } }); await completer.future; if (_responseError != null) { return Future.error(_responseError!); } return _FakeHttpClientResponse(_response); } @override HttpConnectionInfo get connectionInfo => throw UnimplementedError(); @override List get cookies => throw UnimplementedError(); @override Future get done => throw UnimplementedError(); @override Future flush() async { } @override final HttpHeaders headers = _FakeHttpHeaders(>{}); @override String get method => _method; @override Uri get uri => _uri; @override void write(Object? object) { _body.addAll(utf8.encode(object.toString())); } @override void writeAll(Iterable objects, [String separator = '']) { _body.addAll(utf8.encode(objects.join(separator))); } @override void writeCharCode(int charCode) { _body.add(charCode); } @override void writeln([Object? object = '']) { _body.addAll(utf8.encode('$object\n')); } } class _FakeHttpClientResponse extends Stream> implements HttpClientResponse { _FakeHttpClientResponse(this._response) : headers = _FakeHttpHeaders(Map>.from(_response.headers)); final FakeResponse _response; @override X509Certificate get certificate => throw UnimplementedError(); @override HttpClientResponseCompressionState get compressionState => throw UnimplementedError(); @override HttpConnectionInfo get connectionInfo => throw UnimplementedError(); @override int get contentLength => _response.body.length; @override List get cookies => throw UnimplementedError(); @override Future detachSocket() { throw UnimplementedError(); } @override final HttpHeaders headers; @override bool get isRedirect => throw UnimplementedError(); @override StreamSubscription> listen( void Function(List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { final Stream> response = Stream>.fromIterable(>[ _response.body, ]); return response.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override bool get persistentConnection => throw UnimplementedError(); @override String get reasonPhrase => 'OK'; @override Future redirect([String? method, Uri? url, bool? followLoops]) { throw UnimplementedError(); } @override List get redirects => throw UnimplementedError(); @override int get statusCode => _response.statusCode; } class _FakeHttpHeaders implements HttpHeaders { _FakeHttpHeaders(this._backingData); final Map> _backingData; @override List? operator [](String name) => _backingData[name]; @override void add(String name, Object value, {bool preserveHeaderCase = false}) { _backingData[name] ??= []; _backingData[name]!.add(value.toString()); } @override late bool chunkedTransferEncoding; @override void clear() { _backingData.clear(); } @override int contentLength = -1; @override ContentType? contentType; @override DateTime? date; @override DateTime? expires; @override void forEach(void Function(String name, List values) action) { } @override String? host; @override void noFolding(String name) { } @override void remove(String name, Object value) { _backingData[name]?.remove(value.toString()); } @override void removeAll(String name) { _backingData.remove(name); } @override void set(String name, Object value, {bool preserveHeaderCase = false}) { _backingData[name] = [value.toString()]; } @override String? value(String name) { return _backingData[name]?.join('; '); } @override DateTime? ifModifiedSince; @override late bool persistentConnection; @override int? port; }