diff --git a/lib/angel_client.dart b/lib/angel_client.dart index 04ed80c8..e6bde541 100644 --- a/lib/angel_client.dart +++ b/lib/angel_client.dart @@ -4,7 +4,7 @@ library angel_client; import 'dart:async'; import 'dart:convert'; import 'package:http/src/response.dart' as http; -export 'package:angel_framework/src/http/angel_http_exception.dart'; +export 'package:angel_http_exception/angel_http_exception.dart'; /// A function that configures an [Angel] client in some way. typedef Future AngelConfigurer(Angel app); diff --git a/lib/base_angel_client.dart b/lib/base_angel_client.dart index e9552ff1..8f9884da 100644 --- a/lib/base_angel_client.dart +++ b/lib/base_angel_client.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:angel_framework/src/http/angel_http_exception.dart'; +import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:collection/collection.dart'; import 'package:http/src/base_client.dart' as http; import 'package:http/src/base_request.dart' as http; @@ -72,17 +72,29 @@ abstract class BaseAngelClient extends Angel { String reviveEndpoint: '/auth/token'}) async { if (type == null) { final url = '$basePath$reviveEndpoint'; + String token; + + if (credentials is String) + token = credentials; + else if (credentials is Map && credentials.containsKey('token')) + token = credentials['token']; + + if (token == null) { + throw new ArgumentError( + 'If `type` is not set, a JWT is expected as the `credentials` argument.'); + } + final response = await client.post(url, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${credentials['token']}' + 'Authorization': 'Bearer $token' }); - try { - if (_invalid(response)) { - throw failure(response); - } + if (_invalid(response)) { + throw failure(response); + } + try { final json = JSON.decode(response.body); if (json is! Map || @@ -96,6 +108,8 @@ abstract class BaseAngelClient extends Angel { var r = new AngelAuthResult.fromMap(json); _onAuthenticated.add(r); return r; + } on AngelHttpException { + rethrow; } catch (e, st) { throw failure(response, error: e, stack: st); } @@ -110,11 +124,11 @@ abstract class BaseAngelClient extends Angel { response = await client.post(url, headers: _writeHeaders); } - try { - if (_invalid(response)) { - throw failure(response); - } + if (_invalid(response)) { + throw failure(response); + } + try { final json = JSON.decode(response.body); if (json is! Map || @@ -128,6 +142,8 @@ abstract class BaseAngelClient extends Angel { var r = new AngelAuthResult.fromMap(json); _onAuthenticated.add(r); return r; + } on AngelHttpException { + rethrow; } catch (e, st) { throw failure(response, error: e, stack: st); } diff --git a/pubspec.yaml b/pubspec.yaml index 3eaaa991..954111d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,14 +1,17 @@ name: angel_client -version: 1.0.7 +version: 1.1.0-alpha description: Client library for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_client environment: sdk: ">=1.21.0" dependencies: - angel_framework: ">=1.0.0-dev <2.0.0" + angel_http_exception: ^1.0.0 http: ">= 0.11.3 < 0.12.0" json_god: ">=2.0.0-beta <3.0.0" merge_map: ">=1.0.0 <2.0.0" dev_dependencies: + angel_framework: ^1.1.0-alpha + angel_model: ^1.0.0 + mock_request: ^1.0.0 test: ">= 0.12.13 < 0.13.0" diff --git a/test/all_test.dart b/test/all_test.dart new file mode 100644 index 00000000..2bd2d10b --- /dev/null +++ b/test/all_test.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'package:angel_client/angel_client.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + var app = new MockAngel(); + Service todoService = app.service('api/todos'); + + test('sets method,body,headers,path', () async { + await app.post('/post', headers: {'method': 'post'}, body: 'post'); + expect(app.client.spec.method, 'POST'); + expect(app.client.spec.path, '/post'); + expect(app.client.spec.headers['method'], 'post'); + expect(await read(app.client.spec.request.finalize()), 'post'); + }); + + group('service methods', () { + test('index', () async { + await todoService.index(); + expect(app.client.spec.method, 'GET'); + expect(app.client.spec.path, '/api/todos'); + }); + + test('read', () async { + await todoService.read('sleep'); + expect(app.client.spec.method, 'GET'); + expect(app.client.spec.path, '/api/todos/sleep'); + }); + + test('create', () async { + await todoService.create({}); + expect(app.client.spec.method, 'POST'); + expect(app.client.spec.headers['content-type'], + startsWith('application/json')); + expect(app.client.spec.path, '/api/todos/'); + expect(await read(app.client.spec.request.finalize()), '{}'); + }); + + test('modify', () async { + await todoService.modify('sleep', {}); + expect(app.client.spec.method, 'PATCH'); + expect(app.client.spec.headers['content-type'], + startsWith('application/json')); + expect(app.client.spec.path, '/api/todos/sleep'); + expect(await read(app.client.spec.request.finalize()), '{}'); + }); + + test('update', () async { + await todoService.update('sleep', {}); + expect(app.client.spec.method, 'POST'); + expect(app.client.spec.headers['content-type'], + startsWith('application/json')); + expect(app.client.spec.path, '/api/todos/sleep'); + expect(await read(app.client.spec.request.finalize()), '{}'); + }); + + test('remove', () async { + await todoService.remove('sleep'); + expect(app.client.spec.method, 'DELETE'); + expect(app.client.spec.path, '/api/todos/sleep'); + }); + }); + + group('authentication', () { + test('no type, no token throws', () async { + expect(app.authenticate, throwsArgumentError); + }); + + test('no type defaults to token', () async { + await app.authenticate(credentials: ''); + expect(app.client.spec.path, '/auth/token'); + }); + + test('sets type', () async { + await app.authenticate(type: 'local'); + expect(app.client.spec.path, '/auth/local'); + }); + + test('token sends headers', () async { + await app.authenticate(credentials: ''); + expect(app.client.spec.headers['authorization'], 'Bearer '); + }); + + test('credentials send right body', () async { + await app + .authenticate(type: 'local', credentials: {'username': 'password'}); + expect( + await read(app.client.spec.request.finalize()), + JSON.encode({'username': 'password'}), + ); + }); + }); +} diff --git a/test/browser_test.dart b/test/browser_test.dart deleted file mode 100644 index c6b71bac..00000000 --- a/test/browser_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -@TestOn('browser') -import 'package:angel_client/browser.dart'; -import 'package:test/test.dart'; -import 'for_browser_tests.dart'; - -main() { - test("list todos", () async { - var channel = spawnHybridCode(SERVER); - String url = await channel.stream.first; - print(url); - var app = new Rest(url); - var todoService = app.service("todos"); - - var todos = await todoService.index(); - expect(todos, isEmpty); - }); - - test('create todos', () async { - var channel = spawnHybridCode(SERVER); - String url = await channel.stream.first; - print(url); - var app = new Rest(url); - var todoService = app.service("todos"); - - var data = {'hello': 'world'}; - var response = await todoService.create(data); - print('Created response: $response'); - - var todos = await todoService.index(); - expect(todos, hasLength(1)); - - Map todo = todos.first; - expect(todo, equals(data)); - }); -} diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..536665a2 --- /dev/null +++ b/test/common.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:angel_client/base_angel_client.dart'; +import 'package:http/src/base_client.dart' as http; +import 'package:http/src/base_request.dart' as http; +import 'package:http/src/streamed_response.dart' as http; + +Future read(Stream> stream) => + stream.transform(UTF8.decoder).join(); + +class MockAngel extends BaseAngelClient { + @override + final SpecClient client = new SpecClient(); + + MockAngel() : super(null, 'http://localhost:3000'); + + @override + authenticateViaPopup(String url, {String eventName: 'token'}) { + throw new UnsupportedError('Nope'); + } +} + +class SpecClient extends http.BaseClient { + Spec _spec; + + Spec get spec => _spec; + + @override + send(http.BaseRequest request) { + _spec = new Spec(request, request.method, request.url.path, request.headers, + request.contentLength); + var data = {'text': 'Clean your room!', 'completed': true}; + + if (request.url.path.contains('auth')) + data = { + 'token': '', + 'data': {'username': 'password'} + }; + + return new Future.value(new http.StreamedResponse( + new Stream>.fromIterable([UTF8.encode(JSON.encode(data))]), + 200, + headers: { + 'content-type': 'application/json', + }, + )); + } +} + +class Spec { + final http.BaseRequest request; + final String method, path; + final Map headers; + final int contentLength; + + Spec(this.request, this.method, this.path, this.headers, this.contentLength); + + @override + String toString() { + return { + 'method': method, + 'path': path, + 'headers': headers, + 'content_length': contentLength, + }.toString(); + } +} diff --git a/test/for_browser_tests.dart b/test/for_browser_tests.dart deleted file mode 100644 index 12378097..00000000 --- a/test/for_browser_tests.dart +++ /dev/null @@ -1,30 +0,0 @@ -const String SERVER = ''' -import 'dart:io'; -import "package:angel_framework/angel_framework.dart"; -import "package:angel_framework/common.dart"; -import 'package:stream_channel/stream_channel.dart'; - -hybridMain(StreamChannel channel) async { - var app = new Angel(); - - app.before.add((req, ResponseContext res) { - res.headers["Access-Control-Allow-Origin"] = "*"; - return true; - }); - - app.use("/todos", new TypedService(new MapService())); - - var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); - - print("Server up; listening at http://localhost:\${server.port}"); - channel.sink.add('http://\${server.address.address}:\${server.port}'); -} - -class Todo extends Model { - String hello; - - Todo({int id, this.hello}) { - this.id = id; - } -} -'''; diff --git a/test/io_test.dart b/test/io_test.dart deleted file mode 100644 index 61c8882e..00000000 --- a/test/io_test.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:io'; -import 'package:angel_client/io.dart' as client; -import 'package:angel_framework/angel_framework.dart' as server; -import 'package:json_god/json_god.dart' as god; -import 'package:test/test.dart'; -import 'shared.dart'; - -main() { - group("rest", () { - server.Angel serverApp = new server.Angel(); - server.HookedService serverPostcards; - client.Angel clientApp; - client.Service clientPostcards; - client.Service clientTypedPostcards; - HttpServer httpServer; - String url; - - setUp(() async { - httpServer = - await serverApp.startServer(InternetAddress.LOOPBACK_IP_V4, 0); - url = "http://localhost:${httpServer.port}"; - serverApp.use("/postcards", new server.TypedService(new server.MapService())); - serverPostcards = serverApp.service("postcards"); - - clientApp = new client.Rest(url); - clientPostcards = clientApp.service("postcards"); - clientTypedPostcards = clientApp.service("postcards", type: Postcard); - }); - - tearDown(() async { - await httpServer.close(force: true); - }); - - test('plain requests', () async { - final response = await clientApp.get('/foo'); - print(response.body); - }); - - test("index", () async { - Map niagara = await clientPostcards.create( - new Postcard(location: "Niagara Falls", message: "Missing you!")); - Postcard niagaraFalls = new Postcard.fromJson(niagara); - - print('Niagara Falls: ${niagaraFalls.toJson()}'); - - List indexed = await clientPostcards.index(); - print(indexed); - - expect(indexed.length, equals(1)); - expect(indexed[0].keys.length, equals(3)); - expect(indexed[0]['id'], equals(niagaraFalls.id)); - expect(indexed[0]['location'], equals(niagaraFalls.location)); - expect(indexed[0]['message'], equals(niagaraFalls.message)); - - Map l = await clientPostcards.create(new Postcard( - location: "The Louvre", message: "The Mona Lisa was watching me!")); - Postcard louvre = new Postcard.fromJson(l); - print(god.serialize(louvre)); - List typedIndexed = await clientTypedPostcards.index(); - expect(typedIndexed.length, equals(2)); - expect(typedIndexed[1], equals(louvre)); - }); - - test("create/read", () async { - Map opry = {"location": "Grand Ole Opry", "message": "Yeehaw!"}; - var created = await clientPostcards.create(opry); - print(created); - - expect(created['id'] == null, equals(false)); - expect(created["location"], equals(opry["location"])); - expect(created["message"], equals(opry["message"])); - - var read = await clientPostcards.read(created['id']); - print(read); - expect(read['id'], equals(created['id'])); - expect(read['location'], equals(created['location'])); - expect(read['message'], equals(created['message'])); - - Postcard canyon = new Postcard( - location: "Grand Canyon", - message: "But did you REALLY experience it???"); - created = await clientTypedPostcards.create(canyon); - print(god.serialize(created)); - - expect(created.location, equals(canyon.location)); - expect(created.message, equals(canyon.message)); - - read = await clientTypedPostcards.read(created.id); - print(god.serialize(read)); - expect(read.id, equals(created.id)); - expect(read.location, equals(created.location)); - expect(read.message, equals(created.message)); - }); - - test("modify/update", () async { - var innerPostcards = - serverPostcards.inner as server.TypedService; - print(innerPostcards.items); - Postcard mecca = await clientTypedPostcards - .create(new Postcard(location: "Mecca", message: "Pilgrimage")); - print(god.serialize(mecca)); - - // I'm too lazy to write the tests twice, because I know it works - // So I'll modify using the type-based client, and update using the - // map-based one - - print("Postcards on server: " + - god.serialize(await serverPostcards.index())); - print("Postcards on client: " + - god.serialize(await clientPostcards.index())); - - Postcard modified = await clientTypedPostcards - .modify(mecca.id, {"location": "Saudi Arabia"}); - print(god.serialize(modified)); - expect(modified.id, equals(mecca.id)); - expect(modified.location, equals("Saudi Arabia")); - expect(modified.message, equals(mecca.message)); - - Map updated = await clientPostcards - .update(mecca.id, {"location": "Full", "message": "Overwrite"}); - print(updated); - - expect(updated.keys.length, equals(3)); - expect(updated['id'], equals(mecca.id)); - expect(updated['location'], equals("Full")); - expect(updated['message'], equals("Overwrite")); - }); - - test("remove", () async { - Postcard remove1 = await clientTypedPostcards - .create({"location": "remove", "message": "#1"}); - Postcard remove2 = await clientTypedPostcards - .create({"location": "remove", "message": "#2"}); - print(god.serialize([remove1, remove2])); - - Map removed1 = await clientPostcards.remove(remove1.id); - expect(removed1.keys.length, equals(3)); - expect(removed1['id'], equals(remove1.id)); - expect(removed1['location'], equals(remove1.location)); - expect(removed1['message'], equals(remove1.message)); - - Postcard removed2 = await clientTypedPostcards.remove(remove2.id); - expect(removed2, equals(remove2)); - }); - }); -} diff --git a/test/shared.dart b/test/shared.dart index 6d139b4e..7b8af9f6 100644 --- a/test/shared.dart +++ b/test/shared.dart @@ -1,4 +1,4 @@ -import "package:angel_framework/common.dart"; +import 'package:angel_model/angel_model.dart'; class Postcard extends Model { String location;