diff --git a/.idea/angel_test.iml b/.idea/angel_test.iml index eae13016..954fa6c5 100644 --- a/.idea/angel_test.iml +++ b/.idea/angel_test.iml @@ -2,6 +2,7 @@ + diff --git a/.travis.yml b/.travis.yml index de2210c9..a9e2c109 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,4 @@ -language: dart \ No newline at end of file +language: dart +dart: + - dev + - stable \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b63b05bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.1.0+1 +* Dart 2/strong mode fixes. +* Pass a `useZone` flag to `AngelHttp` through `TestServer`. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..ed8bb393 --- /dev/null +++ b/example/main.dart @@ -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', '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 { + return {'bar': req.body['foo']}; + }) + ..get('/gzip', (req, res) async { + res + ..headers['content-encoding'] = 'gzip' + ..write(gzip.encode('Poop'.codeUnits)) + ..end(); + }) + ..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, 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'); + 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'})); + }); + }); +} diff --git a/lib/angel_test.dart b/lib/angel_test.dart index cf5be8b9..0fb65490 100644 --- a/lib/angel_test.dart +++ b/lib/angel_test.dart @@ -1,2 +1,2 @@ export 'src/client.dart'; -export 'src/matchers.dart'; \ No newline at end of file +export 'src/matchers.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 0b5b81a0..563fa89b 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1,33 +1,37 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +import 'dart:convert' show Encoding; +import 'dart:io' show ContentType, Cookie, HttpSession, HttpServer, WebSocket; +import 'package:dart2_constant/convert.dart'; +import 'package:dart2_constant/io.dart' hide WebSocket; 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_websocket/io.dart' as client; -import 'package:http/http.dart' as http; +import 'package:http/http.dart' as http hide StreamedResponse; +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'; +//import 'package:uuid/uuid.dart'; final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); -const Map _readHeaders = const {'Accept': 'application/json'}; +/*const Map _readHeaders = const {'Accept': 'application/json'}; const Map _writeHeaders = const { 'Accept': 'application/json', 'Content-Type': 'application/json' }; -final Uuid _uuid = new Uuid(); +final Uuid _uuid = new Uuid();*/ /// Shorthand for bootstrapping a [TestClient]. Future connectTo(Angel app, - {Map initialSession, bool autoDecodeGzip: true}) async { - if (!app.isProduction) - app.configuration.putIfAbsent('testMode', () => true); + {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) + for (var plugin in app.startupHooks) await plugin(app); + return new TestClient(app, + autoDecodeGzip: autoDecodeGzip != false, useZone: useZone) ..session.addAll(initialSession ?? {}); } @@ -50,7 +54,12 @@ class TestClient extends client.BaseAngelClient { @override String authToken; - TestClient(this.server, {this.autoDecodeGzip: true}) : super(new http.Client(), '/'); + 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(); @@ -60,8 +69,8 @@ class TestClient extends client.BaseAngelClient { /// 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 websocket({String path, Duration timeout}) async { - HttpServer http = server.httpServer; - if (http == null) http = await server.startServer(); + HttpServer http = _http.httpServer; + if (http == null) http = await _http.startServer(); var url = 'ws://${http.address.address}:${http.port}'; var cleanPath = (path ?? '/ws')?.replaceAll(_straySlashes, ''); if (cleanPath?.isNotEmpty == true) url += '/$cleanPath'; @@ -70,24 +79,21 @@ class TestClient extends client.BaseAngelClient { return ws; } - Future sendUnstreamed(String method, url, - Map headers, - [body, Encoding encoding]) => + Future sendUnstreamed( + String method, url, Map headers, + [body, Encoding encoding]) => send(method, url, headers, body, encoding).then(http.Response.fromStream); - Future send(String method, url, - Map headers, + Future send(String method, url, Map headers, [body, Encoding encoding]) async { var rq = new MockHttpRequest( method, url is Uri ? url : Uri.parse(url.toString())); headers?.forEach(rq.headers.add); if (authToken?.isNotEmpty == true) - rq.headers.set(HttpHeaders.AUTHORIZATION, 'Bearer $authToken'); + rq.headers.set('authorization', 'Bearer $authToken'); - rq - ..cookies.addAll(cookies) - ..session.addAll(session); + rq..cookies.addAll(cookies)..session.addAll(session); if (body is Stream>) { await rq.addStream(body); @@ -95,16 +101,17 @@ class TestClient extends client.BaseAngelClient { rq.add(body); } else if (body is Map) { if (rq.headers.contentType == null || - rq.headers.contentType.mimeType == ContentType.JSON.mimeType) { + rq.headers.contentType.mimeType == 'application/json') { rq - ..headers.contentType = ContentType.JSON - ..write(JSON.encode( - body.keys.fold({}, (out, k) => out..[k.toString()] = body[k]))); + ..headers.contentType = new ContentType('application', 'json') + ..write(json.encode(body.keys.fold>( + {}, (out, k) => out..[k.toString()] = body[k]))); } else if (rq.headers.contentType?.mimeType == 'application/x-www-form-urlencoded') { - rq.write(body.keys.fold>([], - (out, k) => out..add('$k=' + Uri.encodeComponent(body[k]))) - .join()); + rq.write(body.keys.fold>( + [], + (out, k) => out + ..add('$k=' + Uri.encodeComponent(body[k].toString()))).join()); } else { throw new UnsupportedError( 'Map bodies can only be sent for requests with the content type application/json or application/x-www-form-urlencoded.'); @@ -114,7 +121,8 @@ class TestClient extends client.BaseAngelClient { } await rq.close(); - await server.handleRequest(rq); + + await _http.handleRequest(rq); var rs = rq.response; session @@ -130,17 +138,17 @@ class TestClient extends client.BaseAngelClient { Stream> stream = rs; if (autoDecodeGzip != false && - rs.headers[HttpHeaders.CONTENT_ENCODING]?.contains('gzip') == true) - stream = stream.transform(GZIP.decoder); + rs.headers['content-encoding']?.contains('gzip') == true) + stream = stream.transform(gzip.decoder); - return new http.StreamedResponse(stream, rs.statusCode, + return new StreamedResponse(stream, rs.statusCode, contentLength: rs.contentLength, - isRedirect: rs.headers[HttpHeaders.LOCATION] != null, + isRedirect: rs.headers['location'] != null, headers: extractedHeaders, persistentConnection: - rq.headers.value(HttpHeaders.CONNECTION)?.toLowerCase()?.trim() == - 'keep-alive' || - rq.headers.persistentConnection == true, + rq.headers.value('connection')?.toLowerCase()?.trim() == + 'keep-alive' || + rq.headers.persistentConnection == true, reasonPhrase: rs.reasonPhrase); } @@ -175,7 +183,7 @@ class TestClient extends client.BaseAngelClient { Future configure(client.AngelConfigurer configurer) => configurer(this); @override - client.Service service(String path, + client.Service service(String path, {Type type, client.AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ""); return _services.putIfAbsent( @@ -191,7 +199,7 @@ class _MockService extends client.BaseAngelService { : super(null, _app, basePath, deserializer: deserializer); @override - Future send(http.BaseRequest request) { + Future send(http.BaseRequest request) { if (app.authToken != null && app.authToken.isNotEmpty) { request.headers['Authorization'] = 'Bearer ${app.authToken}'; } @@ -211,7 +219,7 @@ class _MockWebSockets extends client.WebSockets { Map headers = {}; if (app.authToken?.isNotEmpty == true) - headers[HttpHeaders.AUTHORIZATION] = 'Bearer ${app.authToken}'; + headers['authorization'] = 'Bearer ${app.authToken}'; var socket = await WebSocket.connect(basePath, headers: headers); return new IOWebSocketChannel(socket); diff --git a/lib/src/matchers.dart b/lib/src/matchers.dart index 74f8178c..16c09630 100644 --- a/lib/src/matchers.dart +++ b/lib/src/matchers.dart @@ -1,7 +1,7 @@ -import 'dart:convert'; import 'dart:io'; import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_validate/angel_validate.dart'; +import 'package:dart2_constant/convert.dart'; import 'package:http/http.dart' as http; import 'package:matcher/matcher.dart'; @@ -51,8 +51,9 @@ class _IsJson extends Matcher { } @override - bool matches(http.Response item, Map matchState) => - equals(value).matches(JSON.decode(item.body), matchState); + bool matches(item, Map matchState) => + item is http.Response && + equals(value).matches(json.decode(item.body), matchState); } class _HasBody extends Matcher { @@ -65,12 +66,16 @@ class _HasBody extends Matcher { 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) - return equals(body).matches(item.bodyBytes, matchState); - else - return equals(body.toString()).matches(item.body, matchState); + bool matches(item, Map matchState) { + if (item is http.Response) { + if (body == true) return isNotEmpty.matches(item.bodyBytes, matchState); + if (body is List) + return equals(body).matches(item.bodyBytes, matchState); + else + return equals(body.toString()).matches(item.body, matchState); + } else { + return false; + } } } @@ -81,21 +86,27 @@ class _HasContentType extends Matcher { @override Description describe(Description description) { - var str = - contentType is ContentType ? contentType.value : contentType.toString(); + var str = contentType is ContentType + ? ((contentType as 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; + 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[HttpHeaders.CONTENT_TYPE]); - return equals(contentType.mimeType).matches(compare.mimeType, matchState); + 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 equals(contentType.toString()) - .matches(item.headers[HttpHeaders.CONTENT_TYPE], matchState); + return false; } } } @@ -115,15 +126,20 @@ class _HasHeader extends Matcher { } @override - bool matches(http.Response item, Map matchState) { - if (value == true) { - return contains(key.toLowerCase()).matches(item.headers.keys, matchState); + 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 : [value]; + return v + .map((x) => x.toString()) + .every(item.headers[key.toLowerCase()].split(',').contains); + } } 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); + return false; } } } @@ -139,7 +155,8 @@ class _HasStatus extends Matcher { } @override - bool matches(http.Response item, Map matchState) => + bool matches(item, Map matchState) => + item is http.Response && equals(status).matches(item.statusCode, matchState); } @@ -153,10 +170,14 @@ class _HasValidBody extends Matcher { 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); + 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; + } } } @@ -198,30 +219,35 @@ class _IsAngelHttpException extends Matcher { } @override - bool matches(http.Response item, Map matchState) { - final json = JSON.decode(item.body); + bool matches(item, Map matchState) { + if (item is http.Response) { + final jsons = json.decode(item.body); - if (json is Map && json['isError'] == true) { - var exc = new AngelHttpException.fromMap(json); + 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 && 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 (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; + if (errors.isNotEmpty) { + if (!errors + .every((err) => contains(err).matches(exc.errors, matchState))) + return false; + } + + return true; } - - return true; - } - } else + } else + return false; + } else { return false; + } } } diff --git a/pubspec.yaml b/pubspec.yaml index 9fd12503..b0e94474 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,18 +1,17 @@ -author: "Tobe O " -description: "Testing utility library for the Angel framework." -homepage: "https://github.com/angel-dart/test.git" -name: "angel_test" -version: 1.1.0 +author: Tobe O +description: Testing utility library for the Angel framework. +homepage: https://github.com/angel-dart/test.git +name: angel_test +version: 1.1.0+1 dependencies: angel_client: ^1.1.0-alpha angel_framework: ^1.1.0-alpha angel_validate: ^1.0.0 angel_websocket: ^1.1.0-alpha - http: "^0.11.3+9" - matcher: "^0.12.0+2" + http: ^0.11.0 + matcher: ^0.12.0+2 mock_request: ^1.0.0 - uuid: "^0.5.3" dev_dependencies: - test: "^0.12.17+2" + test: ^0.12.17+2 environment: - sdk: ">=1.19.0" + sdk: ">=1.8.9 <3.0.0" diff --git a/test/simple_test.dart b/test/simple_test.dart index 5bf6e161..fd96f841 100644 --- a/test/simple_test.dart +++ b/test/simple_test.dart @@ -32,8 +32,8 @@ main() { }) ..get('/gzip', (req, res) async { res - ..headers[HttpHeaders.CONTENT_ENCODING] = 'gzip' - ..write(GZIP.encode('Poop'.codeUnits)) + ..headers['content-encoding'] = 'gzip' + ..write(gzip.encode('Poop'.codeUnits)) ..end(); }) ..use( @@ -50,7 +50,7 @@ main() { app.errorHandler = (e, req, res) => e.toJson(); - client = await connectTo(app); + client = await connectTo(app, useZone: false); }); tearDown(() async { @@ -96,8 +96,9 @@ main() { test('hasValidBody+hasContentType', () async { var res = await client.get('/valid'); + print('Body: ${res.body}'); expect(res, hasContentType('application/json')); - expect(res, hasContentType(ContentType.JSON)); + expect(res, hasContentType(new ContentType('application', 'json'))); expect( res, hasValidBody(new Validator({ @@ -111,7 +112,7 @@ main() { test('gzip decode', () async { var res = await client.get('/gzip'); - expect(res, hasHeader(HttpHeaders.CONTENT_ENCODING, 'gzip')); + expect(res, hasHeader('content-encoding', 'gzip')); expect(res, hasBody('Poop')); }); @@ -136,7 +137,7 @@ main() { var foo = ws.service('foo'); foo.create({}); var result = await foo.onCreated.first; - expect(result.data, equals({'foo': 'bar'})); + expect(result is Map ? result : result.data, equals({'foo': 'bar'})); }); }); }