From f8ec10d4e2d555538f2b2bcd9f5d70a9fee67ced Mon Sep 17 00:00:00 2001 From: thomashii Date: Fri, 19 Mar 2021 07:10:19 +0800 Subject: [PATCH] Added merge_map and mock_request packages --- CHANGELOG.md | 3 + packages/framework/pubspec.yaml | 16 +- packages/merge_map/.gitignore | 76 +++++ packages/merge_map/CHANGELOG.md | 7 + packages/merge_map/LICENSE | 21 ++ packages/merge_map/README.md | 19 ++ packages/merge_map/example/main.dart | 11 + packages/merge_map/lib/merge_map.dart | 34 ++ packages/merge_map/pubspec.yaml | 9 + packages/merge_map/test/all_test.dart | 94 ++++++ packages/mock_request/.gitignore | 72 ++++ packages/mock_request/.travis.yml | 1 + packages/mock_request/CHANGELOG.md | 17 + packages/mock_request/LICENSE | 21 ++ packages/mock_request/README.md | 25 ++ packages/mock_request/analysis_options.yaml | 4 + packages/mock_request/example/main.dart | 7 + packages/mock_request/lib/mock_request.dart | 6 + .../mock_request/lib/src/connection_info.dart | 10 + packages/mock_request/lib/src/headers.dart | 151 +++++++++ .../lib/src/lockable_headers.dart | 67 ++++ packages/mock_request/lib/src/request.dart | 317 ++++++++++++++++++ packages/mock_request/lib/src/response.dart | 150 +++++++++ packages/mock_request/lib/src/session.dart | 74 ++++ packages/mock_request/pubspec.yaml | 13 + packages/mock_request/test/all_test.dart | 66 ++++ 26 files changed, 1285 insertions(+), 6 deletions(-) create mode 100644 packages/merge_map/.gitignore create mode 100644 packages/merge_map/CHANGELOG.md create mode 100644 packages/merge_map/LICENSE create mode 100644 packages/merge_map/README.md create mode 100644 packages/merge_map/example/main.dart create mode 100644 packages/merge_map/lib/merge_map.dart create mode 100644 packages/merge_map/pubspec.yaml create mode 100644 packages/merge_map/test/all_test.dart create mode 100644 packages/mock_request/.gitignore create mode 100644 packages/mock_request/.travis.yml 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/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/CHANGELOG.md b/CHANGELOG.md index 20accd9f..d51e4858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ * Updated angel_route to 5.0.0 * Updated angel_model to 3.0.0 * Updated angel_container to 3.0.0 +* Updated angel_framework to 4.0.0 + - merge_map + - mock_request # 3.0.0 (Non NNBD) * Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0" diff --git a/packages/framework/pubspec.yaml b/packages/framework/pubspec.yaml index 9f41c7ae..18d6acb0 100644 --- a/packages/framework/pubspec.yaml +++ b/packages/framework/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 3.0.0 +version: 4.0.0 description: A high-powered HTTP server with dependency injection, routing and much more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework @@ -10,25 +10,29 @@ dependencies: angel_container: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/container/angel_container angel_http_exception: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/http_exception angel_model: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/model angel_route: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/route charcode: ^1.0.0 - combinator: ^1.0.0 + combinator: + git: + url: https://github.com/dukefirehawk/angel.git + ref: sdk-2.12.x_nnbd + path: packages/combinator file: ^6.1.0 http_parser: ^4.0.0 http_server: ^0.9.0 diff --git a/packages/merge_map/.gitignore b/packages/merge_map/.gitignore new file mode 100644 index 00000000..3707b1e0 --- /dev/null +++ b/packages/merge_map/.gitignore @@ -0,0 +1,76 @@ +# Created by .ignore support plugin (hsz.mobi) +### 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: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.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 +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/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 +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock + +.dart_tool diff --git a/packages/merge_map/CHANGELOG.md b/packages/merge_map/CHANGELOG.md new file mode 100644 index 00000000..b57e6a38 --- /dev/null +++ b/packages/merge_map/CHANGELOG.md @@ -0,0 +1,7 @@ +# 1.0.2 +* Add an example, for Pub's sake. + +# 1.0.1 +* Add a specific constraint on Dart versions, to prevent Pub from rejecting all packages that depend on +`merge_map` (the entire Angel framework). +* Add generic type support diff --git a/packages/merge_map/LICENSE b/packages/merge_map/LICENSE new file mode 100644 index 00000000..5ee3381f --- /dev/null +++ b/packages/merge_map/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Tobe O + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/merge_map/README.md b/packages/merge_map/README.md new file mode 100644 index 00000000..dcc3cfeb --- /dev/null +++ b/packages/merge_map/README.md @@ -0,0 +1,19 @@ +# merge_map +Combine multiple Maps into one. Equivalent to +[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) +in JS. + +# Example + +```dart +import "package:merge_map/merge_map.dart"; + +main() { + Map map1 = {'hello': 'world'}; + Map map2 = {'foo': {'bar': 'baz', 'this': 'will be overwritten'}}; + Map map3 = {'foo': {'john': 'doe', 'this': 'overrides previous maps'}}; + Map merged = mergeMap(map1, map2, map3); + + // {hello: world, foo: {bar: baz, john: doe, this: overrides previous maps}} +} +``` \ No newline at end of file diff --git a/packages/merge_map/example/main.dart b/packages/merge_map/example/main.dart new file mode 100644 index 00000000..00b74c0d --- /dev/null +++ b/packages/merge_map/example/main.dart @@ -0,0 +1,11 @@ +import 'package:merge_map/merge_map.dart'; + +main() { + Map map1 = {'hello': 'world'}; + Map map2 = {'foo': {'bar': 'baz', 'this': 'will be overwritten'}}; + Map map3 = {'foo': {'john': 'doe', 'this': 'overrides previous maps'}}; + Map merged = mergeMap([map1, map2, map3]); + print(merged); + + // {hello: world, foo: {bar: baz, john: doe, this: overrides previous maps}} +} \ No newline at end of file diff --git a/packages/merge_map/lib/merge_map.dart b/packages/merge_map/lib/merge_map.dart new file mode 100644 index 00000000..c9d20ebf --- /dev/null +++ b/packages/merge_map/lib/merge_map.dart @@ -0,0 +1,34 @@ +/// Exposes the [mergeMap] function, which... merges Maps. +library merge_map; + +_copyValues( + Map from, Map? to, bool recursive, bool acceptNull) { + for (var key in from.keys) { + if (from[key] is Map && recursive) { + if (!(to![key] is Map)) { + to[key] = {} as V; + } + _copyValues(from[key] as Map, to[key] as Map?, recursive, acceptNull); + } else { + if (from[key] != null || acceptNull) to![key] = from[key]; + } + } +} + +/// Merges the values of the given maps together. +/// +/// `recursive` is set to `true` by default. If set to `true`, +/// then nested maps will also be merged. Otherwise, nested maps +/// will overwrite others. +/// +/// `acceptNull` is set to `false` by default. If set to `false`, +/// then if the value on a map is `null`, it will be ignored, and +/// that `null` will not be copied. +Map mergeMap(Iterable> maps, + {bool recursive: true, bool acceptNull: false}) { + Map result = {}; + maps.forEach((Map map) { + if (map != null) _copyValues(map, result, recursive, acceptNull); + }); + return result; +} diff --git a/packages/merge_map/pubspec.yaml b/packages/merge_map/pubspec.yaml new file mode 100644 index 00000000..c23b39fb --- /dev/null +++ b/packages/merge_map/pubspec.yaml @@ -0,0 +1,9 @@ +name: merge_map +description: Combine multiple Maps into one. Equivalent to Object.assign in JS. +version: 3.0.0 +homepage: https://github.com/thosakwe/merge_map +author: Tobe O +environment: + sdk: '>=2.12.0 <3.0.0' +dev_dependencies: + test: ^1.16.8 diff --git a/packages/merge_map/test/all_test.dart b/packages/merge_map/test/all_test.dart new file mode 100644 index 00000000..6ce20bd0 --- /dev/null +++ b/packages/merge_map/test/all_test.dart @@ -0,0 +1,94 @@ +import "package:merge_map/merge_map.dart"; +import "package:test/test.dart"; + +void main() { + test('can merge two simple maps', () { + Map merged = mergeMap([ + {'hello': 'world'}, + {'hello': 'dolly'} + ]); + expect(merged['hello'], equals('dolly')); + }); + + test("the last map's values supersede those of prior", () { + Map merged = mergeMap([ + {'letter': 'a'}, + {'letter': 'b'}, + {'letter': 'c'} + ]); + expect(merged['letter'], equals('c')); + }); + + test("can merge two once-nested maps", () { + Map map1 = { + 'hello': 'world', + 'foo': {'nested': false} + }; + Map map2 = { + 'goodbye': 'sad life', + 'foo': {'nested': true, 'it': 'works'} + }; + Map merged = mergeMap([map1, map2]); + + expect(merged['hello'], equals('world')); + expect(merged['goodbye'], equals('sad life')); + expect(merged['foo']['nested'], equals(true)); + expect(merged['foo']['it'], equals('works')); + }); + + test("once-nested map supersession", () { + Map map1 = { + 'hello': 'world', + 'foo': {'nested': false} + }; + Map map2 = { + 'goodbye': 'sad life', + 'foo': {'nested': true, 'it': 'works'} + }; + Map map3 = { + 'foo': {'nested': 'supersession'} + }; + + Map merged = mergeMap([map1, map2, map3]); + expect(merged['foo']['nested'], equals('supersession')); + }); + + test("can merge two twice-nested maps", () { + Map map1 = { + 'a': { + 'b': {'c': 'd'} + } + }; + Map map2 = { + 'a': { + 'b': {'c': 'D', 'e': 'f'} + } + }; + Map merged = mergeMap([map1, map2]); + + expect(merged['a']['b']['c'], equals('D')); + expect(merged['a']['b']['e'], equals('f')); + }); + + test("twice-nested map supersession", () { + Map map1 = { + 'a': { + 'b': {'c': 'd'} + } + }; + Map map2 = { + 'a': { + 'b': {'c': 'D', 'e': 'f'} + } + }; + Map map3 = { + 'a': { + 'b': {'e': 'supersession'} + } + }; + Map merged = mergeMap([map1, map2, map3]); + + expect(merged['a']['b']['c'], equals('D')); + expect(merged['a']['b']['e'], equals('supersession')); + }); +} diff --git a/packages/mock_request/.gitignore b/packages/mock_request/.gitignore new file mode 100644 index 00000000..8fff3906 --- /dev/null +++ b/packages/mock_request/.gitignore @@ -0,0 +1,72 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/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 +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock +### 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: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.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 +.dart_tool \ No newline at end of file diff --git a/packages/mock_request/.travis.yml b/packages/mock_request/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/packages/mock_request/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/packages/mock_request/CHANGELOG.md b/packages/mock_request/CHANGELOG.md new file mode 100644 index 00000000..f52fee60 --- /dev/null +++ b/packages/mock_request/CHANGELOG.md @@ -0,0 +1,17 @@ +# 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 00000000..3de28325 --- /dev/null +++ b/packages/mock_request/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Tobe O + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mock_request/README.md b/packages/mock_request/README.md new file mode 100644 index 00000000..8074b907 --- /dev/null +++ b/packages/mock_request/README.md @@ -0,0 +1,25 @@ +# mock_request + +[![Pub](https://img.shields.io/pub/v/mock_request.svg)](https://pub.dartlang.org/packages/mock_request) +[![build status](https://travis-ci.org/thosakwe/mock_request.svg)](https://travis-ci.org/thosakwe/mock_request) + +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 testing +[Angel](https://github.com/angel-dart/angel/wiki) +applications smoother, but works with any Dart-based server. :) + +# Usage +```dart +var rq = new 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 tests. \ No newline at end of file diff --git a/packages/mock_request/analysis_options.yaml b/packages/mock_request/analysis_options.yaml new file mode 100644 index 00000000..c230cee7 --- /dev/null +++ b/packages/mock_request/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false \ 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 00000000..152ab0ed --- /dev/null +++ b/packages/mock_request/example/main.dart @@ -0,0 +1,7 @@ +import 'dart:async'; +import 'package:mock_request/mock_request.dart'; + +Future main() async { + var rq = MockHttpRequest('GET', Uri.parse('/foo')); + await rq.close(); +} diff --git a/packages/mock_request/lib/mock_request.dart b/packages/mock_request/lib/mock_request.dart new file mode 100644 index 00000000..6cf677fa --- /dev/null +++ b/packages/mock_request/lib/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 00000000..ba7abe88 --- /dev/null +++ b/packages/mock_request/lib/src/connection_info.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +class MockHttpConnectionInfo implements HttpConnectionInfo { + @override + final InternetAddress remoteAddress; + @override + final int localPort, remotePort; + + MockHttpConnectionInfo({this.remoteAddress, this.localPort, this.remotePort}); +} diff --git a/packages/mock_request/lib/src/headers.dart b/packages/mock_request/lib/src/headers.dart new file mode 100644 index 00000000..913d1f1d --- /dev/null +++ b/packages/mock_request/lib/src/headers.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +class MockHttpHeaders extends HttpHeaders { + final Map> _data = {}; + final List _noFolding = []; + Uri _host; + + List get doNotFold => List.unmodifiable(_noFolding); + + @override + ContentType get contentType { + if (_data.containsKey(HttpHeaders.contentTypeHeader)) { + return ContentType.parse(_data[HttpHeaders.contentTypeHeader].join(',')); + } else { + return null; + } + } + + @override + set contentType(ContentType value) => + set(HttpHeaders.contentTypeHeader, value.value); + + @override + DateTime get date => _data.containsKey(HttpHeaders.dateHeader) + ? HttpDate.parse(_data[HttpHeaders.dateHeader].join(',')) + : null; + + @override + set date(DateTime value) => + set(HttpHeaders.dateHeader, HttpDate.format(value)); + + @override + DateTime get expires => _data.containsKey(HttpHeaders.expiresHeader) + ? HttpDate.parse(_data[HttpHeaders.expiresHeader].join(',')) + : null; + + @override + set expires(DateTime value) => + set(HttpHeaders.expiresHeader, HttpDate.format(value)); + + @override + DateTime get ifModifiedSince => + _data.containsKey(HttpHeaders.ifModifiedSinceHeader) + ? HttpDate.parse(_data[HttpHeaders.ifModifiedSinceHeader].join(',')) + : null; + + @override + set ifModifiedSince(DateTime value) => + set(HttpHeaders.ifModifiedSinceHeader, HttpDate.format(value)); + + @override + String get host { + 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 { + host; // Parse it + return _host?.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) f) { + _data.forEach(f); + } + + @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(); + } +} 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 00000000..59b12e3b --- /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 00000000..5b976826 --- /dev/null +++ b/packages/mock_request/lib/src/request.dart @@ -0,0 +1,317 @@ +import 'dart:async'; +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; + BytesBuilder _buf; + final Completer _done = Completer(); + final LockableMockHttpHeaders _headers = LockableMockHttpHeaders(); + Uri _requestedUri; + MockHttpSession _session; + final StreamController _stream = StreamController(); + + @override + final List cookies = []; + + @override + HttpConnectionInfo connectionInfo = + MockHttpConnectionInfo(remoteAddress: InternetAddress.loopbackIPv4); + + @override + MockHttpResponse response = MockHttpResponse(); + + @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}) { + _buf = BytesBuilder(copy: copyBuffer != false); + _session = MockHttpSession(id: sessionId ?? 'mock-http-session'); + this.protocolVersion = + protocolVersion?.isNotEmpty == true ? 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 + 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 00000000..bfc12d0f --- /dev/null +++ b/packages/mock_request/lib/src/response.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +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, + this.statusCode, + this.reasonPhrase, + this.contentLength, + this.deadline, + this.encoding, + 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 ?? HttpStatus.movedTemporarily; + } + + @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 00000000..387341ee --- /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({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) f) { + _data.forEach(f); + } + + @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 00000000..4be6ff57 --- /dev/null +++ b/packages/mock_request/pubspec.yaml @@ -0,0 +1,13 @@ +name: mock_request +version: 2.0.0 +description: Manufacture dart:io HttpRequests, HttpResponses, HttpHeaders, etc. +author: Tobe O +homepage: https://github.com/thosakwe/mock_request +environment: + sdk: ">=2.0.0 <3.0.0" +dependencies: + charcode: ">=1.0.0 <2.0.0" +dev_dependencies: + angel_framework: ^2.1.0 + http: ^0.12.0 + test: ^1.16.8 diff --git a/packages/mock_request/test/all_test.dart b/packages/mock_request/test/all_test.dart new file mode 100644 index 00000000..97acd39b --- /dev/null +++ b/packages/mock_request/test/all_test.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; +import 'package:mock_request/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'); + }); +}