Add 'packages/test/' from commit '7508913df7203436eeddb271a53fb922aaeb77db'
git-subtree-dir: packages/test git-subtree-mainline:c6d7d5f416
git-subtree-split:7508913df7
This commit is contained in:
commit
5834fbe416
18 changed files with 1048 additions and 0 deletions
74
packages/test/.gitignore
vendored
Normal file
74
packages/test/.gitignore
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
.scripts-bin/
|
||||
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
|
||||
|
||||
# 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
|
17
packages/test/.idea/angel_test.iml
Normal file
17
packages/test/.idea/angel_test.iml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
7
packages/test/.idea/jsLibraryMappings.xml
Normal file
7
packages/test/.idea/jsLibraryMappings.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="ECMAScript 6" />
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
28
packages/test/.idea/misc.xml
Normal file
28
packages/test/.idea/misc.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||
<entry key="Project Default">
|
||||
<profile-state>
|
||||
<expanded-state>
|
||||
<State>
|
||||
<id />
|
||||
</State>
|
||||
<State>
|
||||
<id>General</id>
|
||||
</State>
|
||||
<State>
|
||||
<id>XPath</id>
|
||||
</State>
|
||||
</expanded-state>
|
||||
<selected-state>
|
||||
<State>
|
||||
<id>AngularJS</id>
|
||||
</State>
|
||||
</selected-state>
|
||||
</profile-state>
|
||||
</entry>
|
||||
</component>
|
||||
</project>
|
8
packages/test/.idea/modules.xml
Normal file
8
packages/test/.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/angel_test.iml" filepath="$PROJECT_DIR$/.idea/angel_test.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
7
packages/test/.idea/runConfigurations/Simple_Tests.xml
Normal file
7
packages/test/.idea/runConfigurations/Simple_Tests.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Simple Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/simple_test.dart" />
|
||||
<option name="testRunnerOptions" value="-j 4" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
6
packages/test/.idea/vcs.xml
Normal file
6
packages/test/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
4
packages/test/.travis.yml
Normal file
4
packages/test/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- dev
|
||||
- stable
|
23
packages/test/CHANGELOG.md
Normal file
23
packages/test/CHANGELOG.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# 2.0.1
|
||||
* Update badge.
|
||||
* Handle userInfo + basic auth.
|
||||
|
||||
# 2.0.0
|
||||
* Update to work with `client@2`.
|
||||
|
||||
# 2.0.0-alpha.4
|
||||
# 2.0.0-alpha.3
|
||||
* Update `http` dependency.
|
||||
|
||||
# 2.0.0-alpha.2
|
||||
* Explicitly import `package:angel_framework/http.dart`.
|
||||
|
||||
# 2.0.0-alpha.1
|
||||
* Update for compliance with newer `angel_client`.
|
||||
|
||||
# 2.0.0-alpha
|
||||
* Depend on Dart 2 and Angel 2.
|
||||
|
||||
# 1.1.0+1
|
||||
* Dart 2/strong mode fixes.
|
||||
* Pass a `useZone` flag to `AngelHttp` through `TestServer`.
|
21
packages/test/LICENSE
Normal file
21
packages/test/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 The Angel Framework
|
||||
|
||||
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.
|
72
packages/test/README.md
Normal file
72
packages/test/README.md
Normal file
|
@ -0,0 +1,72 @@
|
|||
# angel_test
|
||||
[![Pub](https://img.shields.io/pub/v/angel_test.svg)](https://pub.dartlang.org/packages/angel_test)
|
||||
[![build status](https://travis-ci.org/angel-dart/test.svg)](https://travis-ci.org/angel-dart/test)
|
||||
|
||||
Testing utility library for the Angel framework.
|
||||
|
||||
# TestClient
|
||||
The `TestClient` class is a custom `angel_client` that sends mock requests to your server.
|
||||
This means that you will not have to bind your server to HTTP to run.
|
||||
Plus, it is an `angel_client`, and thus supports services and other goodies.
|
||||
|
||||
The `TestClient` also supports WebSockets. WebSockets cannot be mocked (yet!) within this library,
|
||||
so calling the `websocket()` function will also bind your server to HTTP, if it is not already listening.
|
||||
|
||||
The return value is a `WebSockets` client instance
|
||||
(from [`package:angel_websocket`](https://github.com/angel-dart/websocket));
|
||||
|
||||
```dart
|
||||
var ws = await client.websocket('/ws');
|
||||
ws.service('api/users').onCreated.listen(...);
|
||||
|
||||
// To receive all blobs of data sent on the WebSocket:
|
||||
ws.onData.listen(...);
|
||||
```
|
||||
|
||||
# Matchers
|
||||
Several `Matcher`s are bundled with this package, and run on any `package:http` `Response`,
|
||||
not just those returned by Angel.
|
||||
|
||||
```dart
|
||||
test('foo', () async {
|
||||
var res = await client.get('/foo');
|
||||
expect(res, allOf([
|
||||
isJson({'foo': 'bar'}),
|
||||
hasStatus(200),
|
||||
hasContentType(ContentType.JSON),
|
||||
hasContentType('application/json'),
|
||||
hasHeader('server'), // Assert header present
|
||||
hasHeader('server', 'angel'), // Assert header present with value
|
||||
hasHeader('foo', ['bar', 'baz']), // ... Or multiple values
|
||||
hasBody(), // Assert non-empty body
|
||||
hasBody('{"foo":"bar"}') // Assert specific body
|
||||
]));
|
||||
});
|
||||
|
||||
test('error', () async {
|
||||
var res = await client.get('/error');
|
||||
expect(res, isAngelHttpException());
|
||||
expect(res, isAngelHttpException(statusCode: 404, message: ..., errors: [...])) // Optional
|
||||
});
|
||||
```
|
||||
|
||||
`hasValidBody` is one of the most powerful `Matcher`s in this library,
|
||||
because it allows you to validate a JSON body against a
|
||||
[validation schema](https://github.com/angel-dart/validate).
|
||||
|
||||
Angel provides a comprehensive validation library that integrates tightly
|
||||
with the very `matcher` package that you already use for testing. :)
|
||||
|
||||
[https://github.com/angel-dart/validate](https://github.com/angel-dart/validate)
|
||||
|
||||
```dart
|
||||
test('validate response', () async {
|
||||
var res = await client.get('/bar');
|
||||
expect(res, hasValidBody(new Validator({
|
||||
'foo': isBoolean,
|
||||
'bar': [isString, equals('baz')],
|
||||
'age*': [],
|
||||
'nested': someNestedValidator
|
||||
})));
|
||||
});
|
||||
```
|
3
packages/test/analysis_options.yaml
Normal file
3
packages/test/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
142
packages/test/example/main.dart
Normal file
142
packages/test/example/main.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:angel_websocket/server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel()
|
||||
..get('/hello', (req, res) => 'Hello')
|
||||
..get(
|
||||
'/error',
|
||||
(req, res) => throw new AngelHttpException.forbidden(message: 'Test')
|
||||
..errors.addAll(['foo', 'bar']))
|
||||
..get('/body', (req, res) {
|
||||
res
|
||||
..write('OK')
|
||||
..close();
|
||||
})
|
||||
..get(
|
||||
'/valid',
|
||||
(req, res) => {
|
||||
'michael': 'jackson',
|
||||
'billie': {'jean': 'hee-hee', 'is_my_lover': false}
|
||||
})
|
||||
..post('/hello', (req, res) async {
|
||||
var body = await req.parseBody().then((_) => req.bodyAsMap);
|
||||
return {'bar': body['foo']};
|
||||
})
|
||||
..get('/gzip', (req, res) async {
|
||||
res
|
||||
..headers['content-encoding'] = 'gzip'
|
||||
..add(gzip.encode('Poop'.codeUnits))
|
||||
..close();
|
||||
})
|
||||
..use(
|
||||
'/foo',
|
||||
new AnonymousService(
|
||||
index: ([params]) async => [
|
||||
{'michael': 'jackson'}
|
||||
],
|
||||
create: (data, [params]) async => {'foo': 'bar'}));
|
||||
|
||||
var ws = new AngelWebSocket(app);
|
||||
await app.configure(ws.configureServer);
|
||||
app.all('/ws', ws.handleRequest);
|
||||
|
||||
app.errorHandler = (e, req, res) => e.toJson();
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.close();
|
||||
app = null;
|
||||
});
|
||||
|
||||
group('matchers', () {
|
||||
group('isJson+hasStatus', () {
|
||||
test('get', () async {
|
||||
final response = await client.get('/hello');
|
||||
expect(response, isJson('Hello'));
|
||||
});
|
||||
|
||||
test('post', () async {
|
||||
final response = await client.post('/hello', body: {'foo': 'baz'});
|
||||
expect(response, allOf(hasStatus(200), isJson({'bar': 'baz'})));
|
||||
});
|
||||
});
|
||||
|
||||
test('isAngelHttpException', () async {
|
||||
var res = await client.get('/error');
|
||||
print(res.body);
|
||||
expect(res, isAngelHttpException());
|
||||
expect(
|
||||
res,
|
||||
isAngelHttpException(
|
||||
statusCode: 403, message: 'Test', errors: ['foo', 'bar']));
|
||||
});
|
||||
|
||||
test('hasBody', () async {
|
||||
var res = await client.get('/body');
|
||||
expect(res, hasBody());
|
||||
expect(res, hasBody('OK'));
|
||||
});
|
||||
|
||||
test('hasHeader', () async {
|
||||
var res = await client.get('/hello');
|
||||
expect(res, hasHeader('server'));
|
||||
expect(res, hasHeader('server', 'angel'));
|
||||
expect(res, hasHeader('server', ['angel']));
|
||||
});
|
||||
|
||||
test('hasValidBody+hasContentType', () async {
|
||||
var res = await client.get('/valid');
|
||||
expect(res, hasContentType('application/json'));
|
||||
expect(
|
||||
res,
|
||||
hasValidBody(new Validator({
|
||||
'michael*': [isString, isNotEmpty, equals('jackson')],
|
||||
'billie': new Validator({
|
||||
'jean': [isString, isNotEmpty],
|
||||
'is_my_lover': [isBool, isFalse]
|
||||
})
|
||||
})));
|
||||
});
|
||||
|
||||
test('gzip decode', () async {
|
||||
var res = await client.get('/gzip');
|
||||
expect(res, hasHeader('content-encoding', 'gzip'));
|
||||
expect(res, hasBody('Poop'));
|
||||
});
|
||||
|
||||
group('service', () {
|
||||
test('index', () async {
|
||||
var foo = client.service('foo');
|
||||
var result = await foo.index();
|
||||
expect(result, [
|
||||
{'michael': 'jackson'}
|
||||
]);
|
||||
});
|
||||
|
||||
test('index', () async {
|
||||
var foo = client.service('foo');
|
||||
var result = await foo.create({});
|
||||
expect(result, {'foo': 'bar'});
|
||||
});
|
||||
});
|
||||
|
||||
test('websocket', () async {
|
||||
var ws = await client.websocket();
|
||||
var foo = ws.service('foo');
|
||||
foo.create({});
|
||||
var result = await foo.onCreated.first;
|
||||
expect(result.data, equals({'foo': 'bar'}));
|
||||
});
|
||||
});
|
||||
}
|
2
packages/test/lib/angel_test.dart
Normal file
2
packages/test/lib/angel_test.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/client.dart';
|
||||
export 'src/matchers.dart';
|
193
packages/test/lib/src/client.dart
Normal file
193
packages/test/lib/src/client.dart
Normal file
|
@ -0,0 +1,193 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_client/base_angel_client.dart' as client;
|
||||
import 'package:angel_client/io.dart' as client;
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_websocket/io.dart' as client;
|
||||
import 'package:http/http.dart' as http hide StreamedResponse;
|
||||
import 'package:http/io_client.dart' as http;
|
||||
import 'package:http/src/streamed_response.dart';
|
||||
import 'package:mock_request/mock_request.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
//import 'package:uuid/uuid.dart';
|
||||
|
||||
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
/*const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
|
||||
const Map<String, String> _writeHeaders = const {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
final Uuid _uuid = new Uuid();*/
|
||||
|
||||
/// Shorthand for bootstrapping a [TestClient].
|
||||
Future<TestClient> connectTo(Angel app,
|
||||
{Map initialSession,
|
||||
bool autoDecodeGzip: true,
|
||||
bool useZone: false}) async {
|
||||
if (!app.isProduction) app.configuration.putIfAbsent('testMode', () => true);
|
||||
|
||||
for (var plugin in app.startupHooks) await plugin(app);
|
||||
return new TestClient(app,
|
||||
autoDecodeGzip: autoDecodeGzip != false, useZone: useZone)
|
||||
..session.addAll(initialSession ?? {});
|
||||
}
|
||||
|
||||
/// An `angel_client` that sends mock requests to a server, rather than actual HTTP transactions.
|
||||
class TestClient extends client.BaseAngelClient {
|
||||
final Map<String, client.Service> _services = {};
|
||||
|
||||
/// Session info to be sent to the server on every request.
|
||||
final HttpSession session = new MockHttpSession(id: 'angel-test-client');
|
||||
|
||||
/// A list of cookies to be sent to and received from the server.
|
||||
final List<Cookie> cookies = [];
|
||||
|
||||
/// If `true` (default), the client will automatically decode GZIP response bodies.
|
||||
final bool autoDecodeGzip;
|
||||
|
||||
/// The server instance to mock.
|
||||
final Angel server;
|
||||
|
||||
@override
|
||||
String authToken;
|
||||
|
||||
AngelHttp _http;
|
||||
|
||||
TestClient(this.server, {this.autoDecodeGzip: true, bool useZone: false})
|
||||
: super(new http.IOClient(), '/') {
|
||||
_http = new AngelHttp(server, useZone: useZone);
|
||||
}
|
||||
|
||||
Future close() {
|
||||
this.client.close();
|
||||
return server.close();
|
||||
}
|
||||
|
||||
/// Opens a WebSockets connection to the server. This will automatically bind the server
|
||||
/// over HTTP, if it is not already listening. Unfortunately, WebSockets cannot be mocked (yet!).
|
||||
Future<client.WebSockets> websocket(
|
||||
{String path: '/ws', Duration timeout}) async {
|
||||
if (_http.server == null) await _http.startServer();
|
||||
var url = _http.uri.replace(scheme: 'ws', path: path);
|
||||
var ws = new _MockWebSockets(this, url.toString());
|
||||
await ws.connect(timeout: timeout);
|
||||
return ws;
|
||||
}
|
||||
|
||||
Future<StreamedResponse> send(http.BaseRequest request) async {
|
||||
var rq = new MockHttpRequest(request.method, request.url);
|
||||
request.headers.forEach(rq.headers.add);
|
||||
|
||||
if (request.url.userInfo.isNotEmpty) {
|
||||
// Attempt to send as Basic auth
|
||||
var encoded = base64Url.encode(utf8.encode(request.url.userInfo));
|
||||
rq.headers.add('authorization', 'Basic $encoded');
|
||||
} else if (rq.headers.value('authorization')?.startsWith('Basic ') ==
|
||||
true) {
|
||||
var encoded = rq.headers.value('authorization').substring(6);
|
||||
var decoded = utf8.decode(base64Url.decode(encoded));
|
||||
var oldRq = rq;
|
||||
var newRq =
|
||||
new MockHttpRequest(rq.method, rq.uri.replace(userInfo: decoded));
|
||||
oldRq.headers.forEach(newRq.headers.add);
|
||||
rq = newRq;
|
||||
}
|
||||
|
||||
if (authToken?.isNotEmpty == true)
|
||||
rq.headers.add('authorization', 'Bearer $authToken');
|
||||
|
||||
rq..cookies.addAll(cookies)..session.addAll(session);
|
||||
|
||||
await request.finalize().pipe(rq);
|
||||
|
||||
await _http.handleRequest(rq);
|
||||
|
||||
var rs = rq.response;
|
||||
session
|
||||
..clear()
|
||||
..addAll(rq.session);
|
||||
|
||||
Map<String, String> extractedHeaders = {};
|
||||
|
||||
rs.headers.forEach((k, v) {
|
||||
extractedHeaders[k] = v.join(',');
|
||||
});
|
||||
|
||||
Stream<List<int>> stream = rs;
|
||||
|
||||
if (autoDecodeGzip != false &&
|
||||
rs.headers['content-encoding']?.contains('gzip') == true) {
|
||||
stream = stream.transform(gzip.decoder);
|
||||
}
|
||||
|
||||
return new StreamedResponse(stream, rs.statusCode,
|
||||
contentLength: rs.contentLength,
|
||||
isRedirect: rs.headers['location'] != null,
|
||||
headers: extractedHeaders,
|
||||
persistentConnection:
|
||||
rq.headers.value('connection')?.toLowerCase()?.trim() ==
|
||||
'keep-alive' ||
|
||||
rq.headers.persistentConnection == true,
|
||||
reasonPhrase: rs.reasonPhrase);
|
||||
}
|
||||
|
||||
@override
|
||||
String basePath;
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url, {String eventName: 'token'}) {
|
||||
throw new UnsupportedError(
|
||||
'MockClient does not support authentication via popup.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future configure(client.AngelConfigurer configurer) =>
|
||||
new Future.sync(() => configurer(this));
|
||||
|
||||
@override
|
||||
client.Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, client.AngelDeserializer<Data> deserializer}) {
|
||||
String uri = path.toString().replaceAll(_straySlashes, "");
|
||||
return _services.putIfAbsent(
|
||||
uri,
|
||||
() => new _MockService<Id, Data>(this, uri,
|
||||
deserializer: deserializer)) as client.Service<Id, Data>;
|
||||
}
|
||||
}
|
||||
|
||||
class _MockService<Id, Data> extends client.BaseAngelService<Id, Data> {
|
||||
final TestClient _app;
|
||||
|
||||
_MockService(this._app, String basePath,
|
||||
{client.AngelDeserializer<Data> deserializer})
|
||||
: super(null, _app, basePath, deserializer: deserializer);
|
||||
|
||||
@override
|
||||
Future<StreamedResponse> send(http.BaseRequest request) {
|
||||
if (app.authToken != null && app.authToken.isNotEmpty) {
|
||||
request.headers['authorization'] ??= 'Bearer ${app.authToken}';
|
||||
}
|
||||
|
||||
return _app.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
class _MockWebSockets extends client.WebSockets {
|
||||
final TestClient app;
|
||||
|
||||
_MockWebSockets(this.app, String url) : super(url);
|
||||
|
||||
@override
|
||||
Future<WebSocketChannel> getConnectedWebSocket() async {
|
||||
Map<String, String> headers = {};
|
||||
|
||||
if (app.authToken?.isNotEmpty == true)
|
||||
headers['authorization'] = 'Bearer ${app.authToken}';
|
||||
|
||||
var socket = await WebSocket.connect(baseUrl.toString(), headers: headers);
|
||||
return new IOWebSocketChannel(socket);
|
||||
}
|
||||
}
|
253
packages/test/lib/src/matchers.dart
Normal file
253
packages/test/lib/src/matchers.dart
Normal file
|
@ -0,0 +1,253 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matcher/matcher.dart';
|
||||
|
||||
/// Expects a response to be a JSON representation of an `AngelHttpException`.
|
||||
///
|
||||
/// You can optionally check for a matching [message], [statusCode] and [errors].
|
||||
Matcher isAngelHttpException(
|
||||
{String message, int statusCode, Iterable<String> errors: const []}) =>
|
||||
new _IsAngelHttpException(
|
||||
message: message, statusCode: statusCode, errors: errors);
|
||||
|
||||
/// Expects a given response, when parsed as JSON,
|
||||
/// to equal a desired value.
|
||||
Matcher isJson(value) => new _IsJson(value);
|
||||
|
||||
/// Expects a response to have the given content type, whether a `String` or [ContentType].
|
||||
Matcher hasContentType(contentType) => new _HasContentType(contentType);
|
||||
|
||||
/// Expects a response to have the given body.
|
||||
///
|
||||
/// If `true` is passed as the value (default), then this matcher will simply assert
|
||||
/// that the response has a non-empty body.
|
||||
///
|
||||
/// If value is a `List<int>`, then it will be matched against `res.bodyBytes`.
|
||||
/// Otherwise, the string value will be matched against `res.body`.
|
||||
Matcher hasBody([value]) => new _HasBody(value ?? true);
|
||||
|
||||
/// Expects a response to have a header named [key] which contains [value]. [value] can be a `String`, or a List of `String`s.
|
||||
///
|
||||
/// If `value` is true (default), then this matcher will simply assert that the header is present.
|
||||
Matcher hasHeader(String key, [value]) => new _HasHeader(key, value ?? true);
|
||||
|
||||
/// Expects a response to have the given status code.
|
||||
Matcher hasStatus(int status) => new _HasStatus(status);
|
||||
|
||||
/// Expects a response to have a JSON body that is a `Map` and satisfies the given [validator] schema.
|
||||
Matcher hasValidBody(Validator validator) => new _HasValidBody(validator);
|
||||
|
||||
class _IsJson extends Matcher {
|
||||
var value;
|
||||
|
||||
_IsJson(this.value);
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
return description.add('equals the desired JSON response: $value');
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) =>
|
||||
item is http.Response &&
|
||||
equals(value).matches(json.decode(item.body), matchState);
|
||||
}
|
||||
|
||||
class _HasBody extends Matcher {
|
||||
final body;
|
||||
|
||||
_HasBody(this.body);
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('has body $body');
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
if (item is http.Response) {
|
||||
if (body == true) return isNotEmpty.matches(item.bodyBytes, matchState);
|
||||
if (body is List<int>)
|
||||
return equals(body).matches(item.bodyBytes, matchState);
|
||||
else
|
||||
return equals(body.toString()).matches(item.body, matchState);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HasContentType extends Matcher {
|
||||
var contentType;
|
||||
|
||||
_HasContentType(this.contentType);
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
var str = contentType is ContentType
|
||||
? ((contentType as ContentType).value)
|
||||
: contentType.toString();
|
||||
return description.add('has content type ' + str);
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
if (item is http.Response) {
|
||||
if (!item.headers.containsKey('content-type')) return false;
|
||||
|
||||
if (contentType is ContentType) {
|
||||
var compare = ContentType.parse(item.headers['content-type']);
|
||||
return equals(contentType.mimeType)
|
||||
.matches(compare.mimeType, matchState);
|
||||
} else {
|
||||
return equals(contentType.toString())
|
||||
.matches(item.headers['content-type'], matchState);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HasHeader extends Matcher {
|
||||
final String key;
|
||||
final value;
|
||||
|
||||
_HasHeader(this.key, this.value);
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
if (value == true)
|
||||
return description.add('contains header $key');
|
||||
else
|
||||
return description.add('contains header $key with value(s) $value');
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
if (item is http.Response) {
|
||||
if (value == true) {
|
||||
return contains(key.toLowerCase())
|
||||
.matches(item.headers.keys, matchState);
|
||||
} else {
|
||||
if (!item.headers.containsKey(key.toLowerCase())) return false;
|
||||
Iterable v = value is Iterable ? (value as Iterable) : [value];
|
||||
return v
|
||||
.map((x) => x.toString())
|
||||
.every(item.headers[key.toLowerCase()].split(',').contains);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _HasStatus extends Matcher {
|
||||
int status;
|
||||
|
||||
_HasStatus(this.status);
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
return description.add('has status code $status');
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) =>
|
||||
item is http.Response &&
|
||||
equals(status).matches(item.statusCode, matchState);
|
||||
}
|
||||
|
||||
class _HasValidBody extends Matcher {
|
||||
final Validator validator;
|
||||
|
||||
_HasValidBody(this.validator);
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('matches validation schema ${validator.rules}');
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
if (item is http.Response) {
|
||||
final jsons = json.decode(item.body);
|
||||
if (jsons is! Map) return false;
|
||||
return validator.matches(jsons, matchState);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IsAngelHttpException extends Matcher {
|
||||
String message;
|
||||
int statusCode;
|
||||
final List<String> errors = [];
|
||||
|
||||
_IsAngelHttpException(
|
||||
{this.message, this.statusCode, Iterable<String> errors: const []}) {
|
||||
this.errors.addAll(errors ?? []);
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
if (message?.isNotEmpty != true && statusCode == null && errors.isEmpty)
|
||||
return description.add('is an Angel HTTP Exception');
|
||||
else {
|
||||
var buf = new StringBuffer('is an Angel HTTP Exception with');
|
||||
|
||||
if (statusCode != null) buf.write(' status code $statusCode');
|
||||
|
||||
if (message?.isNotEmpty == true) {
|
||||
if (statusCode != null && errors.isNotEmpty)
|
||||
buf.write(',');
|
||||
else if (statusCode != null && errors.isEmpty) buf.write(' and');
|
||||
buf.write(' message "$message"');
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
if (statusCode != null || message?.isNotEmpty == true)
|
||||
buf.write(' and errors $errors');
|
||||
else
|
||||
buf.write(' errors $errors');
|
||||
}
|
||||
|
||||
return description.add(buf.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
if (item is http.Response) {
|
||||
final jsons = json.decode(item.body);
|
||||
|
||||
if (jsons is Map && jsons['isError'] == true) {
|
||||
var exc = new AngelHttpException.fromMap(jsons);
|
||||
print(exc.toJson());
|
||||
|
||||
if (message?.isNotEmpty != true && statusCode == null && errors.isEmpty)
|
||||
return true;
|
||||
else {
|
||||
if (statusCode != null) if (!equals(statusCode)
|
||||
.matches(exc.statusCode, matchState)) return false;
|
||||
|
||||
if (message?.isNotEmpty == true) if (!equals(message)
|
||||
.matches(exc.message, matchState)) return false;
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
if (!errors
|
||||
.every((err) => contains(err).matches(exc.errors, matchState)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} else
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
19
packages/test/pubspec.yaml
Normal file
19
packages/test/pubspec.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
author: Tobe O <thosakwe@gmail.com>
|
||||
description: Testing utility library for the Angel framework. Use with package:test.
|
||||
homepage: https://github.com/angel-dart/test.git
|
||||
name: angel_test
|
||||
version: 2.0.1
|
||||
dependencies:
|
||||
angel_client: ^2.0.0
|
||||
angel_framework: ^2.0.0-alpha
|
||||
angel_http_exception: ^1.0.0
|
||||
angel_validate: ^2.0.0
|
||||
angel_websocket: ^2.0.0
|
||||
http: ^0.12.0
|
||||
matcher: ^0.12.0
|
||||
mock_request: ^1.0.0
|
||||
web_socket_channel: ^1.0.0
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
169
packages/test/test/simple_test.dart
Normal file
169
packages/test/test/simple_test.dart
Normal file
|
@ -0,0 +1,169 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:angel_validate/angel_validate.dart';
|
||||
import 'package:angel_websocket/server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel()
|
||||
..get('/hello', (req, res) => 'Hello')
|
||||
..get('/user_info', (req, res) => {'u': req.uri.userInfo})
|
||||
..get(
|
||||
'/error',
|
||||
(req, res) => throw new AngelHttpException.forbidden(message: 'Test')
|
||||
..errors.addAll(['foo', 'bar']))
|
||||
..get('/body', (req, res) {
|
||||
res
|
||||
..write('OK')
|
||||
..close();
|
||||
})
|
||||
..get(
|
||||
'/valid',
|
||||
(req, res) => {
|
||||
'michael': 'jackson',
|
||||
'billie': {'jean': 'hee-hee', 'is_my_lover': false}
|
||||
})
|
||||
..post('/hello', (req, res) async {
|
||||
var body = await req.parseBody().then((_) => req.bodyAsMap);
|
||||
return {'bar': body['foo']};
|
||||
})
|
||||
..get('/gzip', (req, res) async {
|
||||
res
|
||||
..headers['content-encoding'] = 'gzip'
|
||||
..add(gzip.encode('Poop'.codeUnits))
|
||||
..close();
|
||||
})
|
||||
..use(
|
||||
'/foo',
|
||||
new AnonymousService<String, Map<String, dynamic>>(
|
||||
index: ([params]) async => [
|
||||
<String, dynamic>{'michael': 'jackson'}
|
||||
],
|
||||
create: (data, [params]) async =>
|
||||
<String, dynamic>{'foo': 'bar'}));
|
||||
|
||||
var ws = new AngelWebSocket(app);
|
||||
await app.configure(ws.configureServer);
|
||||
app.all('/ws', ws.handleRequest);
|
||||
|
||||
app.errorHandler = (e, req, res) => e.toJson();
|
||||
|
||||
client = await connectTo(app, useZone: false);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.close();
|
||||
app = null;
|
||||
});
|
||||
|
||||
group('matchers', () {
|
||||
group('isJson+hasStatus', () {
|
||||
test('get', () async {
|
||||
final response = await client.get('/hello');
|
||||
expect(response, isJson('Hello'));
|
||||
});
|
||||
|
||||
test('post', () async {
|
||||
final response = await client.post('/hello', body: {'foo': 'baz'});
|
||||
expect(response, allOf(hasStatus(200), isJson({'bar': 'baz'})));
|
||||
});
|
||||
});
|
||||
|
||||
test('isAngelHttpException', () async {
|
||||
var res = await client.get('/error');
|
||||
print(res.body);
|
||||
expect(res, isAngelHttpException());
|
||||
expect(
|
||||
res,
|
||||
isAngelHttpException(
|
||||
statusCode: 403, message: 'Test', errors: ['foo', 'bar']));
|
||||
});
|
||||
|
||||
test('userInfo from Uri', () async {
|
||||
var url = new Uri(userInfo: 'foo:bar', path: '/user_info');
|
||||
print('URL: $url');
|
||||
var res = await client.get(url);
|
||||
print(res.body);
|
||||
var m = json.decode(res.body) as Map;
|
||||
expect(m, {'u': 'foo:bar'});
|
||||
});
|
||||
|
||||
test('userInfo from Basic auth header', () async {
|
||||
var url = new Uri(path: '/user_info');
|
||||
print('URL: $url');
|
||||
var res = await client.get(url, headers: {
|
||||
'authorization': 'Basic ' + (base64Url.encode(utf8.encode('foo:bar')))
|
||||
});
|
||||
print(res.body);
|
||||
var m = json.decode(res.body) as Map;
|
||||
expect(m, {'u': 'foo:bar'});
|
||||
});
|
||||
|
||||
test('hasBody', () async {
|
||||
var res = await client.get('/body');
|
||||
expect(res, hasBody());
|
||||
expect(res, hasBody('OK'));
|
||||
});
|
||||
|
||||
test('hasHeader', () async {
|
||||
var res = await client.get('/hello');
|
||||
expect(res, hasHeader('server'));
|
||||
expect(res, hasHeader('server', 'angel'));
|
||||
expect(res, hasHeader('server', ['angel']));
|
||||
});
|
||||
|
||||
test('hasValidBody+hasContentType', () async {
|
||||
var res = await client.get('/valid');
|
||||
print('Body: ${res.body}');
|
||||
expect(res, hasContentType('application/json'));
|
||||
expect(res, hasContentType(new ContentType('application', 'json')));
|
||||
expect(
|
||||
res,
|
||||
hasValidBody(new Validator({
|
||||
'michael*': [isString, isNotEmpty, equals('jackson')],
|
||||
'billie': new Validator({
|
||||
'jean': [isString, isNotEmpty],
|
||||
'is_my_lover': [isBool, isFalse]
|
||||
})
|
||||
})));
|
||||
});
|
||||
|
||||
test('gzip decode', () async {
|
||||
var res = await client.get('/gzip');
|
||||
print('Body: ${res.body}');
|
||||
expect(res, hasHeader('content-encoding', 'gzip'));
|
||||
expect(res, hasBody('Poop'));
|
||||
});
|
||||
|
||||
group('service', () {
|
||||
test('index', () async {
|
||||
var foo = client.service('foo');
|
||||
var result = await foo.index();
|
||||
expect(result, [
|
||||
<String, dynamic>{'michael': 'jackson'}
|
||||
]);
|
||||
});
|
||||
|
||||
test('index', () async {
|
||||
var foo = client.service('foo');
|
||||
var result = await foo.create({});
|
||||
expect(result, <String, dynamic>{'foo': 'bar'});
|
||||
});
|
||||
});
|
||||
|
||||
test('websocket', () async {
|
||||
var ws = await client.websocket();
|
||||
var foo = ws.service('foo');
|
||||
foo.create(<String, dynamic>{});
|
||||
var result = await foo.onCreated.first;
|
||||
expect(result is Map ? result : result.data,
|
||||
equals(<String, dynamic>{'foo': 'bar'}));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue