// 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) { switch (method) { case HttpMethod.get: return 'GET'; case HttpMethod.put: return 'PUT'; case HttpMethod.delete: return 'DELETE'; case HttpMethod.post: return 'POST'; case HttpMethod.patch: return 'PATCH'; case HttpMethod.head: return '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<int>? 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 <int>[], this.headers = const <String, List<String>>{}, }); static const FakeResponse empty = FakeResponse(); final int statusCode; final List<int> body; final Map<String, List<String>> 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<FakeRequest> requests) : _requests = requests.toList(); /// Creates an HTTP client that always returns an empty 200 request. FakeHttpClient.any() : _any = true, _requests = <FakeRequest>[]; bool _any = false; final List<FakeRequest> _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<ConnectionTask<Socket>> Function(Uri url, String? proxyHost, int? proxyPort)? connectionFactory; @override Future<bool> Function(Uri url, String scheme, String realm)? authenticate; @override Future<bool> 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<HttpClientRequest> delete(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return deleteUrl(uri); } @override Future<HttpClientRequest> deleteUrl(Uri url) async { return _findRequest(HttpMethod.delete, url, StackTrace.current); } @override String Function(Uri url)? findProxy; @override Future<HttpClientRequest> get(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return getUrl(uri); } @override Future<HttpClientRequest> getUrl(Uri url) async { return _findRequest(HttpMethod.get, url, StackTrace.current); } @override Future<HttpClientRequest> head(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return headUrl(uri); } @override Future<HttpClientRequest> headUrl(Uri url) async { return _findRequest(HttpMethod.head, url, StackTrace.current); } @override Future<HttpClientRequest> 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<HttpClientRequest> openUrl(String method, Uri url) async { return _findRequest(_fromMethodString(method), url, StackTrace.current); } @override Future<HttpClientRequest> patch(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return patchUrl(uri); } @override Future<HttpClientRequest> patchUrl(Uri url) async { return _findRequest(HttpMethod.patch, url, StackTrace.current); } @override Future<HttpClientRequest> post(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return postUrl(uri); } @override Future<HttpClientRequest> postUrl(Uri url) async { return _findRequest(HttpMethod.post, url, StackTrace.current); } @override Future<HttpClientRequest> put(String host, int port, String path) { final Uri uri = Uri(host: host, port: port, path: path); return putUrl(uri); } @override Future<HttpClientRequest> 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<int> _body = <int>[]; final List<int>? _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<int> data) { _body.addAll(data); } @override void addError(Object error, [StackTrace? stackTrace]) { } @override Future<void> addStream(Stream<List<int>> stream) async { final Completer<void> completer = Completer<void>(); stream.listen(_body.addAll, onDone: completer.complete); await completer.future; } @override Future<HttpClientResponse> close() async { final Completer<void> completer = Completer<void>(); Timer.run(() { if (_expectedBody != null && !const ListEquality<int>().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<HttpClientResponse>.error(_responseError!); } return _FakeHttpClientResponse(_response); } @override HttpConnectionInfo get connectionInfo => throw UnimplementedError(); @override List<Cookie> get cookies => throw UnimplementedError(); @override Future<HttpClientResponse> get done => throw UnimplementedError(); @override Future<void> flush() async { } @override final HttpHeaders headers = _FakeHttpHeaders(<String, List<String>>{}); @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<dynamic> 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<List<int>> implements HttpClientResponse { _FakeHttpClientResponse(this._response) : headers = _FakeHttpHeaders(Map<String, List<String>>.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<Cookie> get cookies => throw UnimplementedError(); @override Future<Socket> detachSocket() { throw UnimplementedError(); } @override final HttpHeaders headers; @override bool get isRedirect => throw UnimplementedError(); @override StreamSubscription<List<int>> listen( void Function(List<int> event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { final Stream<List<int>> response = Stream<List<int>>.fromIterable(<List<int>>[ _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<HttpClientResponse> redirect([String? method, Uri? url, bool? followLoops]) { throw UnimplementedError(); } @override List<RedirectInfo> get redirects => throw UnimplementedError(); @override int get statusCode => _response.statusCode; } class _FakeHttpHeaders extends HttpHeaders { _FakeHttpHeaders(this._backingData); final Map<String, List<String>> _backingData; @override List<String>? operator [](String name) => _backingData[name]; @override void add(String name, Object value, {bool preserveHeaderCase = false}) { _backingData[name] ??= <String>[]; _backingData[name]!.add(value.toString()); } @override void clear() { _backingData.clear(); } @override void forEach(void Function(String name, List<String> values) action) { } @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] = <String>[value.toString()]; } @override String? value(String name) { return _backingData[name]?.join('; '); } }