diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 00000000..11ef66c4 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 48544501..e46408c2 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,9 +2,18 @@ + + + + + - - + + + + + + @@ -17,7 +26,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -236,7 +187,7 @@ - + @@ -249,7 +200,92 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -357,19 +393,44 @@ false + + + + + + + + - - - - - - - - + + + + + + + + + + + + @@ -402,7 +463,8 @@ - + + 1481237183504 @@ -516,43 +578,50 @@ - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - @@ -565,26 +634,29 @@ + - + + + - + + + + + - - + - - - @@ -610,70 +682,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -774,13 +790,6 @@ - - - - - - - @@ -795,13 +804,6 @@ - - - - - - - @@ -809,20 +811,6 @@ - - - - - - - - - - - - - - @@ -830,13 +818,6 @@ - - - - - - - @@ -880,14 +861,6 @@ - - - - - - - - @@ -898,9 +871,9 @@ - + - + @@ -922,19 +895,9 @@ - + - - - - - - - - - - - + @@ -948,7 +911,7 @@ - + @@ -957,17 +920,9 @@ - - - - - - - - - + @@ -976,8 +931,8 @@ - - + + @@ -986,10 +941,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart index 60b4124c..2c869965 100644 --- a/lib/src/http/angel_base.dart +++ b/lib/src/http/angel_base.dart @@ -28,6 +28,5 @@ class AngelBase extends Routable { /// A function that renders views. /// /// Called by [ResponseContext]@`render`. - ViewGenerator viewGenerator = (String view, - [Map data]) async => "No view engine has been configured yet."; + ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet."; } \ No newline at end of file diff --git a/lib/src/http/angel_http_exception.dart b/lib/src/http/angel_http_exception.dart index 54cb14a0..781b5643 100644 --- a/lib/src/http/angel_http_exception.dart +++ b/lib/src/http/angel_http_exception.dart @@ -32,7 +32,7 @@ class AngelHttpException implements Exception { Map toJson() { return { 'isError': true, - 'statusCode': statusCode, + 'status_code': statusCode, 'message': message, 'errors': errors }; @@ -47,7 +47,7 @@ class AngelHttpException implements Exception { factory AngelHttpException.fromMap(Map data) { return new AngelHttpException(null, - statusCode: data['statusCode'], + statusCode: data['status_code'] ?? data['statusCode'], message: data['message'], errors: data['errors']); } diff --git a/lib/src/http/anonymous_service.dart b/lib/src/http/anonymous_service.dart index dbe927eb..9e578be1 100644 --- a/lib/src/http/anonymous_service.dart +++ b/lib/src/http/anonymous_service.dart @@ -8,7 +8,7 @@ class AnonymousService extends Service { Function _index, _read, _create, _modify, _update, _remove; AnonymousService( - {Future index([Map params]), + {Future index([Map params]), Future read(id, [Map params]), Future create(data, [Map params]), Future modify(id, data, [Map params]), diff --git a/lib/src/http/typed_service.dart b/lib/src/http/typed_service.dart index eb883119..9ee37834 100644 --- a/lib/src/http/typed_service.dart +++ b/lib/src/http/typed_service.dart @@ -15,7 +15,7 @@ class TypedService extends Service { deserialize(x) { // print('DESERIALIZE: $x (${x.runtimeType})'); - if (x == dynamic || x == Object || x is T) + if (x is Type || x is T) return x; else if (x is Iterable) return x.map(deserialize).toList(); @@ -23,7 +23,11 @@ class TypedService extends Service { Map data = x.keys.fold({}, (map, key) { var value = x[key]; - if ((key == 'createdAt' || key == 'updatedAt') && value is String) { + if ((key == 'createdAt' || + key == 'updatedAt' || + key == 'created_at' || + key == 'updated_at') && + value is String) { return map..[key] = DateTime.parse(value); } else { return map..[key] = value; @@ -32,16 +36,16 @@ class TypedService extends Service { Model result = god.deserializeDatum(data, outputType: T); - if (x['createdAt'] is String) { - result.createdAt = DateTime.parse(x['createdAt']); - } else if (x['createdAt'] is DateTime) { - result.createdAt = x['createdAt']; + if (data['createdAt'] is DateTime) { + result.createdAt = data['createdAt']; + } else if (data['created_at'] is DateTime) { + result.createdAt = data['created_at']; } - if (x['updatedAt'] is String) { - result.updatedAt = DateTime.parse(x['updatedAt']); - } else if (x['updatedAt'] is DateTime) { - result.updatedAt = x['updatedAt']; + if (data['updatedAt'] is DateTime) { + result.updatedAt = data['updatedAt']; + } else if (data['updated_at'] is DateTime) { + result.updatedAt = data['updated_at']; } // print('x: $x\nresult: $result'); diff --git a/test/all.dart b/test/all.dart index c68df6c9..a4910443 100644 --- a/test/all.dart +++ b/test/all.dart @@ -1,23 +1,31 @@ +import 'anonymous_service_test.dart' as anonymous_service; import 'controller_test.dart' as controller; import 'di_test.dart' as di; +import 'exception_test.dart' as exception; import 'general_test.dart' as general; import 'hooked_test.dart' as hooked; import 'precontained_test.dart' as precontained; import 'routing_test.dart' as routing; import 'serialize_test.dart' as serialize; import 'services_test.dart' as services; +import 'typed_service_test.dart' as typed_service; import 'util_test.dart' as util; +import 'view_generator_test.dart' as view_generator; import 'package:test/test.dart'; /// For running with coverage main() { + group('anonymous service', anonymous_service.main); group('controller', controller.main); group('di', di.main); + group('exception', exception.main); group('general', general.main); group('hooked', hooked.main); group('precontained', precontained.main); group('routing', routing.main); group('serialize', serialize.main); group('services', services.main); + group('typed_service', typed_service.main); group('util', util.main); -} \ No newline at end of file + group('view generator', view_generator.main); +} diff --git a/test/anonymous_service_test.dart b/test/anonymous_service_test.dart new file mode 100644 index 00000000..7df3601b --- /dev/null +++ b/test/anonymous_service_test.dart @@ -0,0 +1,44 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:test/test.dart'; + +main() { + test('custom methods', () async { + var svc = new AnonymousService( + index: ([p]) async => 'index', + read: (id, [p]) async => 'read', + create: (data, [p]) async => 'create', + modify: (id, data, [p]) async => 'modify', + update: (id, data, [p]) async => 'update', + remove: (id, [p]) async => 'remove'); + expect(await svc.index(), 'index'); + expect(await svc.read(null), 'read'); + expect(await svc.create(null), 'create'); + expect(await svc.modify(null, null), 'modify'); + expect(await svc.update(null, null), 'update'); + expect(await svc.remove(null), 'remove'); + }); + + test('defaults to throwing', () async { + try { + var svc = new AnonymousService(); + await svc.read(1); + throw 'Should have thrown 405!'; + } on AngelHttpException { + // print('Ok!'); + } + try { + var svc = new AnonymousService(); + await svc.modify(2, null); + throw 'Should have thrown 405!'; + } on AngelHttpException { + // print('Ok!'); + } + try { + var svc = new AnonymousService(); + await svc.update(3, null); + throw 'Should have thrown 405!'; + } on AngelHttpException { + // print('Ok!'); + } + }); +} diff --git a/test/controller_test.dart b/test/controller_test.dart index c023ff38..e56000ae 100644 --- a/test/controller_test.dart +++ b/test/controller_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:http/http.dart' as http; +import 'package:mock_request/mock_request.dart'; import 'package:test/test.dart'; import 'common.dart'; @@ -25,8 +26,19 @@ class TodoController extends Controller { } } +class NoExposeController extends Controller { + +} + +@Expose('/named', as: 'foo') +class NamedController extends Controller { + @Expose('/optional/:arg?', allowNull: const ['arg']) + optional() => 2; +} + main() { Angel app; + TodoController ctrl; HttpServer server; http.Client client = new http.Client(); String url; @@ -39,7 +51,7 @@ main() { "/redirect", (req, ResponseContext res) async => res.redirectToAction("TodoController@foo", {"foo": "world"})); - await app.configure(new TodoController()); + await app.configure(ctrl = new TodoController()); print(app.controllers); app.dumpTree(); @@ -54,6 +66,39 @@ main() { url = null; }); + test('basic', () { + expect(ctrl.app, app); + }); + + test('require expose', () async { + try { + var app = new Angel(); + await app.configure(new NoExposeController()); + throw 'Should require @Expose'; + } on Exception { + // :) + } + }); + + test('create dynamic handler', () async { + var app = new Angel(); + app.get('/foo', createDynamicHandler(({String bar}) { + return 2; + }, optional: [ + 'bar' + ])); + var rq = new MockHttpRequest('GET', new Uri(path: 'foo')); + await app.handleRequest(rq); + var body = await rq.response.transform(UTF8.decoder).join(); + expect(JSON.decode(body), 2); + }); + + test('optional name', () async { + var app = new Angel(); + await app.configure(new NamedController()); + expect(app.controllers['foo'], new isInstanceOf()); + }); + test("middleware", () async { var rgx = new RegExp("^Hello, world!"); var response = await client.get("$url/todos/0"); diff --git a/test/exception_test.dart b/test/exception_test.dart new file mode 100644 index 00000000..f1db47ff --- /dev/null +++ b/test/exception_test.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart'; + +main() { + test('named constructors', () { + expect(new AngelHttpException.badRequest(), + isException(HttpStatus.BAD_REQUEST, '400 Bad Request')); + expect(new AngelHttpException.BadRequest(), + isException(HttpStatus.BAD_REQUEST, '400 Bad Request')); + expect(new AngelHttpException.notAuthenticated(), + isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated')); + expect(new AngelHttpException.NotAuthenticated(), + isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated')); + expect(new AngelHttpException.paymentRequired(), + isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required')); + expect(new AngelHttpException.PaymentRequired(), + isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required')); + expect(new AngelHttpException.forbidden(), + isException(HttpStatus.FORBIDDEN, '403 Forbidden')); + expect(new AngelHttpException.Forbidden(), + isException(HttpStatus.FORBIDDEN, '403 Forbidden')); + expect(new AngelHttpException.notFound(), + isException(HttpStatus.NOT_FOUND, '404 Not Found')); + expect(new AngelHttpException.NotFound(), + isException(HttpStatus.NOT_FOUND, '404 Not Found')); + expect(new AngelHttpException.methodNotAllowed(), + isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed')); + expect(new AngelHttpException.MethodNotAllowed(), + isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed')); + expect(new AngelHttpException.notAcceptable(), + isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable')); + expect(new AngelHttpException.NotAcceptable(), + isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable')); + expect(new AngelHttpException.methodTimeout(), + isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout')); + expect(new AngelHttpException.MethodTimeout(), + isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout')); + expect(new AngelHttpException.conflict(), + isException(HttpStatus.CONFLICT, '409 Conflict')); + expect(new AngelHttpException.Conflict(), + isException(HttpStatus.CONFLICT, '409 Conflict')); + expect(new AngelHttpException.notProcessable(), + isException(422, '422 Not Processable')); + expect(new AngelHttpException.NotProcessable(), + isException(422, '422 Not Processable')); + expect(new AngelHttpException.notImplemented(), + isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented')); + expect(new AngelHttpException.NotImplemented(), + isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented')); + expect(new AngelHttpException.unavailable(), + isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable')); + expect(new AngelHttpException.Unavailable(), + isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable')); + }); + + test('fromMap', () { + expect(new AngelHttpException.fromMap({'status_code': -1, 'message': 'ok'}), + isException(-1, 'ok')); + }); + + test('toMap = toJson', () { + var exc = new AngelHttpException.badRequest(); + expect(exc.toMap(), exc.toJson()); + var json = JSON.encode(exc.toJson()); + var exc2 = new AngelHttpException.fromJson(json); + expect(exc2.toJson(), exc.toJson()); + }); + + test('toString', () { + expect( + new AngelHttpException(null, statusCode: 420, message: 'Blaze It') + .toString(), + '420: Blaze It'); + }); +} + +Matcher isException(int statusCode, String message) => + new _IsException(statusCode, message); + +class _IsException extends Matcher { + final int statusCode; + final String message; + + _IsException(this.statusCode, this.message); + + @override + Description describe(Description description) => + description.add('has status code $statusCode and message "$message"'); + + @override + bool matches(AngelHttpException item, Map matchState) { + return item.statusCode == statusCode && item.message == message; + } +} diff --git a/test/typed_service_test.dart b/test/typed_service_test.dart new file mode 100644 index 00000000..f084de04 --- /dev/null +++ b/test/typed_service_test.dart @@ -0,0 +1,65 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/common.dart'; +import 'package:test/test.dart'; + +main() { + var svc = new TypedService(new MapService()); + + test('force model', () { + expect(() => new TypedService(null), throwsException); + }); + + test('serialize', () { + expect(svc.serialize({'foo': 'bar'}), {'foo': 'bar'}); + expect( + svc.serialize([ + {'foo': 'bar'} + ]), + [ + {'foo': 'bar'} + ]); + expect(() => svc.serialize(2), throwsArgumentError); + var now = new DateTime.now(); + var t = + new Todo(text: 'a', completed: false, createdAt: now, updatedAt: now); + var m = svc.serialize(t); + print(m); + expect(m, { + 'id': null, + 'createdAt': now.toIso8601String(), + 'updatedAt': now.toIso8601String(), + 'text': 'a', + 'completed': false + }); + }); + + test('deserialize date', () { + var now = new DateTime.now(); + var m = svc.deserialize({ + 'createdAt': now.toIso8601String(), + 'updatedAt': now.toIso8601String() + }); + expect(m, new isInstanceOf()); + var t = m as Todo; + expect(t.createdAt.millisecondsSinceEpoch, now.millisecondsSinceEpoch); + }); + + test('deserialize date w/ underscore', () { + var now = new DateTime.now(); + var m = svc.deserialize({ + 'created_at': now.toIso8601String(), + 'updated_at': now.toIso8601String() + }); + expect(m, new isInstanceOf()); + var t = m as Todo; + expect(t.createdAt.millisecondsSinceEpoch, now.millisecondsSinceEpoch); + }); +} + +class Todo extends Model { + String text; + bool completed; + @override + DateTime createdAt, updatedAt; + Todo({this.text, this.completed, this.createdAt, this.updatedAt}); +} diff --git a/test/view_generator_test.dart b/test/view_generator_test.dart new file mode 100644 index 00000000..680536dc --- /dev/null +++ b/test/view_generator_test.dart @@ -0,0 +1,10 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:test/test.dart'; + +main() { + test('default view generator', () async { + var app = new Angel(); + var view = await app.viewGenerator('foo', {'bar': 'baz'}); + expect(view, contains('No view engine')); + }); +} \ No newline at end of file