blob: d0f864640f47bc6e6269c75f21d196f861f49a4c [file] [log] [blame]
// Copyright 2019 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 'dart:typed_data';
import 'package:meta/meta.dart';
typedef ContentLengthProvider = int Function();
/// Signature for a callback function that will be notified whenever a
/// [FakeHttpClient] issues requests.
typedef IssueRequestCallback = void Function(FakeHttpClientRequest request);
@immutable
class _Body {
_Body.empty()
: isUtf8 = true,
value = null,
bytes = Uint8List(0),
stream =
Stream<Uint8List>.fromIterable(const Iterable<Uint8List>.empty());
_Body.utf8(this.value)
: assert(value != null),
isUtf8 = true,
bytes = utf8.encode(value) as Uint8List,
stream = Stream<Uint8List>.fromIterable(
<Uint8List>[utf8.encode(value) as Uint8List]);
_Body.rawBytes(this.bytes)
: assert(bytes != null),
isUtf8 = false,
value = null,
stream = Stream<Uint8List>.fromIterable(<Uint8List>[bytes]);
_Body.copy(_Body other)
: assert(other != null),
isUtf8 = other.isUtf8,
value = other.value,
bytes = other.bytes,
stream = Stream<Uint8List>.fromIterable(<Uint8List>[other.bytes]);
final bool isUtf8;
final String value;
final Uint8List bytes;
final Stream<Uint8List> stream;
}
abstract class FakeTransport {
int get contentLength;
HttpConnectionInfo get connectionInfo => null;
final List<FakeCookie> cookies = <FakeCookie>[];
FakeHttpHeaders get headers {
_headers ??= FakeHttpHeaders(contentLengthProvider: () => contentLength);
return _headers;
}
FakeHttpHeaders _headers;
bool get persistentConnection => false;
}
// TODO(tvolkert): `implements Stream<Uint8List>` once HttpClientResponse does the same
abstract class FakeInbound extends FakeTransport {
FakeInbound(String body)
: _body = body == null ? _Body.empty() : _Body.utf8(body);
/// Indicates whether the body stream has been exposed to callers in any way.
/// Once the body stream has been exposed to callers, [body] becomes
/// immutable.
bool _isStreamExposed = false;
/// Resets this transport so that it may be reused.
@mustCallSuper
void reset() {
_body = _Body.copy(_body);
_isStreamExposed = false;
}
/// The UTF-8 encoded value of the HTTP request body, or null if this request
/// specifies no body.
///
/// If the HTTP request body was set via [bodyBytes], then it's assumed that
/// the body is not a UTF-8 encoded string, and subsequently attempting to
/// access [body] will throw a [StateError].
///
/// Once the body stream has been exposed to callers in any way, the [body]
/// value becomes immutable (as does the [bodyBytes] value), and any attempt
/// to modify it will throw a [StateError].
String get body {
if (!_body.isUtf8) {
throw StateError('body is not a valid UTF-8 string');
}
return _body.value;
}
_Body _body;
set body(String value) {
if (_isStreamExposed) {
throw StateError('The body of this transport has been made immutable');
}
_body = value == null ? _Body.empty() : _Body.utf8(value);
}
/// The raw bytes of the HTTP request body.
///
/// This will never be null; if the HTTP request body is empty, this will be
/// the empty list.
///
/// Setting this value directly will be assumed to be because the bytes are
/// not a UTF-8 encoded string, and subsequently attempting to access [body]
/// will throw a [StateError].
///
/// Once the body stream has been exposed to callers in any way, the
/// [bodyBytes] value becomes immutable (as does the [body] value), and any
/// attempt to modify it will throw a [StateError].
Uint8List get bodyBytes => _body.bytes;
set bodyBytes(Uint8List value) {
if (_isStreamExposed) {
throw StateError('The body of this transport has been made immutable');
}
assert(value != null);
_body = _Body.rawBytes(value);
}
StreamSubscription<Uint8List> listen(
void Function(Uint8List event) onData, {
Function onError,
void Function() onDone,
bool cancelOnError,
}) {
_isStreamExposed = true;
return _body.stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
Future<bool> any(bool Function(Uint8List element) test) {
_isStreamExposed = true;
return _body.stream.any(test);
}
Stream<Uint8List> asBroadcastStream({
void Function(StreamSubscription<Uint8List> subscription) onListen,
void Function(StreamSubscription<Uint8List> subscription) onCancel,
}) {
_isStreamExposed = true;
return _body.stream
.asBroadcastStream(onListen: onListen, onCancel: onCancel);
}
Stream<E> asyncExpand<E>(Stream<E> Function(Uint8List event) convert) {
_isStreamExposed = true;
return _body.stream.asyncExpand<E>(convert);
}
Stream<E> asyncMap<E>(FutureOr<E> Function(Uint8List event) convert) {
_isStreamExposed = true;
return _body.stream.asyncMap<E>(convert);
}
Stream<R> cast<R>() {
_isStreamExposed = true;
return _body.stream.cast<R>();
}
Future<bool> contains(Object needle) {
_isStreamExposed = true;
return _body.stream.contains(needle);
}
Stream<Uint8List> distinct(
[bool Function(Uint8List previous, Uint8List next) equals]) {
_isStreamExposed = true;
return _body.stream.distinct(equals);
}
Future<E> drain<E>([E futureValue]) {
_isStreamExposed = true;
return _body.stream.drain<E>(futureValue);
}
Future<Uint8List> elementAt(int index) {
_isStreamExposed = true;
return _body.stream.elementAt(index);
}
Future<bool> every(bool Function(Uint8List element) test) {
_isStreamExposed = true;
return _body.stream.every(test);
}
Stream<S> expand<S>(Iterable<S> Function(Uint8List element) convert) {
_isStreamExposed = true;
return _body.stream.expand(convert);
}
Future<Uint8List> get first {
_isStreamExposed = true;
return _body.stream.first;
}
Future<Uint8List> firstWhere(
bool Function(Uint8List element) test, {
List<int> Function() orElse,
}) {
_isStreamExposed = true;
return _body.stream
.firstWhere(test, orElse: () => Uint8List.fromList(orElse()));
}
Future<S> fold<S>(
S initialValue, S Function(S previous, Uint8List element) combine) {
_isStreamExposed = true;
return _body.stream.fold<S>(initialValue, combine);
}
Future<dynamic> forEach(void Function(Uint8List element) action) {
_isStreamExposed = true;
return _body.stream.forEach(action);
}
Stream<Uint8List> handleError(
Function onError, {
bool Function(dynamic error) test,
}) {
_isStreamExposed = true;
return _body.stream.handleError(onError, test: test);
}
bool get isBroadcast {
_isStreamExposed = true;
return _body.stream.isBroadcast;
}
Future<bool> get isEmpty {
_isStreamExposed = true;
return _body.stream.isEmpty;
}
Future<String> join([String separator = '']) {
_isStreamExposed = true;
return _body.stream.join(separator);
}
Future<Uint8List> get last {
_isStreamExposed = true;
return _body.stream.last;
}
Future<Uint8List> lastWhere(
bool Function(Uint8List element) test, {
List<int> Function() orElse,
}) {
_isStreamExposed = true;
return _body.stream
.lastWhere(test, orElse: () => Uint8List.fromList(orElse()));
}
Future<int> get length {
_isStreamExposed = true;
return _body.stream.length;
}
Stream<S> map<S>(S Function(Uint8List event) convert) {
_isStreamExposed = true;
return _body.stream.map<S>(convert);
}
Future<dynamic> pipe(StreamConsumer<List<int>> streamConsumer) {
_isStreamExposed = true;
return _body.stream
.map((Uint8List list) => list.toList())
.pipe(streamConsumer);
}
Future<Uint8List> reduce(
List<int> Function(Uint8List previous, Uint8List element) combine) {
_isStreamExposed = true;
return _body.stream.reduce((Uint8List previous, Uint8List element) =>
Uint8List.fromList(combine(previous, element)));
}
Future<Uint8List> get single {
_isStreamExposed = true;
return _body.stream.single;
}
Future<Uint8List> singleWhere(
bool Function(Uint8List element) test, {
List<int> Function() orElse,
}) {
_isStreamExposed = true;
return _body.stream
.singleWhere(test, orElse: () => Uint8List.fromList(orElse()));
}
Stream<Uint8List> skip(int count) {
_isStreamExposed = true;
return _body.stream.skip(count);
}
Stream<Uint8List> skipWhile(bool Function(Uint8List element) test) {
_isStreamExposed = true;
return _body.stream.skipWhile(test);
}
Stream<Uint8List> take(int count) {
_isStreamExposed = true;
return _body.stream.take(count);
}
Stream<Uint8List> takeWhile(bool Function(Uint8List element) test) {
_isStreamExposed = true;
return _body.stream.takeWhile(test);
}
Stream<Uint8List> timeout(
Duration timeLimit, {
void Function(EventSink<Uint8List> sink) onTimeout,
}) {
_isStreamExposed = true;
return _body.stream.timeout(timeLimit, onTimeout: onTimeout);
}
Future<List<Uint8List>> toList() {
_isStreamExposed = true;
return _body.stream.toList();
}
Future<Set<Uint8List>> toSet() {
_isStreamExposed = true;
return _body.stream.toSet();
}
Stream<S> transform<S>(StreamTransformer<List<int>, S> streamTransformer) {
_isStreamExposed = true;
return _body.stream
.map((Uint8List list) => list.toList())
.transform<S>(streamTransformer);
}
Stream<Uint8List> where(bool Function(Uint8List event) test) {
_isStreamExposed = true;
return _body.stream.where(test);
}
@override
int get contentLength => _body.bytes.length;
X509Certificate get certificate => null;
}
abstract class FakeOutbound extends FakeTransport implements IOSink {
StringBuffer _buffer = StringBuffer();
String get body => _buffer.toString();
List<Object> get errors => _errors;
List<Object> _errors = <Object>[];
/// Whether this outbound has been closed.
bool get isClosed => _isClosed;
bool _isClosed = false;
/// Resets this transport so that it may be reused.
@mustCallSuper
void reset() {
_isClosed = false;
_buffer = StringBuffer();
_errors = <Object>[];
}
@override
Encoding get encoding => utf8;
@override
set encoding(Encoding value) => throw UnsupportedError('Unsupported');
@override
void add(List<int> data) {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.write(utf8.decode(data));
}
@override
void addError(Object error, [StackTrace stackTrace]) {
if (isClosed) {
throw StateError('Transport is closed');
}
errors.add(error);
}
@override
Future<dynamic> addStream(Stream<List<int>> stream) async {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.write(await utf8.decoder.bind(stream).join());
}
@override
void write(Object obj) {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.write(obj);
}
@override
void writeAll(Iterable<dynamic> objects, [String separator = '']) {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.writeAll(objects, separator);
}
@override
void writeCharCode(int charCode) {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.writeCharCode(charCode);
}
@override
void writeln([Object obj = '']) {
if (isClosed) {
throw StateError('Transport is closed');
}
headers._sealed = true;
_buffer.writeln(obj);
}
@override
Future<dynamic> get done async {}
@override
Future<dynamic> flush() async {}
@override
Future<dynamic> close() async {
_isClosed = true;
}
bool get bufferOutput => false;
set bufferOutput(bool value) => throw UnsupportedError('Unsupported');
@override
int get contentLength => _contentLength ?? _buffer.length;
int _contentLength;
set contentLength(int value) {
_contentLength = value;
}
set persistentConnection(bool value) => throw UnsupportedError('Unsupported');
}
class FakeCookie implements Cookie {
FakeCookie({
this.name,
this.value,
this.domain,
this.path,
this.expires,
this.httpOnly,
this.maxAge,
this.secure,
});
@override
String name;
@override
String value;
@override
String domain;
@override
String path;
@override
DateTime expires;
@override
bool httpOnly;
@override
int maxAge;
@override
bool secure;
}
class FakeHttpHeaders implements HttpHeaders {
FakeHttpHeaders({this.contentLengthProvider});
final ContentLengthProvider contentLengthProvider;
final Map<String, List<String>> _values = <String, List<String>>{};
bool _sealed = false;
void _checkSealed() {
if (_sealed) {
throw StateError('HTTP headers are sealed');
}
}
@override
bool get chunkedTransferEncoding => false;
@override
set chunkedTransferEncoding(bool value) =>
throw UnsupportedError('Unsupported');
@override
int get contentLength =>
contentLengthProvider != null ? contentLengthProvider() : -1;
@override
set contentLength(int value) => throw UnsupportedError('Unsupported');
@override
ContentType get contentType => throw UnimplementedError();
@override
set contentType(ContentType value) {
_checkSealed();
removeAll(HttpHeaders.contentTypeHeader);
add(HttpHeaders.contentTypeHeader, '$value');
}
@override
DateTime date;
@override
DateTime expires;
@override
String host;
@override
DateTime ifModifiedSince;
@override
bool persistentConnection;
@override
int port;
@override
List<String> operator [](String name) => _values[name];
@override
void add(String name, Object value, {bool preserveHeaderCase = false}) {
_checkSealed();
name = name.toLowerCase();
_values[name] ??= <String>[];
_values[name].add('$value');
}
@override
void clear() {
_checkSealed();
_values.clear();
}
@override
void forEach(void Function(String name, List<String> values) f) {
_values.forEach(f);
}
@override
void noFolding(String name) {}
@override
void remove(String name, Object value) {
_checkSealed();
name = name.toLowerCase();
if (_values.containsKey('$value')) {
_values[name].remove('$value');
}
}
@override
void removeAll(String name) {
_checkSealed();
name = name.toLowerCase();
_values.remove(name);
}
@override
void set(String name, Object value, {bool preserveHeaderCase = false}) {
_checkSealed();
name = name.toLowerCase();
_values[name] = <String>['$value'];
}
@override
String value(String name) {
final List<String> value = _values[name.toLowerCase()];
return value == null ? null : value.single;
}
}
class FakeHttpRequest extends FakeInbound implements HttpRequest {
/// Creates a new [FakeHttpRequest].
///
/// If the optional [body] argument is specified, the request stream will
/// yield the specified body value when UTF-8 decoded. By default, the
/// request stream will be empty. The [body] property can be modified until
/// the stream has been exposed to callers, at which time it becomes
/// immutable.
FakeHttpRequest({
this.method = 'GET',
String body,
String path = '/',
Map<String, dynamic> queryParametersValue,
FakeHttpResponse response,
}) : assert(method != null),
assert(path != null),
uri = Uri(path: path, queryParameters: queryParametersValue),
response = response ?? FakeHttpResponse(),
super(body);
@override
String method;
@override
Uri uri;
String get path => uri.path;
set path(String value) {
uri = uri.replace(path: value);
}
@override
FakeHttpResponse response;
@override
String get protocolVersion => '1.1';
@override
Uri get requestedUri => uri;
@override
HttpSession get session => throw UnsupportedError('Unsupported');
}
class FakeHttpResponse extends FakeOutbound implements HttpResponse {
@override
Duration get deadline => null;
@override
set deadline(Duration value) => throw UnsupportedError('Unsupported');
@override
String get reasonPhrase => null;
@override
set reasonPhrase(String value) => throw UnsupportedError('Unsupported');
@override
int statusCode = HttpStatus.ok;
@override
Future<dynamic> redirect(Uri location,
{int status = HttpStatus.movedTemporarily}) {
assert(location != null);
assert(status != null);
statusCode = status;
headers.add(HttpHeaders.locationHeader, '$location');
return close();
}
@override
Future<Socket> detachSocket({bool writeHeaders = true}) =>
throw UnsupportedError('Unsupported');
}
class FakeHttpClient implements HttpClient {
FakeHttpClient({
FakeHttpClientRequest request,
this.onIssueRequest,
}) : request = request ?? FakeHttpClientRequest();
/// The request to return from the HTTP methods.
FakeHttpClientRequest request;
/// Optional callback that will be notified when this client issues requests.
IssueRequestCallback onIssueRequest;
/// The number of requests that have been issued.
int get requestCount => _requestCount;
int _requestCount = 0;
static const String methodDelete = 'DELETE';
static const String methodGet = 'GET';
static const String methodHead = 'HEAD';
static const String methodPatch = 'PATCH';
static const String methodPost = 'POST';
static const String methodPut = 'PUT';
@override
bool autoUncompress;
@override
Duration connectionTimeout;
@override
Duration idleTimeout;
@override
int maxConnectionsPerHost;
@override
String userAgent;
@override
void addCredentials(
Uri url, String realm, HttpClientCredentials credentials) {}
@override
void addProxyCredentials(
String host, int port, String realm, HttpClientCredentials credentials) {}
@override
set authenticate(
Future<bool> Function(Uri url, String scheme, String realm) f) {}
@override
set authenticateProxy(
Future<bool> Function(String host, int port, String scheme, String realm)
f) {}
@override
set badCertificateCallback(
bool Function(X509Certificate cert, String host, int port) callback) {}
@override
set findProxy(String Function(Uri url) f) {}
@override
void close({bool force = false}) {}
@override
Future<HttpClientRequest> delete(String host, int port, String path) async {
return open(methodDelete, host, port, path);
}
@override
Future<HttpClientRequest> deleteUrl(Uri url) async {
return openUrl(methodDelete, url);
}
@override
Future<HttpClientRequest> get(String host, int port, String path) async {
return open(methodGet, host, port, path);
}
@override
Future<HttpClientRequest> getUrl(Uri url) async {
return openUrl(methodGet, url);
}
@override
Future<HttpClientRequest> head(String host, int port, String path) async {
return open(methodHead, host, port, path);
}
@override
Future<HttpClientRequest> headUrl(Uri url) async {
return openUrl(methodHead, url);
}
@override
Future<HttpClientRequest> patch(String host, int port, String path) {
return open(methodPatch, host, port, path);
}
@override
Future<HttpClientRequest> patchUrl(Uri url) {
return openUrl(methodPatch, url);
}
@override
Future<HttpClientRequest> post(String host, int port, String path) async {
return open(methodPost, host, port, path);
}
@override
Future<HttpClientRequest> postUrl(Uri url) async {
return openUrl(methodPost, url);
}
@override
Future<HttpClientRequest> put(String host, int port, String path) async {
return open(methodPut, host, port, path);
}
@override
Future<HttpClientRequest> putUrl(Uri url) async {
return openUrl(methodPut, url);
}
@override
Future<HttpClientRequest> open(
String method, String host, int port, String path) {
return openUrl(method, Uri(host: host, port: port, path: path));
}
@override
Future<HttpClientRequest> openUrl(String method, Uri url) async {
_requestCount++;
request.reset();
request.method = method;
request.uri = url;
if (onIssueRequest != null) {
onIssueRequest(request);
}
return request;
}
}
class FakeHttpClientRequest extends FakeOutbound implements HttpClientRequest {
FakeHttpClientRequest({
FakeHttpClientResponse response,
}) : response = response ?? FakeHttpClientResponse();
/// The response to produce when this request is closed.
FakeHttpClientResponse response;
Completer<HttpClientResponse> _doneCompleter =
Completer<HttpClientResponse>();
/// Resets this fake request so that it may be reused.
@override
void reset() {
super.reset();
response.reset();
if (!_doneCompleter.isCompleted) {
_doneCompleter.complete(response);
}
_doneCompleter = Completer<HttpClientResponse>();
}
@override
String method;
@override
Uri uri;
@override
bool followRedirects;
@override
int maxRedirects;
@override
Future<HttpClientResponse> close() async {
await super.close();
_doneCompleter.complete(response);
return response;
}
@override
Future<HttpClientResponse> get done => _doneCompleter.future;
}
class FakeHttpClientResponse extends FakeInbound implements HttpClientResponse {
FakeHttpClientResponse({String body}) : super(body);
@override
HttpClientResponseCompressionState get compressionState {
return HttpClientResponseCompressionState.decompressed;
}
@override
Future<Socket> detachSocket() async =>
throw UnsupportedError('Mocked response');
@override
bool get isRedirect => false;
@override
String get reasonPhrase => null;
@override
Future<HttpClientResponse> redirect(
[String method, Uri url, bool followLoops]) {
return Future<HttpClientResponse>.error(
UnsupportedError('Mocked response'));
}
@override
List<RedirectInfo> get redirects => <RedirectInfo>[];
@override
int statusCode = HttpStatus.ok;
}