platform/lib/src/client.dart

220 lines
7.4 KiB
Dart
Raw Normal View History

2016-12-10 17:05:31 +00:00
import 'dart:async';
2016-12-10 18:11:27 +00:00
import 'dart:convert';
2016-12-10 17:05:31 +00:00
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;
2017-06-03 17:59:55 +00:00
import 'package:http/http.dart' as http;
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';
2016-12-10 18:11:27 +00:00
import 'package:uuid/uuid.dart';
2017-03-25 15:26:00 +00:00
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
const Map<String, String> _writeHeaders = const {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
2016-12-10 18:11:27 +00:00
final Uuid _uuid = new Uuid();
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,
2017-05-27 13:26:05 +00:00
{Map initialSession, bool autoDecodeGzip: true}) async {
if (!app.isProduction)
2017-11-18 05:04:42 +00:00
app.configuration.putIfAbsent('testMode', () => true);
2017-05-27 13:26:05 +00:00
2017-09-24 04:40:35 +00:00
for (var plugin in app.startupHooks)
2017-05-27 13:26:05 +00:00
await plugin(app);
return new TestClient(app, autoDecodeGzip: autoDecodeGzip != false)
..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;
2017-06-03 17:59:55 +00:00
TestClient(this.server, {this.autoDecodeGzip: true}) : super(new http.Client(), '/');
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 {
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;
2016-12-10 18:11:27 +00:00
}
2016-12-10 17:05:31 +00:00
2017-05-27 13:26:05 +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);
2017-05-27 13:26:05 +00:00
Future<http.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)
rq.headers.set(HttpHeaders.AUTHORIZATION, 'Bearer $authToken');
2017-05-27 13:26:05 +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 ||
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<List<String>>([],
2017-05-27 13:26:05 +00:00
(out, k) => out..add('$k=' + Uri.encodeComponent(body[k])))
.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();
await server.handleRequest(rq);
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 &&
rs.headers[HttpHeaders.CONTENT_ENCODING]?.contains('gzip') == true)
stream = stream.transform(GZIP.decoder);
return new http.StreamedResponse(stream, rs.statusCode,
2017-03-25 15:26:00 +00:00
contentLength: rs.contentLength,
isRedirect: rs.headers[HttpHeaders.LOCATION] != null,
headers: extractedHeaders,
persistentConnection:
2017-05-27 13:26:05 +00:00
rq.headers.value(HttpHeaders.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
Future configure(client.AngelConfigurer configurer) => configurer(this);
2017-03-25 04:28:50 +00:00
2017-03-25 15:26:00 +00:00
@override
client.Service service<T>(String path,
{Type type, client.AngelDeserializer deserializer}) {
String uri = path.toString().replaceAll(_straySlashes, "");
2017-03-29 02:00:25 +00:00
return _services.putIfAbsent(
2017-03-29 02:28:58 +00:00
uri, () => new _MockService(this, uri, deserializer: deserializer));
2017-03-25 15:26:00 +00:00
}
2017-03-25 04:12:21 +00:00
}
2017-03-29 02:00:25 +00:00
class _MockService extends client.BaseAngelService {
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,
2017-03-25 15:26:00 +00:00
{client.AngelDeserializer deserializer})
: super(null, _app, basePath, deserializer: deserializer);
2016-12-10 18:11:27 +00:00
2017-03-25 15:26:00 +00:00
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
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)
headers[HttpHeaders.AUTHORIZATION] = 'Bearer ${app.authToken}';
var socket = await WebSocket.connect(basePath, headers: headers);
return new IOWebSocketChannel(socket);
2016-12-10 17:05:31 +00:00
}
}