2016-12-10 17:05:31 +00:00
|
|
|
import 'dart:async';
|
2018-10-21 08:22:41 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
2017-03-25 15:26:00 +00:00
|
|
|
import 'package:angel_client/base_angel_client.dart' as client;
|
2016-12-10 17:05:31 +00:00
|
|
|
import 'package:angel_client/io.dart' as client;
|
2016-12-10 18:11:27 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2017-03-25 15:26:00 +00:00
|
|
|
import 'package:angel_websocket/io.dart' as client;
|
2018-07-09 16:10:25 +00:00
|
|
|
import 'package:http/http.dart' as http hide StreamedResponse;
|
|
|
|
import 'package:http/src/streamed_response.dart';
|
2017-03-25 04:12:21 +00:00
|
|
|
import 'package:mock_request/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
|
|
|
|
2017-03-25 15:26:00 +00:00
|
|
|
final RegExp _straySlashes = new 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'
|
|
|
|
};
|
2018-07-09 16:10:25 +00:00
|
|
|
final Uuid _uuid = new 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,
|
2018-07-09 16:10:25 +00:00
|
|
|
{Map initialSession,
|
|
|
|
bool autoDecodeGzip: true,
|
|
|
|
bool useZone: false}) 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, 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.
|
|
|
|
final HttpSession session = new MockHttpSession(id: 'angel-test-client');
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
String authToken;
|
|
|
|
|
2018-07-09 16:10:25 +00:00
|
|
|
AngelHttp _http;
|
|
|
|
|
|
|
|
TestClient(this.server, {this.autoDecodeGzip: true, bool useZone: false})
|
|
|
|
: super(new http.IOClient(), '/') {
|
|
|
|
_http = new AngelHttp(server, useZone: useZone);
|
|
|
|
}
|
2017-03-25 15:26:00 +00:00
|
|
|
|
2017-11-18 05:04:42 +00:00
|
|
|
Future close() {
|
|
|
|
this.client.close();
|
|
|
|
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!).
|
|
|
|
Future<client.WebSockets> websocket({String path, Duration timeout}) async {
|
2018-07-09 16:10:25 +00:00
|
|
|
HttpServer http = _http.httpServer;
|
|
|
|
if (http == null) http = await _http.startServer();
|
2017-03-25 15:26:00 +00:00
|
|
|
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;
|
2016-12-10 18:11:27 +00:00
|
|
|
}
|
2016-12-10 17:05:31 +00:00
|
|
|
|
2018-07-09 16:10:25 +00:00
|
|
|
Future<http.Response> sendUnstreamed(
|
|
|
|
String method, url, Map<String, String> headers,
|
|
|
|
[body, Encoding encoding]) =>
|
2017-03-25 15:26:00 +00:00
|
|
|
send(method, url, headers, body, encoding).then(http.Response.fromStream);
|
|
|
|
|
2018-07-09 16:10:25 +00:00
|
|
|
Future<StreamedResponse> send(String method, url, Map<String, String> headers,
|
2017-03-25 15:26:00 +00:00
|
|
|
[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)
|
2018-07-09 16:10:25 +00:00
|
|
|
rq.headers.set('authorization', 'Bearer $authToken');
|
2017-03-25 15:26:00 +00:00
|
|
|
|
2018-07-09 16:10:25 +00:00
|
|
|
rq..cookies.addAll(cookies)..session.addAll(session);
|
2017-03-25 15:26:00 +00:00
|
|
|
|
|
|
|
if (body is Stream<List<int>>) {
|
|
|
|
await rq.addStream(body);
|
|
|
|
} else if (body is List<int>) {
|
|
|
|
rq.add(body);
|
|
|
|
} else if (body is Map) {
|
|
|
|
if (rq.headers.contentType == null ||
|
2018-07-09 16:10:25 +00:00
|
|
|
rq.headers.contentType.mimeType == 'application/json') {
|
2017-03-25 15:26:00 +00:00
|
|
|
rq
|
2018-07-09 16:10:25 +00:00
|
|
|
..headers.contentType = new ContentType('application', 'json')
|
|
|
|
..write(json.encode(body.keys.fold<Map<String, dynamic>>(
|
|
|
|
{}, (out, k) => out..[k.toString()] = body[k])));
|
2017-03-25 15:26:00 +00:00
|
|
|
} else if (rq.headers.contentType?.mimeType ==
|
|
|
|
'application/x-www-form-urlencoded') {
|
2018-07-09 16:10:25 +00:00
|
|
|
rq.write(body.keys.fold<List<String>>(
|
|
|
|
[],
|
|
|
|
(out, k) => out
|
|
|
|
..add('$k=' + Uri.encodeComponent(body[k].toString()))).join());
|
2017-03-25 15:26:00 +00:00
|
|
|
} 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();
|
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
|
|
|
|
|
|
|
Map<String, String> extractedHeaders = {};
|
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
|
|
|
|
2018-07-09 16:10:25 +00:00
|
|
|
return new 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:
|
2018-07-09 16:10:25 +00:00
|
|
|
rq.headers.value('connection')?.toLowerCase()?.trim() ==
|
|
|
|
'keep-alive' ||
|
|
|
|
rq.headers.persistentConnection == true,
|
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
|
|
|
Future<http.Response> delete(url, {Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('DELETE', url, headers);
|
|
|
|
|
|
|
|
Future<http.Response> get(url, {Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('GET', url, headers);
|
|
|
|
|
|
|
|
Future<http.Response> head(url, {Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('HEAD', url, headers);
|
|
|
|
|
|
|
|
Future<http.Response> patch(url, {body, Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('PATCH', url, headers, body);
|
2016-12-10 17:05:31 +00:00
|
|
|
|
2017-03-25 15:26:00 +00:00
|
|
|
Future<http.Response> post(url, {body, Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('POST', url, headers, body);
|
|
|
|
|
|
|
|
Future<http.Response> put(url, {body, Map<String, String> headers}) =>
|
|
|
|
sendUnstreamed('PUT', url, headers, body);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String basePath;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Stream<String> authenticateViaPopup(String url, {String eventName: 'token'}) {
|
2017-03-25 04:28:50 +00:00
|
|
|
throw new 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) =>
|
|
|
|
new 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,
|
|
|
|
{Type type, client.AngelDeserializer<Data> deserializer}) {
|
2017-03-25 15:26:00 +00:00
|
|
|
String uri = path.toString().replaceAll(_straySlashes, "");
|
2017-03-29 02:00:25 +00:00
|
|
|
return _services.putIfAbsent(
|
2018-10-21 08:22:41 +00:00
|
|
|
uri,
|
|
|
|
() => new _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,
|
2018-10-21 08:22:41 +00:00
|
|
|
{client.AngelDeserializer<Data> deserializer})
|
2017-03-25 15:26:00 +00:00
|
|
|
: super(null, _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) {
|
2017-03-25 15:26:00 +00:00
|
|
|
if (app.authToken != null && app.authToken.isNotEmpty) {
|
|
|
|
request.headers['Authorization'] = 'Bearer ${app.authToken}';
|
|
|
|
}
|
|
|
|
|
2017-03-29 02:28:58 +00:00
|
|
|
return _app.send(
|
|
|
|
request.method, request.url, request.headers, request.finalize());
|
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 {
|
|
|
|
Map<String, String> headers = {};
|
|
|
|
|
|
|
|
if (app.authToken?.isNotEmpty == true)
|
2018-07-09 16:10:25 +00:00
|
|
|
headers['authorization'] = 'Bearer ${app.authToken}';
|
2017-03-25 15:26:00 +00:00
|
|
|
|
|
|
|
var socket = await WebSocket.connect(basePath, headers: headers);
|
|
|
|
return new IOWebSocketChannel(socket);
|
2016-12-10 17:05:31 +00:00
|
|
|
}
|
|
|
|
}
|