blob: 9e85e7cc7d8473c5a54902035445dd7613a38208 [file] [log] [blame]
// 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('; ');
}
}