| // 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) { | |
| return switch (value.toLowerCase()) { | |
| 'get' => HttpMethod.get, | |
| 'put' => HttpMethod.put, | |
| 'delete' => HttpMethod.delete, | |
| 'post' => HttpMethod.post, | |
| 'patch' => HttpMethod.patch, | |
| 'head' => HttpMethod.head, | |
| _ => 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<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 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>[]; | |
| var _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 | |
| void Function(String line)? keyLog; | |
| @override | |
| void close({bool force = false}) {} | |
| @override | |
| Future<HttpClientRequest> delete(String host, int port, String path) { | |
| final 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(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(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(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(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(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(host: host, port: port, path: path); | |
| return putUrl(uri); | |
| } | |
| @override | |
| Future<HttpClientRequest> putUrl(Uri url) async { | |
| return _findRequest(HttpMethod.put, url, StackTrace.current); | |
| } | |
| var _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 _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 = Completer<void>(); | |
| stream.listen(_body.addAll, onDone: completer.complete); | |
| await completer.future; | |
| } | |
| @override | |
| Future<HttpClientResponse> close() async { | |
| final 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 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 implements 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 | |
| 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<String> 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] = <String>[value.toString()]; | |
| } | |
| @override | |
| String? value(String name) { | |
| return _backingData[name]?.join('; '); | |
| } | |
| @override | |
| DateTime? ifModifiedSince; | |
| @override | |
| late bool persistentConnection; | |
| @override | |
| int? port; | |
| } |