blob: 0d0f026309871fa8b253cb3cba56a2a733629a1e [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();
@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) as Uint8List,
stream = Stream<Uint8List>.fromIterable(<Uint8List>[utf8.encode(value) as Uint8List]);
_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');
}
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');
}