import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:angel3_client/base_angel_client.dart' as client; import 'package:angel3_framework/angel3_framework.dart'; import 'package:angel3_framework/http.dart'; import 'package:angel3_websocket/io.dart' as client; import 'package:http/http.dart' as http hide StreamedResponse; import 'package:http/http.dart'; import 'package:http/io_client.dart' as http; import 'package:angel3_mock_request/angel3_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 = RegExp(r'(^/)|(/+$)'); /*const Map _readHeaders = const {'Accept': 'application/json'}; const Map _writeHeaders = const { 'Accept': 'application/json', 'Content-Type': 'application/json' }; final Uuid _uuid = Uuid();*/ /// Shorthand for bootstrapping a [TestClient]. Future connectTo(Angel app, {Map? initialSession, bool autoDecodeGzip = true, bool useZone = false}) async { print('Load configuration'); if (!app.environment.isProduction) { app.configuration.putIfAbsent('testMode', () => true); } for (var plugin in app.startupHooks) { print('Load plugins'); await plugin(app); } return TestClient(app, autoDecodeGzip: autoDecodeGzip != false, useZone: useZone) ..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 = 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; late AngelHttp _http; TestClient(this.server, {this.autoDecodeGzip = true, bool useZone = false}) : super(http.IOClient(), '/') { _http = AngelHttp(server, useZone: useZone); } @override 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 = '/ws', Duration? timeout}) async { if (_http.server == null) await _http.startServer(); var url = _http.uri.replace(scheme: 'ws', path: path); var ws = _MockWebSockets(this, url.toString()); await ws.connect(timeout: timeout); return ws; } @override Future send(http.BaseRequest request) async { var rq = MockHttpRequest(request.method, request.url); request.headers.forEach(rq.headers.add); if (request.url.userInfo.isNotEmpty) { // Attempt to send as Basic auth var encoded = base64Url.encode(utf8.encode(request.url.userInfo)); rq.headers.add('authorization', 'Basic $encoded'); } else if (rq.headers.value('authorization')?.startsWith('Basic ') == true) { var encoded = rq.headers.value('authorization')!.substring(6); var decoded = utf8.decode(base64Url.decode(encoded)); var oldRq = rq; var newRq = MockHttpRequest(rq.method, rq.uri.replace(userInfo: decoded)); oldRq.headers.forEach(newRq.headers.add); rq = newRq; } if (authToken?.isNotEmpty == true) { rq.headers.add('authorization', 'Bearer $authToken'); } rq ..cookies.addAll(cookies) ..session.addAll(session); await request.finalize().pipe(rq); await _http.handleRequest(rq); var rs = rq.response; session ..clear() ..addAll(rq.session); var extractedHeaders = {}; rs.headers.forEach((k, v) { extractedHeaders[k] = v.join(','); }); Stream> stream = rs; if (autoDecodeGzip != false && rs.headers['content-encoding']?.contains('gzip') == true) { stream = stream.transform(gzip.decoder); } // Calling persistentConnection causes LateInitialization Exception //var keepAliveState = rq.headers?.persistentConnection; //if (keepAliveState == null) { // keepAliveState = false; //} return StreamedResponse(stream, rs.statusCode, contentLength: rs.contentLength, isRedirect: rs.headers['location'] != null, headers: extractedHeaders, persistentConnection: rq.headers.value('connection')?.toLowerCase().trim() == 'keep-alive', //|| keepAliveState, reasonPhrase: rs.reasonPhrase); } //@override late String basePath; @override Stream authenticateViaPopup(String url, {String eventName = 'token'}) { throw UnsupportedError( 'MockClient does not support authentication via popup.'); } @override Future configure(client.AngelConfigurer configurer) => Future.sync(() => configurer(this)); @override client.Service service(String path, {Type? type, client.AngelDeserializer? deserializer}) { var uri = path.toString().replaceAll(_straySlashes, ''); return _services.putIfAbsent(uri, () => _MockService(this, uri, deserializer: deserializer)) as client.Service; } } class _MockService extends client.BaseAngelService { final TestClient _app; _MockService(this._app, String basePath, {client.AngelDeserializer? deserializer}) : super(_app, _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); } } class _MockWebSockets extends client.WebSockets { final TestClient app; _MockWebSockets(this.app, String url) : super(url); @override Future getConnectedWebSocket() async { var headers = {}; if (app.authToken?.isNotEmpty == true) { headers['authorization'] = 'Bearer ${app.authToken}'; } var socket = await WebSocket.connect(baseUrl.toString(), headers: headers); return IOWebSocketChannel(socket); } }