1.0.2
This commit is contained in:
parent
e2ab72ba96
commit
7f709fb91c
5 changed files with 516 additions and 130 deletions
71
README.md
71
README.md
|
@ -1,7 +1,72 @@
|
||||||
# angel_test
|
# angel_test
|
||||||
[![version 1.0.1](https://img.shields.io/badge/pub-1.0.1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_test)
|
[![version 1.0.2](https://img.shields.io/badge/pub-1.0.2-brightgreen.svg)](https://pub.dartlang.org/packages/angel_test)
|
||||||
[![build status](https://travis-ci.org/angel-dart/test.svg?branch=master)](https://travis-ci.org/angel-dart/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.
|
Testing utility library for the Angel framework.
|
||||||
|
|
||||||
See the tests for examples.
|
# 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
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
```
|
|
@ -1,104 +1,198 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:angel_client/angel_client.dart' show AngelAuthResult;
|
||||||
|
import 'package:angel_client/base_angel_client.dart' as client;
|
||||||
import 'package:angel_client/io.dart' as client;
|
import 'package:angel_client/io.dart' as client;
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_websocket/io.dart' as client;
|
||||||
|
import 'package:http/src/base_request.dart' as http;
|
||||||
|
import 'package:http/src/response.dart' as http;
|
||||||
|
import 'package:http/src/streamed_response.dart' as http;
|
||||||
import 'package:mock_request/mock_request.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';
|
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();
|
final Uuid _uuid = new Uuid();
|
||||||
|
|
||||||
Future<TestClient> connectTo(Angel app,
|
/// Shorthand for bootstrapping a [TestClient].
|
||||||
{Map initialSession, bool saveSession: false}) async {
|
Future<TestClient> connectTo(Angel app, {Map initialSession}) async =>
|
||||||
TestClient client;
|
new TestClient(app)..session.addAll(initialSession ?? {});
|
||||||
var path = '/${_uuid.v1()}/${_uuid.v1()}/${_uuid.v1()}';
|
|
||||||
|
|
||||||
if (saveSession) {
|
/// An `angel_client` that sends mock requests to a server, rather than actual HTTP transactions.
|
||||||
app
|
class TestClient extends client.BaseAngelClient {
|
||||||
..get(path, (RequestContext req, res) async {
|
final Map<String, Service> _services = {};
|
||||||
client._session = req.session;
|
|
||||||
|
|
||||||
if (initialSession != null) {
|
/// Session info to be sent to the server on every request.
|
||||||
req.session.addAll(initialSession);
|
final HttpSession session = new MockHttpSession(id: 'angel-test-client');
|
||||||
}
|
|
||||||
})
|
/// A list of cookies to be sent to and received from the server.
|
||||||
..post(path, (RequestContext req, res) async {
|
final List<Cookie> cookies = [];
|
||||||
client._session = req.session..addAll(req.body);
|
|
||||||
})
|
/// The server instance to mock.
|
||||||
..patch(path, (RequestContext req, res) async {
|
final Angel server;
|
||||||
req.body['keys'].forEach(req.session.remove);
|
|
||||||
client._session = req.session;
|
@override
|
||||||
});
|
String authToken;
|
||||||
|
|
||||||
|
TestClient(this.server) : super(null, '/');
|
||||||
|
|
||||||
|
Future close() async {
|
||||||
|
if (server.httpServer != null) await server.httpServer.close(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final server = await app.startServer();
|
/// Opens a WebSockets connection to the server. This will automatically bind the server
|
||||||
final url = 'http://${server.address.address}:${server.port}';
|
/// over HTTP, if it is not already listening. Unfortunately, WebSockets cannot be mocked (yet!).
|
||||||
client = new TestClient(server, url);
|
Future<client.WebSockets> websocket({String path, Duration timeout}) async {
|
||||||
|
HttpServer http = server.httpServer;
|
||||||
if (saveSession) {
|
if (http == null) http = await server.startServer();
|
||||||
await client.client.get('$url$path');
|
var url = 'ws://${http.address.address}:${http.port}';
|
||||||
client._sessionPath = path;
|
var cleanPath = (path ?? '/ws')?.replaceAll(_straySlashes, '');
|
||||||
|
if (cleanPath?.isNotEmpty == true) url += '/$cleanPath';
|
||||||
|
var ws = new _MockWebSockets(this, url);
|
||||||
|
await ws.connect(timeout: timeout);
|
||||||
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
Future<http.Response> sendUnstreamed(
|
||||||
}
|
String method, url, Map<String, String> headers,
|
||||||
|
[body, Encoding encoding]) =>
|
||||||
|
send(method, url, headers, body, encoding).then(http.Response.fromStream);
|
||||||
|
|
||||||
Future<MockHttpResponse> mock(Angel app, String method, Uri uri,
|
Future<http.StreamedResponse> send(
|
||||||
{body,
|
String method, url, Map<String, String> headers,
|
||||||
Iterable<Cookie> cookies: const [],
|
[body, Encoding encoding]) async {
|
||||||
Map<String, dynamic> headers: const {}}) async {
|
var rq = new MockHttpRequest(
|
||||||
var rq = new MockHttpRequest(method, uri);
|
method, url is Uri ? url : Uri.parse(url.toString()));
|
||||||
rq.cookies.addAll(cookies ?? []);
|
headers?.forEach(rq.headers.add);
|
||||||
headers.forEach(rq.headers.add);
|
|
||||||
|
|
||||||
if (body is! Map) {
|
if (authToken?.isNotEmpty == true)
|
||||||
rq.write(body);
|
rq.headers.set(HttpHeaders.AUTHORIZATION, 'Bearer $authToken');
|
||||||
} else if (rq.headers.contentType == null ||
|
|
||||||
|
rq..cookies.addAll(cookies)..session.addAll(session);
|
||||||
|
|
||||||
|
if (body is Stream<List<int>>) {
|
||||||
|
await rq.addStream(body);
|
||||||
|
} else if (body is List<int>) {
|
||||||
|
rq.add(body);
|
||||||
|
} else if (body is Map) {
|
||||||
|
if (rq.headers.contentType == null ||
|
||||||
rq.headers.contentType.mimeType == ContentType.JSON.mimeType) {
|
rq.headers.contentType.mimeType == ContentType.JSON.mimeType) {
|
||||||
rq
|
rq
|
||||||
..headers.contentType = ContentType.JSON
|
..headers.contentType = ContentType.JSON
|
||||||
..write(JSON.encode(body));
|
..write(JSON.encode(
|
||||||
} else if (rq.headers.contentType.mimeType ==
|
body.keys.fold({}, (out, k) => out..[k.toString()] = body[k])));
|
||||||
|
} else if (rq.headers.contentType?.mimeType ==
|
||||||
'application/x-www-form-urlencoded') {
|
'application/x-www-form-urlencoded') {
|
||||||
rq
|
rq.write(body.keys.fold<List<String>>([],
|
||||||
..headers.contentType =
|
(out, k) => out..add('$k=' + Uri.encodeComponent(body[k]))).join());
|
||||||
new ContentType('application', 'x-www-form-urlencoded')
|
} else {
|
||||||
..write(body.keys.fold<List<String>>(
|
|
||||||
[],
|
|
||||||
(out, k) =>
|
|
||||||
out..add('$k=' + Uri.encodeComponent(body[k]))).join('&'));
|
|
||||||
} else
|
|
||||||
throw new UnsupportedError(
|
throw new UnsupportedError(
|
||||||
'mock() only supports sending JSON or URL-encoded bodies.');
|
'Map bodies can only be sent for requests with the content type application/json or application/x-www-form-urlencoded.');
|
||||||
|
}
|
||||||
|
} else if (body != null) {
|
||||||
|
rq.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
await rq.close();
|
await rq.close();
|
||||||
await app.handleRequest(rq);
|
await server.handleRequest(rq);
|
||||||
return rq.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Interacts with an Angel server.
|
var rs = rq.response;
|
||||||
class TestClient extends client.Rest {
|
session.addAll(rq.session);
|
||||||
final HttpServer server;
|
|
||||||
HttpSession _session;
|
|
||||||
String _sessionPath;
|
|
||||||
|
|
||||||
/// Returns a pointer to the current session.
|
Map<String, String> extractedHeaders = {};
|
||||||
HttpSession get session => _session;
|
|
||||||
|
|
||||||
TestClient(this.server, String path) : super(path);
|
rs.headers.forEach((k, v) {
|
||||||
|
extractedHeaders[k] = v.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
/// Adds data to the [session].
|
return new http.StreamedResponse(rs, rs.statusCode,
|
||||||
Future addToSession(Map data) => post(_sessionPath, body: data);
|
contentLength: rs.contentLength,
|
||||||
|
isRedirect: rs.headers[HttpHeaders.LOCATION] != null,
|
||||||
|
headers: extractedHeaders,
|
||||||
|
persistentConnection:
|
||||||
|
rq.headers.value(HttpHeaders.CONNECTION)?.toLowerCase()?.trim() ==
|
||||||
|
'keep-alive',
|
||||||
|
reasonPhrase: rs.reasonPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes data from the [session].
|
Future<http.Response> delete(url, {Map<String, String> headers}) =>
|
||||||
Future removeFromSession(List<String> keys) => patch(_sessionPath,
|
sendUnstreamed('DELETE', url, headers);
|
||||||
body: JSON.encode({'keys': keys}),
|
|
||||||
headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType});
|
Future<http.Response> get(url, {Map<String, String> headers}) =>
|
||||||
|
sendUnstreamed('GET', url, headers);
|
||||||
|
|
||||||
|
Future<http.Response> head(url, {Map<String, String> headers}) =>
|
||||||
|
sendUnstreamed('HEAD', url, headers);
|
||||||
|
|
||||||
|
Future<http.Response> patch(url, {body, Map<String, String> headers}) =>
|
||||||
|
sendUnstreamed('PATCH', url, headers, body);
|
||||||
|
|
||||||
|
Future<http.Response> post(url, {body, Map<String, String> headers}) =>
|
||||||
|
sendUnstreamed('POST', url, headers, body);
|
||||||
|
|
||||||
|
Future<http.Response> put(url, {body, Map<String, String> headers}) =>
|
||||||
|
sendUnstreamed('PUT', url, headers, body);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future close() async {
|
String basePath;
|
||||||
if (server != null) {
|
|
||||||
await server.close(force: true);
|
@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) => configurer(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
client.Service service<T>(String path,
|
||||||
|
{Type type, client.AngelDeserializer deserializer}) {
|
||||||
|
String uri = path.toString().replaceAll(_straySlashes, "");
|
||||||
|
return _services.putIfAbsent(uri,
|
||||||
|
new MockService(this, '$basePath/$uri', deserializer: deserializer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockService extends client.BaseAngelService {
|
||||||
|
final TestClient _app;
|
||||||
|
|
||||||
|
MockService(this._app, String basePath,
|
||||||
|
{client.AngelDeserializer deserializer})
|
||||||
|
: super(null, _app, basePath, deserializer: deserializer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||||
|
if (app.authToken != null && app.authToken.isNotEmpty) {
|
||||||
|
request.headers['Authorization'] = 'Bearer ${app.authToken}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return _app.send(request.method, request.url, request.headers,
|
||||||
|
request.finalize(), 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[HttpHeaders.AUTHORIZATION] = 'Bearer ${app.authToken}';
|
||||||
|
|
||||||
|
var socket = await WebSocket.connect(basePath, headers: headers);
|
||||||
|
return new IOWebSocketChannel(socket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,45 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_framework/src/http/angel_http_exception.dart';
|
||||||
|
import 'package:angel_validate/angel_validate.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:matcher/matcher.dart';
|
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,
|
/// Expects a given response, when parsed as JSON,
|
||||||
/// to equal a desired value.
|
/// to equal a desired value.
|
||||||
Matcher isJson(value) => new _IsJson(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.
|
/// Expects a response to have the given status code.
|
||||||
Matcher hasStatus(int status) => new _HasStatus(status);
|
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 {
|
class _IsJson extends Matcher {
|
||||||
var value;
|
var value;
|
||||||
|
|
||||||
|
@ -16,7 +47,7 @@ class _IsJson extends Matcher {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Description describe(Description description) {
|
Description describe(Description description) {
|
||||||
return description.add('should equal the desired JSON response: $value');
|
return description.add('equals the desired JSON response: $value');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -24,6 +55,79 @@ class _IsJson extends Matcher {
|
||||||
equals(value).matches(JSON.decode(item.body), matchState);
|
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(http.Response item, Map matchState) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HasContentType extends Matcher {
|
||||||
|
var contentType;
|
||||||
|
|
||||||
|
_HasContentType(this.contentType);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Description describe(Description description) {
|
||||||
|
var str =
|
||||||
|
contentType is ContentType ? contentType.value : contentType.toString();
|
||||||
|
return description.add('has content type ' + str);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool matches(http.Response item, Map matchState) {
|
||||||
|
if (!item.headers.containsKey(HttpHeaders.CONTENT_TYPE)) return false;
|
||||||
|
|
||||||
|
if (contentType is ContentType) {
|
||||||
|
var compare = ContentType.parse(item.headers[HttpHeaders.CONTENT_TYPE]);
|
||||||
|
return equals(contentType.mimeType).matches(compare.mimeType, matchState);
|
||||||
|
} else {
|
||||||
|
return equals(contentType.toString())
|
||||||
|
.matches(item.headers[HttpHeaders.CONTENT_TYPE], matchState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(http.Response item, Map matchState) {
|
||||||
|
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 : [value];
|
||||||
|
return v
|
||||||
|
.map((x) => x.toString())
|
||||||
|
.every(item.headers[key.toLowerCase()].split(',').contains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _HasStatus extends Matcher {
|
class _HasStatus extends Matcher {
|
||||||
int status;
|
int status;
|
||||||
|
|
||||||
|
@ -31,10 +135,93 @@ class _HasStatus extends Matcher {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Description describe(Description description) {
|
Description describe(Description description) {
|
||||||
return description.add('should have status code $status');
|
return description.add('has status code $status');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(http.Response item, Map matchState) =>
|
bool matches(http.Response item, Map matchState) =>
|
||||||
equals(status).matches(item.statusCode, matchState);
|
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(http.Response item, Map matchState) {
|
||||||
|
final json = JSON.decode(item.body);
|
||||||
|
if (json is! Map) return false;
|
||||||
|
return validator.matches(json, matchState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(http.Response item, Map matchState) {
|
||||||
|
final json = JSON.decode(item.body);
|
||||||
|
|
||||||
|
if (json is Map && json['isError'] == true) {
|
||||||
|
var exc = new AngelHttpException.fromMap(json);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@ author: "Tobe O <thosakwe@gmail.com>"
|
||||||
description: "Testing utility library for the Angel framework."
|
description: "Testing utility library for the Angel framework."
|
||||||
homepage: "https://github.com/angel-dart/test.git"
|
homepage: "https://github.com/angel-dart/test.git"
|
||||||
name: "angel_test"
|
name: "angel_test"
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_client: "^1.0.0-dev+16"
|
angel_client: "^1.0.0"
|
||||||
angel_framework: "^1.0.0-dev"
|
angel_framework: "^1.0.0-dev"
|
||||||
|
angel_validate: ^1.0.0
|
||||||
|
angel_websocket: ^1.0.0
|
||||||
http: "^0.11.3+9"
|
http: "^0.11.3+9"
|
||||||
matcher: "^0.12.0+2"
|
matcher: "^0.12.0+2"
|
||||||
mock_request: ^1.0.0
|
mock_request: ^1.0.0
|
||||||
|
|
|
@ -1,67 +1,105 @@
|
||||||
import 'dart:convert';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/angel_framework.dart' as server;
|
|
||||||
import 'package:angel_test/angel_test.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';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
server.Angel app;
|
Angel app;
|
||||||
TestClient testClient;
|
TestClient client;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
app = new server.Angel()
|
app = new Angel()
|
||||||
..get('/hello', 'Hello')
|
..get('/hello', 'Hello')
|
||||||
|
..get(
|
||||||
|
'/error',
|
||||||
|
() => throw new AngelHttpException.forbidden(message: 'Test')
|
||||||
|
..errors.addAll(['foo', 'bar']))
|
||||||
|
..get('/body', (ResponseContext res) {
|
||||||
|
res
|
||||||
|
..write('OK')
|
||||||
|
..end();
|
||||||
|
})
|
||||||
|
..get(
|
||||||
|
'/valid',
|
||||||
|
() => {
|
||||||
|
'michael': 'jackson',
|
||||||
|
'billie': {'jean': 'hee-hee', 'is_my_lover': false}
|
||||||
|
})
|
||||||
..post('/hello', (req, res) async {
|
..post('/hello', (req, res) async {
|
||||||
return {'bar': req.body['foo']};
|
return {'bar': req.body['foo']};
|
||||||
});
|
})
|
||||||
|
..use(
|
||||||
|
'/foo',
|
||||||
|
new AnonymousService(
|
||||||
|
create: (data, [params]) async => {'foo': 'bar'}));
|
||||||
|
|
||||||
testClient = await connectTo(app);
|
var ws = new AngelWebSocket();
|
||||||
|
await app.configure(ws);
|
||||||
|
|
||||||
|
client = await connectTo(app);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await testClient.close();
|
await client.close();
|
||||||
app = null;
|
app = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mock()', () async {
|
group('matchers', () {
|
||||||
var response = await mock(app, 'GET', Uri.parse('/hello'));
|
|
||||||
expect(await response.transform(UTF8.decoder).join(), equals('"Hello"'));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('isJson+hasStatus', () {
|
group('isJson+hasStatus', () {
|
||||||
test('get', () async {
|
test('get', () async {
|
||||||
final response = await testClient.get('/hello');
|
final response = await client.get('/hello');
|
||||||
expect(response, isJson('Hello'));
|
expect(response, isJson('Hello'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('post', () async {
|
test('post', () async {
|
||||||
final response = await testClient.post('/hello', body: {'foo': 'baz'});
|
final response = await client.post('/hello', body: {'foo': 'baz'});
|
||||||
expect(response, allOf(hasStatus(200), isJson({'bar': 'baz'})));
|
expect(response, allOf(hasStatus(200), isJson({'bar': 'baz'})));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('session', () {
|
test('isAngelHttpException', () async {
|
||||||
test('initial session', () async {
|
var res = await client.get('/error');
|
||||||
final TestClient client = await connectTo(app,
|
expect(res, isAngelHttpException());
|
||||||
initialSession: {'foo': 'bar'}, saveSession: true);
|
expect(
|
||||||
expect(client.session['foo'], equals('bar'));
|
res,
|
||||||
|
isAngelHttpException(
|
||||||
|
statusCode: 403, message: 'Test', errors: ['foo', 'bar']));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('add to session', () async {
|
test('hasBody', () async {
|
||||||
final TestClient client = await connectTo(app, saveSession: true);
|
var res = await client.get('/body');
|
||||||
await client.addToSession({'michael': 'jackson'});
|
expect(res, hasBody());
|
||||||
expect(client.session['michael'], equals('jackson'));
|
expect(res, hasBody('OK'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('remove from session', () async {
|
test('hasHeader', () async {
|
||||||
final TestClient client = await connectTo(app,
|
var res = await client.get('/hello');
|
||||||
initialSession: {'angel': 'framework'}, saveSession: true);
|
expect(res, hasHeader('server'));
|
||||||
await client.removeFromSession(['angel']);
|
expect(res, hasHeader('server', 'angel'));
|
||||||
expect(client.session.containsKey('angel'), isFalse);
|
expect(res, hasHeader('server', ['angel']));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('disable session', () async {
|
test('hasValidBody', () async {
|
||||||
final client = await connectTo(app, saveSession: false);
|
var res = await client.get('/valid');
|
||||||
expect(client.session, isNull);
|
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('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'}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue