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_websocket/io.dart' as client; import 'package:http/http.dart' as http; 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 _readHeaders = const {'Accept': 'application/json'}; const Map _writeHeaders = const { 'Accept': 'application/json', 'Content-Type': 'application/json' }; 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); for (var plugin in app.startupHooks) await plugin(app); return new TestClient(app, autoDecodeGzip: autoDecodeGzip != false) ..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 _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 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; TestClient(this.server, {this.autoDecodeGzip: true}) : super(new http.Client(), '/'); 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 websocket({String path, Duration timeout}) async { HttpServer http = server.httpServer; if (http == null) http = await server.startServer(); var url = 'ws://${http.address.address}:${http.port}'; 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; } 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, [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 ..cookies.addAll(cookies) ..session.addAll(session); if (body is Stream>) { await rq.addStream(body); } else if (body is List) { rq.add(body); } else if (body is Map) { if (rq.headers.contentType == null || rq.headers.contentType.mimeType == ContentType.JSON.mimeType) { rq ..headers.contentType = ContentType.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()); } else { throw new UnsupportedError( '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 server.handleRequest(rq); var rs = rq.response; session ..clear() ..addAll(rq.session); Map extractedHeaders = {}; rs.headers.forEach((k, v) { extractedHeaders[k] = v.join(','); }); Stream> stream = rs; if (autoDecodeGzip != false && rs.headers[HttpHeaders.CONTENT_ENCODING]?.contains('gzip') == true) stream = stream.transform(GZIP.decoder); return new http.StreamedResponse(stream, rs.statusCode, contentLength: rs.contentLength, isRedirect: rs.headers[HttpHeaders.LOCATION] != null, headers: extractedHeaders, persistentConnection: rq.headers.value(HttpHeaders.CONNECTION)?.toLowerCase()?.trim() == 'keep-alive' || rq.headers.persistentConnection == true, reasonPhrase: rs.reasonPhrase); } Future delete(url, {Map headers}) => sendUnstreamed('DELETE', url, headers); Future get(url, {Map headers}) => sendUnstreamed('GET', url, headers); Future head(url, {Map headers}) => sendUnstreamed('HEAD', url, headers); Future patch(url, {body, Map headers}) => sendUnstreamed('PATCH', url, headers, body); Future post(url, {body, Map headers}) => sendUnstreamed('POST', url, headers, body); Future put(url, {body, Map headers}) => sendUnstreamed('PUT', url, headers, body); @override String basePath; @override Stream 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(String path, {Type type, client.AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ""); return _services.putIfAbsent( uri, () => new _MockService(this, 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 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()); } } class _MockWebSockets extends client.WebSockets { final TestClient app; _MockWebSockets(this.app, String url) : super(url); @override Future getConnectedWebSocket() async { Map headers = {}; if (app.authToken?.isNotEmpty == true) headers[HttpHeaders.AUTHORIZATION] = 'Bearer ${app.authToken}'; var socket = await WebSocket.connect(basePath, headers: headers); return new IOWebSocketChannel(socket); } }