| // 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(); |
| |
| @immutable |
| class _Body { |
| _Body.empty() |
| : isUtf8 = true, |
| value = null, |
| bytes = Uint8List(0), |
| stream = Stream<Uint8List>.fromIterable(const Iterable<Uint8List>.empty()); |
| |
| _Body.utf8(String this.value) |
| : isUtf8 = true, |
| bytes = utf8.encode(value), |
| stream = Stream<Uint8List>.fromIterable(<Uint8List>[utf8.encode(value)]); |
| |
| _Body.rawBytes(this.bytes) |
| : isUtf8 = false, |
| value = null, |
| stream = Stream<Uint8List>.fromIterable(<Uint8List>[bytes]); |
| |
| _Body.copy(_Body other) |
| : 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'); |
| } |
| _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, { |
| Uint8List 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<Uint8List> 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'); |
| } |
| |
| abstract class FakeCookie implements Cookie { |
| FakeCookie({ |
| required this.name, |
| required this.value, |
| this.domain, |
| this.path, |
| this.expires, |
| required this.httpOnly, |
| this.maxAge, |
| required 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 |
| late 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?.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, |
| }) : 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 => 'reason'; |
| |
| @override |
| set reasonPhrase(String value) => throw UnsupportedError('Unsupported'); |
| |
| @override |
| int statusCode = HttpStatus.ok; |
| |
| @override |
| Future<dynamic> redirect(Uri location, {int status = HttpStatus.movedTemporarily}) { |
| statusCode = status; |
| headers.add(HttpHeaders.locationHeader, '$location'); |
| return close(); |
| } |
| |
| @override |
| Future<Socket> detachSocket({bool writeHeaders = true}) => throw UnsupportedError('Unsupported'); |
| } |