Init shelf driver
This commit is contained in:
parent
aacd91e267
commit
b115aae975
11 changed files with 160 additions and 84 deletions
|
@ -1,3 +1,7 @@
|
||||||
|
# 2.1.0
|
||||||
|
* `pedantic` lints.
|
||||||
|
* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf.
|
||||||
|
|
||||||
# 2.0.0
|
# 2.0.0
|
||||||
* Removed `supportShelf`.
|
* Removed `supportShelf`.
|
||||||
|
|
||||||
|
|
|
@ -30,8 +30,8 @@ import 'package:shelf/shelf.dart' as shelf;
|
||||||
import 'api/api.dart';
|
import 'api/api.dart';
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
var app = new Angel();
|
var app = Angel();
|
||||||
var http = new AngelHttp(app);
|
var http = AngelHttp(app);
|
||||||
|
|
||||||
// Angel routes on top
|
// Angel routes on top
|
||||||
await app.mountController<ApiController>();
|
await app.mountController<ApiController>();
|
||||||
|
@ -39,7 +39,7 @@ main() async {
|
||||||
// Re-route all other traffic to an
|
// Re-route all other traffic to an
|
||||||
// existing application.
|
// existing application.
|
||||||
app.fallback(embedShelf(
|
app.fallback(embedShelf(
|
||||||
new shelf.Pipeline()
|
shelf.Pipeline()
|
||||||
.addMiddleware(shelf.logRequests())
|
.addMiddleware(shelf.logRequests())
|
||||||
.addHandler(_echoRequest)
|
.addHandler(_echoRequest)
|
||||||
));
|
));
|
||||||
|
@ -62,7 +62,7 @@ handleRequest(shelf.Request request) {
|
||||||
var req = request.context['angel_shelf.request'] as RequestContext;
|
var req = request.context['angel_shelf.request'] as RequestContext;
|
||||||
|
|
||||||
// ... And then interact with it.
|
// ... And then interact with it.
|
||||||
req.container.registerNamedSingleton<Foo>('from_shelf', new Foo());
|
req.container.registerNamedSingleton<Foo>('from_shelf', Foo());
|
||||||
|
|
||||||
// `req.container` is also available.
|
// `req.container` is also available.
|
||||||
var container = request.context['angel_shelf.container'] as Container;
|
var container = request.context['angel_shelf.container'] as Container;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
include: package:pedantic/analysis_options.yaml
|
||||||
analyzer:
|
analyzer:
|
||||||
strong-mode:
|
strong-mode:
|
||||||
implicit-casts: false
|
implicit-casts: false
|
|
@ -2,11 +2,17 @@ import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/http.dart';
|
import 'package:angel_framework/http.dart';
|
||||||
import 'package:angel_shelf/angel_shelf.dart';
|
import 'package:angel_shelf/angel_shelf.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:pretty_logging/pretty_logging.dart';
|
||||||
import 'package:shelf_static/shelf_static.dart';
|
import 'package:shelf_static/shelf_static.dart';
|
||||||
|
|
||||||
main() async {
|
main() async {
|
||||||
var app = new Angel();
|
Logger.root
|
||||||
var http = new AngelHttp(app);
|
..level = Level.ALL
|
||||||
|
..onRecord.listen(prettyLog);
|
||||||
|
|
||||||
|
var app = Angel(logger: Logger('angel_shelf_demo'));
|
||||||
|
var http = AngelHttp(app);
|
||||||
|
|
||||||
// `shelf` request handler
|
// `shelf` request handler
|
||||||
var shelfHandler = createStaticHandler('.',
|
var shelfHandler = createStaticHandler('.',
|
||||||
|
@ -21,9 +27,9 @@ main() async {
|
||||||
return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
|
return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy any other request through to the static file handler
|
// Pass any other request through to the static file handler
|
||||||
app.fallback(wrappedHandler);
|
app.fallback(wrappedHandler);
|
||||||
|
|
||||||
await http.startServer(InternetAddress.loopbackIPv4, 8080);
|
await http.startServer(InternetAddress.loopbackIPv4, 8080);
|
||||||
print('Proxying at ${http.uri}');
|
print('Running at ${http.uri}');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/http.dart';
|
|
||||||
import 'package:angel_framework/http2.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:shelf/shelf.dart' as shelf;
|
import 'package:shelf/shelf.dart' as shelf;
|
||||||
import 'package:stream_channel/stream_channel.dart';
|
import 'package:stream_channel/stream_channel.dart';
|
||||||
|
|
||||||
/// Creates a [shelf.Request] analogous to the input [req].
|
/// Creates a [shelf.Request] analogous to the input [req].
|
||||||
///
|
///
|
||||||
/// The new request's `context` will contain [req.container] as `angel_shelf.container`, as well as
|
/// The request's `context` will contain [req.container] as `angel_shelf.container`, as well as
|
||||||
/// the provided [context], if any.
|
/// the provided [context], if any.
|
||||||
///
|
///
|
||||||
/// The context will also have the original request available as `angel_shelf.request`.
|
/// The context will also have the original request available as `angel_shelf.request`.
|
||||||
|
@ -30,47 +28,26 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
|
||||||
String protocolVersion;
|
String protocolVersion;
|
||||||
Uri requestedUri;
|
Uri requestedUri;
|
||||||
|
|
||||||
if (req is HttpRequestContext && res is HttpResponseContext) {
|
protocolVersion = '1.1';
|
||||||
protocolVersion = req.rawRequest.protocolVersion;
|
requestedUri = Uri.parse('http://${req.hostname}');
|
||||||
requestedUri = req.rawRequest.requestedUri;
|
requestedUri = requestedUri.replace(path: req.uri.path);
|
||||||
|
|
||||||
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
|
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
|
||||||
new Future(() async {
|
Future.sync(res.detach).then((_) {
|
||||||
var rs = res.detach();
|
var ctrl = StreamChannelController<List<int>>();
|
||||||
var socket = await rs.detachSocket(writeHeaders: false);
|
if (req.hasParsedBody) {
|
||||||
var ctrl = new StreamChannelController<List<int>>();
|
req.body.listen(ctrl.local.sink.add,
|
||||||
var body = await req.parseRawRequestBuffer() ?? [];
|
|
||||||
ctrl.local.sink.add(body ?? []);
|
|
||||||
socket.listen(ctrl.local.sink.add,
|
|
||||||
onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
|
onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
|
||||||
ctrl.local.stream.pipe(socket);
|
|
||||||
hijack(ctrl.foreign);
|
|
||||||
}).catchError((e, st) {
|
|
||||||
app.logger?.severe('An error occurred while hijacking a shelf request',
|
|
||||||
e, st as StackTrace);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else if (req is Http2RequestContext && res is Http2ResponseContext) {
|
|
||||||
protocolVersion = '2.0';
|
|
||||||
requestedUri = req.uri;
|
|
||||||
|
|
||||||
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
|
|
||||||
new Future(() async {
|
|
||||||
var rs = await res.detach();
|
|
||||||
var ctrl = new StreamChannelController<List<int>>();
|
|
||||||
var body = await req.parseRawRequestBuffer() ?? [];
|
|
||||||
ctrl.local.sink.add(body ?? []);
|
|
||||||
ctrl.local.stream.listen(rs.sendData, onDone: rs.terminate);
|
|
||||||
hijack(ctrl.foreign);
|
|
||||||
}).catchError((e, st) {
|
|
||||||
stderr.writeln('An error occurred while hijacking a shelf request: $e');
|
|
||||||
stderr.writeln(st);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedError(
|
ctrl.local.sink.close();
|
||||||
'`embedShelf` is only supported for HTTP and HTTP2 requests in Angel.');
|
|
||||||
}
|
}
|
||||||
|
ctrl.local.stream.pipe(res);
|
||||||
|
hijack(ctrl.foreign);
|
||||||
|
}).catchError((e, st) {
|
||||||
|
app.logger?.severe('An error occurred while hijacking a shelf request', e,
|
||||||
|
st as StackTrace);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var url = req.uri;
|
var url = req.uri;
|
||||||
|
|
||||||
|
@ -78,12 +55,12 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
|
||||||
url = url.replace(path: url.path.substring(1));
|
url = url.replace(path: url.path.substring(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new shelf.Request(req.method, requestedUri,
|
return shelf.Request(req.method, requestedUri,
|
||||||
protocolVersion: protocolVersion,
|
protocolVersion: protocolVersion,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
handlerPath: handlerPath,
|
handlerPath: handlerPath,
|
||||||
url: url,
|
url: url,
|
||||||
body: (await req.parseRawRequestBuffer()) ?? [],
|
body: req.body,
|
||||||
context: {'angel_shelf.request': req}
|
context: {'angel_shelf.request': req}
|
||||||
..addAll({'angel_shelf.container': req.container})
|
..addAll({'angel_shelf.container': req.container})
|
||||||
..addAll(context ?? {}),
|
..addAll(context ?? {}),
|
||||||
|
|
|
@ -13,19 +13,21 @@ import 'convert.dart';
|
||||||
RequestHandler embedShelf(shelf.Handler handler,
|
RequestHandler embedShelf(shelf.Handler handler,
|
||||||
{String handlerPath,
|
{String handlerPath,
|
||||||
Map<String, Object> context,
|
Map<String, Object> context,
|
||||||
bool throwOnNullResponse: false}) {
|
bool throwOnNullResponse = false}) {
|
||||||
return (RequestContext req, ResponseContext res) async {
|
return (RequestContext req, ResponseContext res) async {
|
||||||
var shelfRequest = await convertRequest(req, res,
|
var shelfRequest = await convertRequest(req, res,
|
||||||
handlerPath: handlerPath, context: context);
|
handlerPath: handlerPath, context: context);
|
||||||
try {
|
try {
|
||||||
var result = await handler(shelfRequest);
|
var result = await handler(shelfRequest);
|
||||||
if (result is! shelf.Response && result != null) return result;
|
if (result is! shelf.Response && result != null) return result;
|
||||||
if (result == null && throwOnNullResponse == true)
|
if (result == null && throwOnNullResponse == true) {
|
||||||
throw new AngelHttpException('Internal Server Error');
|
throw AngelHttpException('Internal Server Error');
|
||||||
|
}
|
||||||
await mergeShelfResponse(result, res);
|
await mergeShelfResponse(result, res);
|
||||||
return false;
|
return false;
|
||||||
} on shelf.HijackException {
|
} on shelf.HijackException {
|
||||||
// On hijack, do nothing, because the hijack handlers already call res.detach();
|
// On hijack, do nothing, because the hijack handlers already call res.detach();
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
0
lib/src/shelf_driver.dart
Normal file
0
lib/src/shelf_driver.dart
Normal file
77
lib/src/shelf_request.dart
Normal file
77
lib/src/shelf_request.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_container/angel_container.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:mock_request/mock_request.dart';
|
||||||
|
import 'package:shelf/shelf.dart' as shelf;
|
||||||
|
|
||||||
|
class ShelfRequestContext extends RequestContext {
|
||||||
|
@override
|
||||||
|
final Angel app;
|
||||||
|
@override
|
||||||
|
final Container container;
|
||||||
|
@override
|
||||||
|
final shelf.Request rawRequest;
|
||||||
|
@override
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
List<Cookie> _cookies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final MockHttpHeaders headers = MockHttpHeaders();
|
||||||
|
|
||||||
|
ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) {
|
||||||
|
rawRequest.headers.forEach(headers.add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<int>> get body => rawRequest.read();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Cookie> get cookies {
|
||||||
|
if (_cookies == null) {
|
||||||
|
// Parse cookies
|
||||||
|
_cookies = [];
|
||||||
|
var value = headers.value('cookie');
|
||||||
|
if (value != null) {
|
||||||
|
var cookieStrings = value.split(';').map((s) => s.trim());
|
||||||
|
|
||||||
|
for (var cookieString in cookieStrings) {
|
||||||
|
try {
|
||||||
|
_cookies.add(Cookie.fromSetCookieValue(cookieString));
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore malformed cookies, and just don't add them to the container.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get hostname => rawRequest.headers['host'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get method {
|
||||||
|
if (!app.allowMethodOverrides) {
|
||||||
|
return originalMethod;
|
||||||
|
} else {
|
||||||
|
return headers.value('x-http-method-override')?.toUpperCase() ??
|
||||||
|
originalMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get originalMethod => rawRequest.method;
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement remoteAddress
|
||||||
|
InternetAddress get remoteAddress => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement session
|
||||||
|
HttpSession get session => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Uri get uri => rawRequest.url;
|
||||||
|
}
|
0
lib/src/shelf_response.dart
Normal file
0
lib/src/shelf_response.dart
Normal file
|
@ -2,7 +2,7 @@ author: Tobe O <thosakwe@gmail.com>
|
||||||
description: Shelf interop with Angel. Use this to wrap existing server code.
|
description: Shelf interop with Angel. Use this to wrap existing server code.
|
||||||
homepage: https://github.com/angel-dart/shelf
|
homepage: https://github.com/angel-dart/shelf
|
||||||
name: angel_shelf
|
name: angel_shelf
|
||||||
version: 2.0.0
|
version: 2.1.0
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ^2.0.0-alpha
|
angel_framework: ^2.0.0-alpha
|
||||||
path: ^1.0.0
|
path: ^1.0.0
|
||||||
|
@ -10,6 +10,8 @@ dependencies:
|
||||||
stream_channel: ^1.0.0
|
stream_channel: ^1.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
angel_test: ^2.0.0-alpha
|
angel_test: ^2.0.0-alpha
|
||||||
|
pedantic: ^1.0.0
|
||||||
|
pretty_logging: ^1.0.0
|
||||||
shelf_static: ^0.2.8
|
shelf_static: ^0.2.8
|
||||||
test: ^1.0.0
|
test: ^1.0.0
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:angel_shelf/angel_shelf.dart';
|
||||||
import 'package:angel_test/angel_test.dart';
|
import 'package:angel_test/angel_test.dart';
|
||||||
import 'package:charcode/charcode.dart';
|
import 'package:charcode/charcode.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:pretty_logging/pretty_logging.dart';
|
||||||
import 'package:shelf/shelf.dart' as shelf;
|
import 'package:shelf/shelf.dart' as shelf;
|
||||||
import 'package:stream_channel/stream_channel.dart';
|
import 'package:stream_channel/stream_channel.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
@ -16,15 +17,24 @@ main() {
|
||||||
HttpServer server;
|
HttpServer server;
|
||||||
String url;
|
String url;
|
||||||
|
|
||||||
|
String _path(String p) {
|
||||||
|
return Uri(
|
||||||
|
scheme: 'http',
|
||||||
|
host: server.address.address,
|
||||||
|
port: server.port,
|
||||||
|
path: p)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
var handler = new shelf.Pipeline().addHandler((shelf.Request request) {
|
var handler = shelf.Pipeline().addHandler((shelf.Request request) {
|
||||||
if (request.url.path == 'two')
|
if (request.url.path == 'two') {
|
||||||
return new shelf.Response(200, body: json.encode(2));
|
return shelf.Response(200, body: json.encode(2));
|
||||||
else if (request.url.path == 'error')
|
} else if (request.url.path == 'error') {
|
||||||
throw new AngelHttpException.notFound();
|
throw AngelHttpException.notFound();
|
||||||
else if (request.url.path == 'status')
|
} else if (request.url.path == 'status') {
|
||||||
return new shelf.Response.notModified(headers: {'foo': 'bar'});
|
return shelf.Response.notModified(headers: {'foo': 'bar'});
|
||||||
else if (request.url.path == 'hijack') {
|
} else if (request.url.path == 'hijack') {
|
||||||
request.hijack((StreamChannel<List<int>> channel) {
|
request.hijack((StreamChannel<List<int>> channel) {
|
||||||
var sink = channel.sink;
|
var sink = channel.sink;
|
||||||
sink.add(utf8.encode('HTTP/1.1 200 OK\r\n'));
|
sink.add(utf8.encode('HTTP/1.1 200 OK\r\n'));
|
||||||
|
@ -32,25 +42,22 @@ main() {
|
||||||
sink.add(utf8.encode(json.encode({'error': 'crime'})));
|
sink.add(utf8.encode(json.encode({'error': 'crime'})));
|
||||||
sink.close();
|
sink.close();
|
||||||
});
|
});
|
||||||
} else if (request.url.path == 'throw')
|
|
||||||
return null;
|
return null;
|
||||||
else
|
} else if (request.url.path == 'throw') {
|
||||||
return new shelf.Response.ok('Request for "${request.url}"');
|
return null;
|
||||||
|
} else {
|
||||||
|
return shelf.Response.ok('Request for "${request.url}"');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = new Angel();
|
var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog);
|
||||||
var http = new AngelHttp(app);
|
var app = Angel(logger: logger);
|
||||||
|
var http = AngelHttp(app);
|
||||||
app.get('/angel', (req, res) => 'Angel');
|
app.get('/angel', (req, res) => 'Angel');
|
||||||
app.fallback(embedShelf(handler, throwOnNullResponse: true));
|
app.fallback(embedShelf(handler, throwOnNullResponse: true));
|
||||||
app.logger = new Logger.detached('angel_shelf')
|
|
||||||
..onRecord.listen((rec) {
|
|
||||||
stdout.writeln(rec);
|
|
||||||
if (rec.error != null) stdout.writeln(rec.error);
|
|
||||||
if (rec.stackTrace != null) stdout.writeln(rec.stackTrace);
|
|
||||||
});
|
|
||||||
|
|
||||||
server = await http.startServer(InternetAddress.loopbackIPv4, 0);
|
server = await http.startServer(InternetAddress.loopbackIPv4, 0);
|
||||||
client = new c.Rest(url = http.uri.toString());
|
client = c.Rest(url = http.uri.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
@ -59,24 +66,24 @@ main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expose angel side', () async {
|
test('expose angel side', () async {
|
||||||
var response = await client.get('/angel');
|
var response = await client.get(_path('/angel'));
|
||||||
expect(json.decode(response.body), equals('Angel'));
|
expect(json.decode(response.body), equals('Angel'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expose shelf side', () async {
|
test('expose shelf side', () async {
|
||||||
var response = await client.get('/foo');
|
var response = await client.get(_path('/foo'));
|
||||||
expect(response, hasStatus(200));
|
expect(response, hasStatus(200));
|
||||||
expect(response.body, equals('Request for "foo"'));
|
expect(response.body, equals('Request for "foo"'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shelf can return arbitrary values', () async {
|
test('shelf can return arbitrary values', () async {
|
||||||
var response = await client.get('/two');
|
var response = await client.get(_path('/two'));
|
||||||
expect(response, isJson(2));
|
expect(response, isJson(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shelf can hijack', () async {
|
test('shelf can hijack', () async {
|
||||||
try {
|
try {
|
||||||
var client = new HttpClient();
|
var client = HttpClient();
|
||||||
var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
|
var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
|
||||||
var rs = await rq.close();
|
var rs = await rq.close();
|
||||||
var body = await rs.cast<List<int>>().transform(utf8.decoder).join();
|
var body = await rs.cast<List<int>>().transform(utf8.decoder).join();
|
||||||
|
@ -90,17 +97,17 @@ main() {
|
||||||
}, skip: '');
|
}, skip: '');
|
||||||
|
|
||||||
test('shelf can set status code', () async {
|
test('shelf can set status code', () async {
|
||||||
var response = await client.get('/status');
|
var response = await client.get(_path('/status'));
|
||||||
expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
|
expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shelf can throw error', () async {
|
test('shelf can throw error', () async {
|
||||||
var response = await client.get('/error');
|
var response = await client.get(_path('/error'));
|
||||||
expect(response, hasStatus(404));
|
expect(response, hasStatus(404));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throw on null', () async {
|
test('throw on null', () async {
|
||||||
var response = await client.get('/throw');
|
var response = await client.get(_path('/throw'));
|
||||||
expect(response, hasStatus(500));
|
expect(response, hasStatus(500));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue