platform/packages/test/lib/src/client.dart

210 lines
6.6 KiB
Dart
Raw Normal View History

2016-12-10 17:05:31 +00:00
import 'dart:async';
2019-01-06 17:58:41 +00:00
import 'dart:convert';
2018-10-21 08:22:41 +00:00
import 'dart:io';
2021-05-15 08:35:27 +00:00
import 'package:angel3_client/base_angel_client.dart' as client;
import 'package:angel3_client/io.dart' as client;
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/http.dart';
import 'package:angel3_websocket/io.dart' as client;
2018-07-09 16:10:25 +00:00
import 'package:http/http.dart' as http hide StreamedResponse;
2018-11-08 21:57:56 +00:00
import 'package:http/io_client.dart' as http;
2018-07-09 16:10:25 +00:00
import 'package:http/src/streamed_response.dart';
2021-05-15 08:35:27 +00:00
import 'package:angel3_mock_request/angel3_mock_request.dart';
2017-03-25 15:26:00 +00:00
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';
2018-07-09 16:10:25 +00:00
//import 'package:uuid/uuid.dart';
2016-12-10 18:11:27 +00:00
2021-05-18 11:43:58 +00:00
final RegExp _straySlashes = RegExp(r'(^/)|(/+$)');
2018-07-09 16:10:25 +00:00
/*const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
2017-03-25 15:26:00 +00:00
const Map<String, String> _writeHeaders = const {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
2021-05-15 08:35:27 +00:00
final Uuid _uuid = Uuid();*/
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
/// Shorthand for bootstrapping a [TestClient].
2017-04-25 02:50:37 +00:00
Future<TestClient> connectTo(Angel app,
2021-04-26 01:05:34 +00:00
{Map? initialSession,
2021-05-18 11:43:58 +00:00
bool autoDecodeGzip = true,
bool useZone = false}) async {
print('Load configuration');
2021-02-21 02:47:23 +00:00
if (!app.environment.isProduction) {
app.configuration.putIfAbsent('testMode', () => true);
}
2018-07-09 16:10:25 +00:00
2021-02-21 02:47:23 +00:00
for (var plugin in app.startupHooks) {
2021-05-18 11:43:58 +00:00
print('Load plugins');
2021-02-21 02:47:23 +00:00
await plugin(app);
}
2021-05-15 08:35:27 +00:00
return TestClient(app,
2018-07-09 16:10:25 +00:00
autoDecodeGzip: autoDecodeGzip != false, useZone: useZone)
2017-05-27 13:26:05 +00:00
..session.addAll(initialSession ?? {});
}
2017-03-25 15:26:00 +00:00
/// An `angel_client` that sends mock requests to a server, rather than actual HTTP transactions.
class TestClient extends client.BaseAngelClient {
2017-03-29 02:00:25 +00:00
final Map<String, client.Service> _services = {};
2017-03-25 15:26:00 +00:00
/// Session info to be sent to the server on every request.
2021-05-15 08:35:27 +00:00
final HttpSession session = MockHttpSession(id: 'angel-test-client');
2017-03-25 15:26:00 +00:00
/// A list of cookies to be sent to and received from the server.
final List<Cookie> cookies = [];
2017-04-25 02:50:37 +00:00
/// If `true` (default), the client will automatically decode GZIP response bodies.
final bool autoDecodeGzip;
2017-03-25 15:26:00 +00:00
/// The server instance to mock.
final Angel server;
@override
2021-04-26 01:05:34 +00:00
String? authToken;
2017-03-25 15:26:00 +00:00
2021-04-26 01:05:34 +00:00
late AngelHttp _http;
2018-07-09 16:10:25 +00:00
2021-05-18 11:43:58 +00:00
TestClient(this.server, {this.autoDecodeGzip = true, bool useZone = false})
2021-02-21 02:47:23 +00:00
: super(http.IOClient(), '/') {
_http = AngelHttp(server, useZone: useZone);
2018-07-09 16:10:25 +00:00
}
2017-03-25 15:26:00 +00:00
2021-05-18 11:43:58 +00:00
@override
2017-11-18 05:04:42 +00:00
Future close() {
2021-08-20 04:59:02 +00:00
this.client.close();
2017-11-18 05:04:42 +00:00
return server.close();
}
2017-03-25 15:26:00 +00:00
/// 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!).
2019-01-06 17:41:06 +00:00
Future<client.WebSockets> websocket(
2021-05-18 11:43:58 +00:00
{String path = '/ws', Duration? timeout}) async {
2019-01-06 17:41:06 +00:00
if (_http.server == null) await _http.startServer();
var url = _http.uri.replace(scheme: 'ws', path: path);
2021-02-21 02:47:23 +00:00
var ws = _MockWebSockets(this, url.toString());
2017-03-25 15:26:00 +00:00
await ws.connect(timeout: timeout);
return ws;
2016-12-10 18:11:27 +00:00
}
2016-12-10 17:05:31 +00:00
2021-05-18 11:43:58 +00:00
@override
2019-01-06 17:41:06 +00:00
Future<StreamedResponse> send(http.BaseRequest request) async {
2021-02-21 02:47:23 +00:00
var rq = MockHttpRequest(request.method, request.url);
2019-01-06 17:41:06 +00:00
request.headers.forEach(rq.headers.add);
2017-03-25 15:26:00 +00:00
2019-01-06 17:58:41 +00:00
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');
2019-01-06 17:58:51 +00:00
} else if (rq.headers.value('authorization')?.startsWith('Basic ') ==
true) {
2021-04-26 01:05:34 +00:00
var encoded = rq.headers.value('authorization')!.substring(6);
2019-01-06 17:58:41 +00:00
var decoded = utf8.decode(base64Url.decode(encoded));
var oldRq = rq;
2021-02-21 02:47:23 +00:00
var newRq = MockHttpRequest(rq.method, rq.uri.replace(userInfo: decoded));
2019-01-06 17:58:41 +00:00
oldRq.headers.forEach(newRq.headers.add);
rq = newRq;
}
2021-05-15 08:35:27 +00:00
if (authToken?.isNotEmpty == true) {
2019-01-06 17:41:06 +00:00
rq.headers.add('authorization', 'Bearer $authToken');
2021-05-15 08:35:27 +00:00
}
2021-09-25 14:51:11 +00:00
rq
..cookies.addAll(cookies)
..session.addAll(session);
2017-03-25 15:26:00 +00:00
2019-01-06 17:41:06 +00:00
await request.finalize().pipe(rq);
2018-07-09 16:10:25 +00:00
await _http.handleRequest(rq);
2017-03-25 15:26:00 +00:00
var rs = rq.response;
2017-03-29 02:28:58 +00:00
session
..clear()
..addAll(rq.session);
2017-03-25 15:26:00 +00:00
2021-05-18 11:43:58 +00:00
var extractedHeaders = <String, String>{};
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
rs.headers.forEach((k, v) {
extractedHeaders[k] = v.join(',');
});
2017-04-25 02:50:37 +00:00
Stream<List<int>> stream = rs;
if (autoDecodeGzip != false &&
2018-07-09 16:21:14 +00:00
rs.headers['content-encoding']?.contains('gzip') == true) {
2018-07-09 16:10:25 +00:00
stream = stream.transform(gzip.decoder);
2018-07-09 16:21:14 +00:00
}
2017-04-25 02:50:37 +00:00
2021-05-18 11:43:58 +00:00
// Calling persistentConnection causes LateInitialization Exception
2021-02-21 02:47:23 +00:00
//var keepAliveState = rq.headers?.persistentConnection;
//if (keepAliveState == null) {
// keepAliveState = false;
//}
return StreamedResponse(stream, rs.statusCode,
2017-03-25 15:26:00 +00:00
contentLength: rs.contentLength,
2018-07-09 16:10:25 +00:00
isRedirect: rs.headers['location'] != null,
2017-03-25 15:26:00 +00:00
headers: extractedHeaders,
persistentConnection:
2021-04-26 01:05:34 +00:00
rq.headers.value('connection')?.toLowerCase().trim() ==
2021-02-21 02:47:23 +00:00
'keep-alive',
//|| keepAliveState,
2017-03-25 15:26:00 +00:00
reasonPhrase: rs.reasonPhrase);
2016-12-10 18:11:27 +00:00
}
2017-03-25 15:26:00 +00:00
@override
2021-04-26 01:05:34 +00:00
late String basePath;
2017-03-25 15:26:00 +00:00
@override
2021-05-18 11:43:58 +00:00
Stream<String> authenticateViaPopup(String url,
{String eventName = 'token'}) {
2021-02-21 02:47:23 +00:00
throw UnsupportedError(
2017-03-25 15:26:00 +00:00
'MockClient does not support authentication via popup.');
}
@override
2018-10-21 08:22:41 +00:00
Future configure(client.AngelConfigurer configurer) =>
2021-02-21 02:47:23 +00:00
Future.sync(() => configurer(this));
2017-03-25 04:28:50 +00:00
2017-03-25 15:26:00 +00:00
@override
2018-10-21 08:22:41 +00:00
client.Service<Id, Data> service<Id, Data>(String path,
2021-04-26 01:05:34 +00:00
{Type? type, client.AngelDeserializer<Data>? deserializer}) {
2021-05-18 11:43:58 +00:00
var uri = path.toString().replaceAll(_straySlashes, '');
2021-02-21 02:47:23 +00:00
return _services.putIfAbsent(uri,
() => _MockService<Id, Data>(this, uri, deserializer: deserializer))
as client.Service<Id, Data>;
2017-03-25 15:26:00 +00:00
}
2017-03-25 04:12:21 +00:00
}
2018-10-21 08:22:41 +00:00
class _MockService<Id, Data> extends client.BaseAngelService<Id, Data> {
2017-03-25 15:26:00 +00:00
final TestClient _app;
2016-12-10 18:11:27 +00:00
2017-03-29 02:00:25 +00:00
_MockService(this._app, String basePath,
2021-04-26 01:05:34 +00:00
{client.AngelDeserializer<Data>? deserializer})
2021-07-10 02:51:07 +00:00
: super(_app, _app, basePath, deserializer: deserializer);
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
@override
2018-07-09 16:10:25 +00:00
Future<StreamedResponse> send(http.BaseRequest request) {
2021-04-26 01:05:34 +00:00
if (app.authToken != null && app.authToken!.isNotEmpty) {
2019-01-06 17:41:06 +00:00
request.headers['authorization'] ??= 'Bearer ${app.authToken}';
2017-03-25 15:26:00 +00:00
}
2019-01-06 17:41:06 +00:00
return _app.send(request);
2017-03-25 15:26:00 +00:00
}
}
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
class _MockWebSockets extends client.WebSockets {
final TestClient app;
2016-12-10 17:05:31 +00:00
2017-03-25 15:26:00 +00:00
_MockWebSockets(this.app, String url) : super(url);
2016-12-10 17:05:31 +00:00
@override
2017-03-25 15:26:00 +00:00
Future<WebSocketChannel> getConnectedWebSocket() async {
2021-05-18 11:43:58 +00:00
var headers = <String, String>{};
2017-03-25 15:26:00 +00:00
2021-05-18 11:43:58 +00:00
if (app.authToken?.isNotEmpty == true) {
2018-07-09 16:10:25 +00:00
headers['authorization'] = 'Bearer ${app.authToken}';
2021-05-18 11:43:58 +00:00
}
2017-03-25 15:26:00 +00:00
2019-01-06 17:41:06 +00:00
var socket = await WebSocket.connect(baseUrl.toString(), headers: headers);
2021-02-21 02:47:23 +00:00
return IOWebSocketChannel(socket);
2016-12-10 17:05:31 +00:00
}
}