From d884105ded1ef092881648868272b8de4a13b102 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Sun, 22 Sep 2024 18:45:32 -0700 Subject: [PATCH] add(angel3): adding re-branded angel3 mock_request package --- packages/mock_request/.gitignore | 71 ++++ packages/mock_request/AUTHORS.md | 12 + packages/mock_request/CHANGELOG.md | 78 +++++ packages/mock_request/LICENSE | 29 ++ packages/mock_request/README.md | 26 ++ packages/mock_request/analysis_options.yaml | 1 + packages/mock_request/example/main.dart | 8 + .../lib/platform_mock_request.dart | 6 + .../mock_request/lib/src/connection_info.dart | 13 + packages/mock_request/lib/src/headers.dart | 174 ++++++++++ .../lib/src/lockable_headers.dart | 67 ++++ packages/mock_request/lib/src/request.dart | 323 ++++++++++++++++++ packages/mock_request/lib/src/response.dart | 151 ++++++++ packages/mock_request/lib/src/session.dart | 74 ++++ packages/mock_request/pubspec.yaml | 25 ++ packages/mock_request/test/all_test.dart | 69 ++++ 16 files changed, 1127 insertions(+) create mode 100644 packages/mock_request/.gitignore create mode 100644 packages/mock_request/AUTHORS.md create mode 100644 packages/mock_request/CHANGELOG.md create mode 100644 packages/mock_request/LICENSE create mode 100644 packages/mock_request/README.md create mode 100644 packages/mock_request/analysis_options.yaml create mode 100644 packages/mock_request/example/main.dart create mode 100644 packages/mock_request/lib/platform_mock_request.dart create mode 100644 packages/mock_request/lib/src/connection_info.dart create mode 100644 packages/mock_request/lib/src/headers.dart create mode 100644 packages/mock_request/lib/src/lockable_headers.dart create mode 100644 packages/mock_request/lib/src/request.dart create mode 100644 packages/mock_request/lib/src/response.dart create mode 100644 packages/mock_request/lib/src/session.dart create mode 100644 packages/mock_request/pubspec.yaml create mode 100644 packages/mock_request/test/all_test.dart diff --git a/packages/mock_request/.gitignore b/packages/mock_request/.gitignore new file mode 100644 index 0000000..24d6831 --- /dev/null +++ b/packages/mock_request/.gitignore @@ -0,0 +1,71 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.dart_tool +.packages +.pub/ +build/ + +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub + +# SDK 1.20 and later (no longer creates packages directories) + +# Older SDK versions +# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20) +.project +.buildlog +**/packages/ + + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: + +## VsCode +.vscode/ + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +.idea/ +/out/ +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/packages/mock_request/AUTHORS.md b/packages/mock_request/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/packages/mock_request/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/packages/mock_request/CHANGELOG.md b/packages/mock_request/CHANGELOG.md new file mode 100644 index 0000000..14cbc25 --- /dev/null +++ b/packages/mock_request/CHANGELOG.md @@ -0,0 +1,78 @@ +# Change Log + +## 8.1.1 + +* Updated repository link + +## 8.1.0 + +* Updated `lints` to 3.0.0 + +## 8.0.0 + +* Require Dart >= 3.0 +* Updated `http` to 1.0.0 + +## 7.0.1 + +* Fixed `BytesBuilder` warnings + +## 7.0.0 + +* Require Dart >= 2.17 + +## 6.0.0 + +* Require Dart >= 2.16 + +## 5.0.0 + +* Skipped release + +## 4.0.0 + +* Skipped release + +## 3.0.0 + +* Skipped release + +## 2.1.0 + +* Updated linter to `package:lints` + +## 2.0.2 + +* Updated README +* Updated test cases + +## 2.0.1 + +* Updated README + +## 2.0.0 + +* Migrated to work with Dart >= 2.12 NNBD + +## 1.0.7 + +* Prepare for upcoming Dart SDK change where `HttpHeaders` methods +`add` and `set` take an additional optional parameter `preserveHeaderCase` (thanks @domesticmouse!). + +## 1.0.6 + +* Prepare for upcoming Dart SDK change whereby `HttpRequest` implements + `Stream` rather than `Stream>`. + +## 1.0.5 + +* Add `toString` to `MockHttpHeaders`. + +## 1.0.4 + +* Fix for `ifModifiedSince` + +## 1.0.3 + +* Dart2 fixes +* Apparently fix hangs that break Angel tests diff --git a/packages/mock_request/LICENSE b/packages/mock_request/LICENSE new file mode 100644 index 0000000..df5e063 --- /dev/null +++ b/packages/mock_request/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, dukefirehawk.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/mock_request/README.md b/packages/mock_request/README.md new file mode 100644 index 0000000..cc2abe1 --- /dev/null +++ b/packages/mock_request/README.md @@ -0,0 +1,26 @@ +# Mock HTTP Request + +![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_mock_request?include_prereleases) +[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) +[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) +[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/mock_request/LICENSE) + +**Forked from `mock_request` to support NNBD** + +Manufacture dart:io HttpRequests, HttpResponses, HttpHeaders, etc. This makes it possible to test server-side Dart applications without having to ever bind to a port. + +This package was originally designed to make testing [Angel3](https://angel3-framework.web.app/) applications smoother, but works with any Dart-based server. + +## Usage + +```dart +var rq = MockHttpRequest('GET', Uri.parse('/foo')); +await rq.close(); +await app.handleRequest(rq); // Run within your server-side application +var rs = rq.response; +expect(rs.statusCode, equals(200)); +expect(await rs.transform(UTF8.decoder).join(), + equals(JSON.encode('Hello, world!'))); +``` + +More examples can be found in the included test cases. diff --git a/packages/mock_request/analysis_options.yaml b/packages/mock_request/analysis_options.yaml new file mode 100644 index 0000000..ea2c9e9 --- /dev/null +++ b/packages/mock_request/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml \ No newline at end of file diff --git a/packages/mock_request/example/main.dart b/packages/mock_request/example/main.dart new file mode 100644 index 0000000..af87ab2 --- /dev/null +++ b/packages/mock_request/example/main.dart @@ -0,0 +1,8 @@ +import 'dart:async'; +import 'package:platform_mock_request/platform_mock_request.dart'; + +Future main() async { + var rq = + MockHttpRequest('GET', Uri.parse('/foo'), persistentConnection: false); + await rq.close(); +} diff --git a/packages/mock_request/lib/platform_mock_request.dart b/packages/mock_request/lib/platform_mock_request.dart new file mode 100644 index 0000000..6cf677f --- /dev/null +++ b/packages/mock_request/lib/platform_mock_request.dart @@ -0,0 +1,6 @@ +export 'src/connection_info.dart'; +export 'src/headers.dart'; +export 'src/lockable_headers.dart'; +export 'src/request.dart'; +export 'src/response.dart'; +export 'src/session.dart'; diff --git a/packages/mock_request/lib/src/connection_info.dart b/packages/mock_request/lib/src/connection_info.dart new file mode 100644 index 0000000..ed76589 --- /dev/null +++ b/packages/mock_request/lib/src/connection_info.dart @@ -0,0 +1,13 @@ +import 'dart:io'; + +class MockHttpConnectionInfo implements HttpConnectionInfo { + @override + final InternetAddress remoteAddress; + @override + final int localPort, remotePort; + + MockHttpConnectionInfo( + {required this.remoteAddress, + this.localPort = 8080, + this.remotePort = 80}); +} diff --git a/packages/mock_request/lib/src/headers.dart b/packages/mock_request/lib/src/headers.dart new file mode 100644 index 0000000..07f015c --- /dev/null +++ b/packages/mock_request/lib/src/headers.dart @@ -0,0 +1,174 @@ +import 'dart:io'; + +class MockHttpHeaders implements HttpHeaders { + final Map> _data = {}; + final List _noFolding = []; + //Uri? _host; + String? _hostname; + int _port = 80; + + List get doNotFold => List.unmodifiable(_noFolding); + + @override + ContentType get contentType { + if (_data.containsKey(HttpHeaders.contentTypeHeader)) { + return ContentType.parse(_data[HttpHeaders.contentTypeHeader]!.join(',')); + } else { + return ContentType.html; + } + } + + @override + set contentType(ContentType? value) => + set(HttpHeaders.contentTypeHeader, value?.value ?? ContentType.html); + + @override + DateTime get date => _data.containsKey(HttpHeaders.dateHeader) + ? HttpDate.parse(_data[HttpHeaders.dateHeader]!.join(',')) + : DateTime.now(); + + @override + set date(DateTime? value) => + set(HttpHeaders.dateHeader, HttpDate.format(value ?? DateTime.now())); + + @override + DateTime get expires => _data.containsKey(HttpHeaders.expiresHeader) + ? HttpDate.parse(_data[HttpHeaders.expiresHeader]!.join(',')) + : DateTime.now(); + + @override + set expires(DateTime? value) => + set(HttpHeaders.expiresHeader, HttpDate.format(value ?? DateTime.now())); + + @override + DateTime get ifModifiedSince => + _data.containsKey(HttpHeaders.ifModifiedSinceHeader) + ? HttpDate.parse(_data[HttpHeaders.ifModifiedSinceHeader]!.join(',')) + : DateTime.now(); + + @override + set ifModifiedSince(DateTime? value) => set(HttpHeaders.ifModifiedSinceHeader, + HttpDate.format(value ?? DateTime.now())); + + @override + String? get host { + return _hostname; + /* + if (_host != null) { + return _host!.host; + } else if (_data.containsKey(HttpHeaders.hostHeader)) { + _host = Uri.parse(_data[HttpHeaders.hostHeader]!.join(',')); + return _host!.host; + } else { + return null; + } + */ + } + + @override + int get port { + return _port; + } + + @override + List? operator [](String name) => _data[name.toLowerCase()]; + + @override + void add(String name, Object value, {bool preserveHeaderCase = false}) { + var lower = preserveHeaderCase ? name : name.toLowerCase(); + + if (_data.containsKey(lower)) { + if (value is Iterable) { + _data[lower]!.addAll(value.map((x) => x.toString()).toList()); + } else { + _data[lower]!.add(value.toString()); + } + } else { + if (value is Iterable) { + _data[lower] = value.map((x) => x.toString()).toList(); + } else { + _data[lower] = [value.toString()]; + } + } + } + + @override + void clear() { + _data.clear(); + } + + @override + void forEach(void Function(String name, List values) action) { + _data.forEach(action); + } + + @override + void noFolding(String name) { + _noFolding.add(name.toLowerCase()); + } + + @override + void remove(String name, Object value) { + var lower = name.toLowerCase(); + + if (_data.containsKey(lower)) { + if (value is Iterable) { + for (var x in value) { + _data[lower]!.remove(x.toString()); + } + } else { + _data[lower]!.remove(value.toString()); + } + } + } + + @override + void removeAll(String name) { + _data.remove(name.toLowerCase()); + } + + @override + void set(String name, Object value, {bool preserveHeaderCase = false}) { + var lower = preserveHeaderCase ? name : name.toLowerCase(); + _data.remove(lower); + + if (value is Iterable) { + _data[lower] = value.map((x) => x.toString()).toList(); + } else { + _data[lower] = [value.toString()]; + } + } + + @override + String? value(String name) => _data[name.toLowerCase()]?.join(','); + + @override + String toString() { + var b = StringBuffer(); + _data.forEach((k, v) { + b.write('$k: '); + b.write(v.join(',')); + b.writeln(); + }); + return b.toString(); + } + + @override + bool chunkedTransferEncoding = false; + + @override + int contentLength = 0; + + @override + bool persistentConnection = true; + + @override + set host(String? host) { + _hostname = host; + } + + @override + set port(int? port) { + _port = port ?? 80; + } +} diff --git a/packages/mock_request/lib/src/lockable_headers.dart b/packages/mock_request/lib/src/lockable_headers.dart new file mode 100644 index 0000000..59b12e3 --- /dev/null +++ b/packages/mock_request/lib/src/lockable_headers.dart @@ -0,0 +1,67 @@ +import 'headers.dart'; + +/// Headers that can be locked to editing, i.e. after a request body has been written. +class LockableMockHttpHeaders extends MockHttpHeaders { + bool _locked = false; + + StateError _stateError() => + StateError('Cannot modify headers after they have been write-locked.'); + + void lock() { + _locked = true; + } + + @override + void add(String name, Object value, {bool preserveHeaderCase = false}) { + if (_locked) { + throw _stateError(); + } else { + super.add(name, value, preserveHeaderCase: preserveHeaderCase); + } + } + + @override + void clear() { + if (_locked) { + throw _stateError(); + } else { + super.clear(); + } + } + + @override + void noFolding(String name) { + if (_locked) { + throw _stateError(); + } else { + super.noFolding(name); + } + } + + @override + void remove(String name, Object value) { + if (_locked) { + throw _stateError(); + } else { + super.remove(name, value); + } + } + + @override + void removeAll(String name) { + if (_locked) { + throw _stateError(); + } else { + super.removeAll(name); + } + } + + @override + void set(String name, Object value, {bool preserveHeaderCase = false}) { + if (_locked) { + throw _stateError(); + } else { + super.set(name, value, preserveHeaderCase: preserveHeaderCase); + } + } +} diff --git a/packages/mock_request/lib/src/request.dart b/packages/mock_request/lib/src/request.dart new file mode 100644 index 0000000..e79a679 --- /dev/null +++ b/packages/mock_request/lib/src/request.dart @@ -0,0 +1,323 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:charcode/ascii.dart'; +import 'connection_info.dart'; +import 'lockable_headers.dart'; +import 'response.dart'; +import 'session.dart'; + +class MockHttpRequest + implements HttpRequest, StreamSink>, StringSink { + int _contentLength = 0; + late BytesBuilder _buf; + final Completer _done = Completer(); + final LockableMockHttpHeaders _headers = LockableMockHttpHeaders(); + Uri? _requestedUri; + late MockHttpSession _session; + final StreamController _stream = StreamController(); + + @override + final List cookies = []; + + @override + HttpConnectionInfo connectionInfo = + MockHttpConnectionInfo(remoteAddress: InternetAddress.loopbackIPv4); + + @override + MockHttpResponse response = MockHttpResponse( + contentLength: 0, + encoding: utf8, + persistentConnection: false, + reasonPhrase: '', + statusCode: 200); + + @override + HttpSession get session => _session; + + @override + final String method; + + @override + final Uri uri; + + @override + bool persistentConnection = true; + + /// [copyBuffer] corresponds to `copy` on the [BytesBuilder] constructor. + MockHttpRequest(this.method, this.uri, + {bool copyBuffer = true, + String? protocolVersion, + String? sessionId, + this.certificate, + this.persistentConnection = true}) { + _buf = BytesBuilder(copy: copyBuffer != false); + _session = MockHttpSession(id: sessionId ?? 'mock-http-session'); + + this.protocolVersion = protocolVersion ?? '1.1'; + } + + @override + int get contentLength => _contentLength; + + @override + HttpHeaders get headers => _headers; + + @override + Uri get requestedUri { + if (_requestedUri != null) { + return _requestedUri!; + } else { + return _requestedUri = Uri( + scheme: 'http', + host: 'example.com', + path: uri.path, + query: uri.query, + ); + } + } + + set requestedUri(Uri value) { + _requestedUri = value; + } + + @override + late String protocolVersion; + + @override + X509Certificate? certificate; + + @override + void add(List data) { + if (_done.isCompleted) { + throw StateError('Cannot add to closed MockHttpRequest.'); + } else { + _headers.lock(); + _contentLength += data.length; + _buf.add(data); + } + } + + @override + void addError(error, [StackTrace? stackTrace]) { + if (_done.isCompleted) { + throw StateError('Cannot add to closed MockHttpRequest.'); + } else { + _stream.addError(error, stackTrace); + } + } + + @override + Future addStream(Stream> stream) { + var c = Completer(); + stream.listen(add, onError: addError, onDone: c.complete); + return c.future; + } + + @override + Future close() async { + await flush(); + _headers.lock(); + scheduleMicrotask(_stream.close); + _done.complete(); + return await _done.future; + } + + @override + Future get done => _done.future; + + // @override + Future flush() async { + _contentLength += _buf.length; + _stream.add(_buf.takeBytes()); + } + + @override + void write(Object? obj) { + obj?.toString().codeUnits.forEach(writeCharCode); + } + + @override + void writeAll(Iterable objects, [String separator = '']) { + write(objects.join(separator)); + } + + @override + void writeCharCode(int charCode) { + add([charCode]); + } + + @override + void writeln([Object? obj = '']) { + write(obj ?? ''); + add([$cr, $lf]); + } + + @override + Future any(bool Function(Uint8List element) test) { + return _stream.stream.any((List e) { + return test(Uint8List.fromList(e)); + }); + } + + @override + Stream asBroadcastStream({ + void Function(StreamSubscription subscription)? onListen, + void Function(StreamSubscription subscription)? onCancel, + }) { + return _stream.stream + .asBroadcastStream(onListen: onListen, onCancel: onCancel); + } + + @override + Stream asyncExpand(Stream? Function(Uint8List event) convert) => + _stream.stream.asyncExpand(convert); + + @override + Stream asyncMap(FutureOr Function(Uint8List event) convert) => + _stream.stream.asyncMap(convert); + + @override + Future contains(Object? needle) => _stream.stream.contains(needle); + + @override + Stream distinct( + [bool Function(Uint8List previous, Uint8List next)? equals]) => + _stream.stream.distinct(equals); + + @override + Future drain([E? futureValue]) => _stream.stream.drain(futureValue); + + @override + Future elementAt(int index) => _stream.stream.elementAt(index); + + @override + Future every(bool Function(Uint8List element) test) => + _stream.stream.every(test); + + @override + Stream expand(Iterable Function(Uint8List value) convert) => + _stream.stream.expand(convert); + + @override + Future get first => _stream.stream.first; + + @override + Future firstWhere(bool Function(Uint8List element) test, + {List Function()? orElse}) => + _stream.stream + .firstWhere(test, orElse: () => Uint8List.fromList(orElse!())); + + @override + Future fold( + S initialValue, S Function(S previous, Uint8List element) combine) => + _stream.stream.fold(initialValue, combine); + + @override + Future forEach(void Function(Uint8List element) action) => + _stream.stream.forEach(action); + + @override + Stream handleError(Function onError, + {bool Function(Object?)? test}) => + _stream.stream.handleError(onError, test: test); + + @override + bool get isBroadcast => _stream.stream.isBroadcast; + + @override + Future get isEmpty => _stream.stream.isEmpty; + + @override + Future join([String separator = '']) => + _stream.stream.join(separator); + + @override + Future get last => _stream.stream.last; + + @override + Future lastWhere(bool Function(Uint8List element) test, + {List Function()? orElse}) => + _stream.stream + .lastWhere(test, orElse: () => Uint8List.fromList(orElse!())); + + @override + Future get length => _stream.stream.length; + + @override + StreamSubscription listen( + void Function(Uint8List event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return _stream.stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError == true, + ); + } + + @override + Stream map(S Function(Uint8List event) convert) => + _stream.stream.map(convert); + + @override + Future pipe(StreamConsumer> streamConsumer) => + _stream.stream.cast>().pipe(streamConsumer); + + @override + Future reduce( + List Function(Uint8List previous, Uint8List element) combine) { + return _stream.stream.reduce((Uint8List previous, Uint8List element) { + return Uint8List.fromList(combine(previous, element)); + }); + } + + @override + Future get single => _stream.stream.single; + + @override + Future singleWhere(bool Function(Uint8List element) test, + {List Function()? orElse}) => + _stream.stream + .singleWhere(test, orElse: () => Uint8List.fromList(orElse!())); + + @override + Stream skip(int count) => _stream.stream.skip(count); + + @override + Stream skipWhile(bool Function(Uint8List element) test) => + _stream.stream.skipWhile(test); + + @override + Stream take(int count) => _stream.stream.take(count); + + @override + Stream takeWhile(bool Function(Uint8List element) test) => + _stream.stream.takeWhile(test); + + @override + Stream timeout(Duration timeLimit, + {void Function(EventSink sink)? onTimeout}) => + _stream.stream.timeout(timeLimit, onTimeout: onTimeout); + + @override + Future> toList() => _stream.stream.toList(); + + @override + Future> toSet() => _stream.stream.toSet(); + + @override + Stream transform(StreamTransformer, S> streamTransformer) => + _stream.stream.cast>().transform(streamTransformer); + + @override + Stream where(bool Function(Uint8List event) test) => + _stream.stream.where(test); + + @override + Stream cast() => Stream.castFrom, R>(this); +} diff --git a/packages/mock_request/lib/src/response.dart b/packages/mock_request/lib/src/response.dart new file mode 100644 index 0000000..00c9dc3 --- /dev/null +++ b/packages/mock_request/lib/src/response.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' hide BytesBuilder; +import 'dart:typed_data'; +import 'package:charcode/ascii.dart'; +import 'connection_info.dart'; +import 'lockable_headers.dart'; + +class MockHttpResponse extends Stream> implements HttpResponse { + BytesBuilder _buf = BytesBuilder(); + bool _bufferOutput = true; + final Completer _done = Completer(); + final LockableMockHttpHeaders _headers = LockableMockHttpHeaders(); + final StreamController> _stream = StreamController>(); + + @override + final List cookies = []; + + @override + HttpConnectionInfo connectionInfo = + MockHttpConnectionInfo(remoteAddress: InternetAddress.anyIPv4); + + /// [copyBuffer] corresponds to `copy` on the [BytesBuilder] constructor. + MockHttpResponse( + {bool copyBuffer = true, + required this.statusCode, + required this.reasonPhrase, + required this.contentLength, + this.deadline, + required this.encoding, + required this.persistentConnection, + bool? bufferOutput}) { + _buf = BytesBuilder(copy: copyBuffer != false); + _bufferOutput = bufferOutput != false; + statusCode = 200; + } + + @override + bool get bufferOutput => _bufferOutput; + + @override + set bufferOutput(bool value) {} + + @override + int contentLength; + + @override + Duration? deadline; + + @override + bool persistentConnection; + + @override + String reasonPhrase; + + @override + int statusCode; + + @override + Encoding encoding; + + @override + HttpHeaders get headers => _headers; + + @override + Future get done => _done.future; + + @override + void add(List data) { + if (_done.isCompleted) { + throw StateError('Cannot add to closed MockHttpResponse.'); + } else { + _headers.lock(); + if (_bufferOutput == true) { + _buf.add(data); + } else { + _stream.add(data); + } + } + } + + @override + void addError(error, [StackTrace? stackTrace]) { + if (_done.isCompleted) { + throw StateError('Cannot add to closed MockHttpResponse.'); + } else { + _stream.addError(error, stackTrace); + } + } + + @override + Future addStream(Stream> stream) { + var c = Completer(); + stream.listen(add, onError: addError, onDone: c.complete); + return c.future; + } + + @override + Future close() async { + _headers.lock(); + await flush(); + scheduleMicrotask(_stream.close); + _done.complete(); + //return await _done.future; + } + + @override + Future detachSocket({bool writeHeaders = true}) { + throw UnsupportedError('MockHttpResponses have no socket to detach.'); + } + + @override + Future flush() async { + _stream.add(_buf.takeBytes()); + } + + @override + Future redirect(Uri location, + {int status = HttpStatus.movedTemporarily}) async { + statusCode = status; + } + + @override + void write(Object? obj) { + obj?.toString().codeUnits.forEach(writeCharCode); + } + + @override + void writeAll(Iterable objects, [String separator = '']) { + write(objects.join(separator)); + } + + @override + void writeCharCode(int charCode) { + add([charCode]); + } + + @override + void writeln([Object? obj = '']) { + write(obj ?? ''); + add([$cr, $lf]); + } + + @override + StreamSubscription> listen(void Function(List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) => + _stream.stream.listen(onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError == true); +} diff --git a/packages/mock_request/lib/src/session.dart b/packages/mock_request/lib/src/session.dart new file mode 100644 index 0000000..3766381 --- /dev/null +++ b/packages/mock_request/lib/src/session.dart @@ -0,0 +1,74 @@ +import 'dart:collection'; +import 'dart:io'; + +class MockHttpSession extends MapBase implements HttpSession { + final Map _data = {}; + + @override + String id; + + MockHttpSession({required this.id}); + + @override + int get length => _data.length; + + @override + dynamic operator [](Object? key) => _data[key]; + + @override + void operator []=(key, value) { + _data[key] = value; + } + + @override + void addAll(Map other) => _data.addAll(other); + + @override + void clear() { + _data.clear(); + } + + @override + bool containsKey(Object? key) => _data.containsKey(key); + + @override + bool containsValue(Object? value) => _data.containsValue(value); + + @override + void destroy() { + print('destroy() was called on a MockHttpSession, which does nothing.'); + } + + @override + void forEach(void Function(dynamic, dynamic) action) { + _data.forEach(action); + } + + @override + bool get isEmpty => _data.isEmpty; + + @override + bool get isNew => true; + + @override + bool get isNotEmpty => _data.isNotEmpty; + + @override + Iterable get keys => _data.keys; + + @override + dynamic putIfAbsent(key, dynamic Function() ifAbsent) => + _data.putIfAbsent(key, ifAbsent); + + @override + dynamic remove(Object? key) => _data.remove(key); + + @override + Iterable get values => _data.values; + + @override + set onTimeout(void Function() callback) { + print( + 'An onTimeout callback was set on a MockHttpSession, which will do nothing.'); + } +} diff --git a/packages/mock_request/pubspec.yaml b/packages/mock_request/pubspec.yaml new file mode 100644 index 0000000..a927776 --- /dev/null +++ b/packages/mock_request/pubspec.yaml @@ -0,0 +1,25 @@ +name: platform_mock_request +version: 9.0.0 +description: Manufacture dart:io HttpRequests, HttpResponses, HttpHeaders, etc. +homepage: https://angel3-framework.web.app/ +repository: https://github.com/dart-backend/angel/tree/master/packages/mock_request +environment: + sdk: '>=3.3.0 <4.0.0' +dependencies: + charcode: ^1.3.0 +dev_dependencies: + #angel3_framework: ^7.0.0 + http: ^1.0.0 + test: ^1.24.0 + lints: ^4.0.0 +# dependency_overrides: +# angel3_framework: +# path: ../framework +# angel3_route: +# path: ../route +# angel3_model: +# path: ../model +# angel3_http_exception: +# path: ../http_exception +# angel3_container: +# path: ../container/angel_container \ No newline at end of file diff --git a/packages/mock_request/test/all_test.dart b/packages/mock_request/test/all_test.dart new file mode 100644 index 0000000..41eb2b2 --- /dev/null +++ b/packages/mock_request/test/all_test.dart @@ -0,0 +1,69 @@ +//import 'dart:convert'; +//import 'dart:io'; +//import 'package:angel3_framework/angel3_framework.dart'; +//import 'package:angel3_framework/http.dart'; +//import 'package:angel3_mock_request/angel3_mock_request.dart'; +//import 'package:test/test.dart'; + +void main() { + //var uri = Uri.parse('http://localhost:3000'); + + /* + var app = Angel() + ..get('/foo', (req, res) => 'Hello, world!') + ..post('/body', + (req, res) => req.parseBody().then((_) => req.bodyAsMap.length)) + ..get('/session', (req, res) async { + req.session?['foo'] = 'bar'; + }) + ..get('/conn', (RequestContext req, res) { + return res.serialize(req.ip == InternetAddress.loopbackIPv4.address); + }); + + var http = AngelHttp(app); + + test('receive a response', () async { + var rq = MockHttpRequest('GET', uri.resolve('/foo')); + await rq.close(); + await http.handleRequest(rq); + var rs = rq.response; + expect(rs.statusCode, equals(200)); + expect(await rs.transform(utf8.decoder).join(), + equals(json.encode('Hello, world!'))); + }); + + test('send a body', () async { + var rq = MockHttpRequest('POST', uri.resolve('/body')); + rq + ..headers.set(HttpHeaders.contentTypeHeader, ContentType.json.mimeType) + ..write(json.encode({'foo': 'bar', 'bar': 'baz', 'baz': 'quux'})); + await rq.close(); + await http.handleRequest(rq); + var rs = rq.response; + expect(rs.statusCode, equals(200)); + expect(await rs.transform(utf8.decoder).join(), equals(json.encode(3))); + }); + + test('session', () async { + var rq = MockHttpRequest('GET', uri.resolve('/session')); + await rq.close(); + await http.handleRequest(rq); + expect(rq.session.keys, contains('foo')); + expect(rq.session['foo'], equals('bar')); + }); + + test('connection info', () async { + var rq = MockHttpRequest('GET', uri.resolve('/conn')); + await rq.close(); + await http.handleRequest(rq); + var rs = rq.response; + expect(await rs.transform(utf8.decoder).join(), equals(json.encode(true))); + }); + + test('requested uri', () { + var rq = MockHttpRequest('GET', uri.resolve('/mock')); + expect(rq.uri.path, '/mock'); + expect(rq.requestedUri.toString(), 'http://example.com/mock'); + }); + */ +}