| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| @TestOn('vm') |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:http/http.dart' as http; |
| import 'package:http_parser/http_parser.dart' as parser; |
| import 'package:scheduled_test/scheduled_stream.dart'; |
| import 'package:scheduled_test/scheduled_test.dart'; |
| import 'package:shelf/shelf.dart'; |
| import 'package:shelf/shelf_io.dart' as shelf_io; |
| |
| import 'ssl_certs.dart'; |
| import 'test_util.dart'; |
| |
| void main() { |
| test('sync handler returns a value to the client', () { |
| _scheduleServer(syncHandler); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /'); |
| }); |
| }); |
| |
| test('async handler returns a value to the client', () { |
| _scheduleServer(asyncHandler); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /'); |
| }); |
| }); |
| |
| test('sync null response leads to a 500', () { |
| _scheduleServer((request) => null); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.INTERNAL_SERVER_ERROR); |
| expect(response.body, 'Internal Server Error'); |
| }); |
| }); |
| |
| test('async null response leads to a 500', () { |
| _scheduleServer((request) => new Future.value(null)); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.INTERNAL_SERVER_ERROR); |
| expect(response.body, 'Internal Server Error'); |
| }); |
| }); |
| |
| test('thrown error leads to a 500', () { |
| _scheduleServer((request) { |
| throw new UnsupportedError('test'); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.INTERNAL_SERVER_ERROR); |
| expect(response.body, 'Internal Server Error'); |
| }); |
| }); |
| |
| test('async error leads to a 500', () { |
| _scheduleServer((request) { |
| return new Future.error('test'); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.INTERNAL_SERVER_ERROR); |
| expect(response.body, 'Internal Server Error'); |
| }); |
| }); |
| |
| test('Request is populated correctly', () { |
| var path = '/foo/bar?qs=value'; |
| |
| _scheduleServer((request) { |
| expect(request.contentLength, 0); |
| expect(request.method, 'GET'); |
| |
| var expectedUrl = 'http://localhost:$_serverPort$path'; |
| expect(request.requestedUri, Uri.parse(expectedUrl)); |
| |
| expect(request.url.path, 'foo/bar'); |
| expect(request.url.pathSegments, ['foo', 'bar']); |
| expect(request.protocolVersion, '1.1'); |
| expect(request.url.query, 'qs=value'); |
| expect(request.handlerPath, '/'); |
| |
| return syncHandler(request); |
| }); |
| |
| return schedule(() => http.get('http://localhost:$_serverPort$path')) |
| .then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /foo/bar'); |
| }); |
| }); |
| |
| test('chunked requests are un-chunked', () { |
| _scheduleServer(expectAsync1((request) { |
| expect(request.contentLength, isNull); |
| expect(request.method, 'POST'); |
| expect(request.headers, isNot(contains(HttpHeaders.TRANSFER_ENCODING))); |
| expect( |
| request.read().toList(), |
| completion(equals([ |
| [1, 2, 3, 4] |
| ]))); |
| return new Response.ok(null); |
| })); |
| |
| schedule(() async { |
| var request = new http.StreamedRequest( |
| 'POST', Uri.parse('http://localhost:$_serverPort')); |
| request.sink.add([1, 2, 3, 4]); |
| request.sink.close(); |
| |
| var response = await request.send(); |
| expect(response.statusCode, HttpStatus.OK); |
| }); |
| }); |
| |
| test('custom response headers are received by the client', () { |
| _scheduleServer((request) { |
| return new Response.ok('Hello from /', |
| headers: {'test-header': 'test-value', 'test-list': 'a, b, c'}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.headers['test-header'], 'test-value'); |
| expect(response.body, 'Hello from /'); |
| }); |
| }); |
| |
| test('custom status code is received by the client', () { |
| _scheduleServer((request) { |
| return new Response(299, body: 'Hello from /'); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, 299); |
| expect(response.body, 'Hello from /'); |
| }); |
| }); |
| |
| test('custom request headers are received by the handler', () { |
| _scheduleServer((request) { |
| expect(request.headers, containsPair('custom-header', 'client value')); |
| |
| // dart:io HttpServer splits multi-value headers into an array |
| // validate that they are combined correctly |
| expect(request.headers, containsPair('multi-header', 'foo,bar,baz')); |
| return syncHandler(request); |
| }); |
| |
| var headers = { |
| 'custom-header': 'client value', |
| 'multi-header': 'foo,bar,baz' |
| }; |
| |
| return _scheduleGet(headers: headers).then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /'); |
| }); |
| }); |
| |
| test('post with empty content', () { |
| _scheduleServer((request) { |
| expect(request.mimeType, isNull); |
| expect(request.encoding, isNull); |
| expect(request.method, 'POST'); |
| expect(request.contentLength, 0); |
| |
| return request.readAsString().then((body) { |
| expect(body, ''); |
| return syncHandler(request); |
| }); |
| }); |
| |
| return _schedulePost().then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.stream.bytesToString(), completion('Hello from /')); |
| }); |
| }); |
| |
| test('post with request content', () { |
| _scheduleServer((request) { |
| expect(request.mimeType, 'text/plain'); |
| expect(request.encoding, UTF8); |
| expect(request.method, 'POST'); |
| expect(request.contentLength, 9); |
| |
| return request.readAsString().then((body) { |
| expect(body, 'test body'); |
| return syncHandler(request); |
| }); |
| }); |
| |
| return _schedulePost(body: 'test body').then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.stream.bytesToString(), completion('Hello from /')); |
| }); |
| }); |
| |
| test('supports request hijacking', () { |
| _scheduleServer((request) { |
| expect(request.method, 'POST'); |
| |
| request.hijack(expectAsync2((stream, sink) { |
| expect(stream.first, completion(equals("Hello".codeUnits))); |
| |
| sink.add(("HTTP/1.1 404 Not Found\r\n" |
| "Date: Mon, 23 May 2005 22:38:34 GMT\r\n" |
| "Content-Length: 13\r\n" |
| "\r\n" |
| "Hello, world!") |
| .codeUnits); |
| sink.close(); |
| })); |
| }); |
| |
| return _schedulePost(body: "Hello").then((response) { |
| expect(response.statusCode, HttpStatus.NOT_FOUND); |
| expect(response.headers["date"], "Mon, 23 May 2005 22:38:34 GMT"); |
| expect( |
| response.stream.bytesToString(), completion(equals("Hello, world!"))); |
| }); |
| }); |
| |
| test('reports an error if a HijackException is thrown without hijacking', () { |
| _scheduleServer((request) => throw const HijackException()); |
| |
| return _scheduleGet().then((response) { |
| expect(response.statusCode, HttpStatus.INTERNAL_SERVER_ERROR); |
| }); |
| }); |
| |
| test('passes asynchronous exceptions to the parent error zone', () { |
| return runZoned(() { |
| return shelf_io.serve((request) { |
| new Future(() => throw 'oh no'); |
| return syncHandler(request); |
| }, 'localhost', 0).then((server) { |
| return http.get('http://localhost:${server.port}').then((response) { |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /'); |
| server.close(); |
| }); |
| }); |
| }, onError: expectAsync1((error) { |
| expect(error, equals('oh no')); |
| })); |
| }); |
| |
| test("doesn't pass asynchronous exceptions to the root error zone", () async { |
| var response = await Zone.ROOT.run(() async { |
| var server = await shelf_io.serve((request) { |
| new Future(() => throw 'oh no'); |
| return syncHandler(request); |
| }, 'localhost', 0); |
| |
| try { |
| return await http.get('http://localhost:${server.port}'); |
| } finally { |
| await server.close(); |
| } |
| }); |
| |
| expect(response.statusCode, HttpStatus.OK); |
| expect(response.body, 'Hello from /'); |
| }); |
| |
| test('a bad HTTP request results in a 500 response', () { |
| Socket socket; |
| |
| _scheduleServer(syncHandler); |
| |
| schedule(() { |
| return Socket.connect('localhost', _serverPort).then((value) { |
| socket = value; |
| |
| currentSchedule.onComplete.schedule(() { |
| return socket.close(); |
| }, 'close the socket'); |
| }); |
| }); |
| |
| schedule(() { |
| socket.write('GET / HTTP/1.1\r\n'); |
| socket.write('Host: ^^super bad !@#host\r\n'); |
| socket.write('\r\n'); |
| return socket.close(); |
| }); |
| |
| schedule(() async { |
| expect(await UTF8.decodeStream(socket), |
| contains('500 Internal Server Error')); |
| }); |
| }); |
| |
| group('date header', () { |
| test('is sent by default', () { |
| _scheduleServer(syncHandler); |
| |
| // Update beforeRequest to be one second earlier. HTTP dates only have |
| // second-level granularity and the request will likely take less than a |
| // second. |
| var beforeRequest = new DateTime.now().subtract(new Duration(seconds: 1)); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, contains('date')); |
| var responseDate = parser.parseHttpDate(response.headers['date']); |
| |
| expect(responseDate.isAfter(beforeRequest), isTrue); |
| expect(responseDate.isBefore(new DateTime.now()), isTrue); |
| }); |
| }); |
| |
| test('defers to header in response', () { |
| var date = new DateTime.utc(1981, 6, 5); |
| _scheduleServer((request) { |
| return new Response.ok('test', |
| headers: {HttpHeaders.DATE: parser.formatHttpDate(date)}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, contains('date')); |
| var responseDate = parser.parseHttpDate(response.headers['date']); |
| expect(responseDate, date); |
| }); |
| }); |
| }); |
| |
| group('server header', () { |
| test('defaults to "dart:io with Shelf"', () { |
| _scheduleServer(syncHandler); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, |
| containsPair(HttpHeaders.SERVER, 'dart:io with Shelf')); |
| }); |
| }); |
| |
| test('defers to header in response', () { |
| _scheduleServer((request) { |
| return new Response.ok('test', |
| headers: {HttpHeaders.SERVER: 'myServer'}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, containsPair(HttpHeaders.SERVER, 'myServer')); |
| }); |
| }); |
| }); |
| |
| group('chunked coding', () { |
| group('is added when the transfer-encoding header is', () { |
| test('unset', () { |
| _scheduleServer((request) { |
| return new Response.ok(new Stream.fromIterable([ |
| [1, 2, 3, 4] |
| ])); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, |
| containsPair(HttpHeaders.TRANSFER_ENCODING, 'chunked')); |
| expect(response.bodyBytes, equals([1, 2, 3, 4])); |
| }); |
| }); |
| |
| test('"identity"', () { |
| _scheduleServer((request) { |
| return new Response.ok( |
| new Stream.fromIterable([ |
| [1, 2, 3, 4] |
| ]), |
| headers: {HttpHeaders.TRANSFER_ENCODING: 'identity'}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, |
| containsPair(HttpHeaders.TRANSFER_ENCODING, 'chunked')); |
| expect(response.bodyBytes, equals([1, 2, 3, 4])); |
| }); |
| }); |
| }); |
| |
| test('is preserved when the transfer-encoding header is "chunked"', () { |
| _scheduleServer((request) { |
| return new Response.ok( |
| new Stream.fromIterable(["2\r\nhi\r\n0\r\n\r\n".codeUnits]), |
| headers: {HttpHeaders.TRANSFER_ENCODING: 'chunked'}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect(response.headers, |
| containsPair(HttpHeaders.TRANSFER_ENCODING, 'chunked')); |
| expect(response.body, equals("hi")); |
| }); |
| }); |
| |
| group('is not added when', () { |
| test('content-length is set', () { |
| _scheduleServer((request) { |
| return new Response.ok( |
| new Stream.fromIterable([ |
| [1, 2, 3, 4] |
| ]), |
| headers: {HttpHeaders.CONTENT_LENGTH: '4'}); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect( |
| response.headers, isNot(contains(HttpHeaders.TRANSFER_ENCODING))); |
| expect(response.bodyBytes, equals([1, 2, 3, 4])); |
| }); |
| }); |
| |
| test('status code is 1xx', () { |
| _scheduleServer((request) { |
| return new Response(123, body: new Stream.empty()); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect( |
| response.headers, isNot(contains(HttpHeaders.TRANSFER_ENCODING))); |
| expect(response.body, isEmpty); |
| }); |
| }); |
| |
| test('status code is 204', () { |
| _scheduleServer((request) { |
| return new Response(204, body: new Stream.empty()); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect( |
| response.headers, isNot(contains(HttpHeaders.TRANSFER_ENCODING))); |
| expect(response.body, isEmpty); |
| }); |
| }); |
| |
| test('status code is 304', () { |
| _scheduleServer((request) { |
| return new Response(304, body: new Stream.empty()); |
| }); |
| |
| return _scheduleGet().then((response) { |
| expect( |
| response.headers, isNot(contains(HttpHeaders.TRANSFER_ENCODING))); |
| expect(response.body, isEmpty); |
| }); |
| }); |
| }); |
| }); |
| |
| test('respects the "shelf.io.buffer_output" context parameter', () { |
| var controller = new StreamController<String>(); |
| _scheduleServer((request) { |
| controller.add("Hello, "); |
| |
| return new Response.ok(UTF8.encoder.bind(controller.stream), |
| context: {"shelf.io.buffer_output": false}); |
| }); |
| |
| schedule(() { |
| var request = |
| new http.Request("GET", Uri.parse('http://localhost:$_serverPort/')); |
| |
| return request.send().then((response) { |
| var stream = new ScheduledStream(UTF8.decoder.bind(response.stream)); |
| |
| return stream.next().then((data) { |
| expect(data, equals("Hello, ")); |
| controller.add("world!"); |
| return stream.next(); |
| }).then((data) { |
| expect(data, equals("world!")); |
| controller.close(); |
| expect(stream.hasNext, completion(isFalse)); |
| }); |
| }); |
| }); |
| }); |
| |
| group('ssl tests', () { |
| var securityContext = new SecurityContext() |
| ..setTrustedCertificatesBytes(certChainBytes) |
| ..useCertificateChainBytes(certChainBytes) |
| ..usePrivateKeyBytes(certKeyBytes, password: 'dartdart'); |
| |
| var sslClient = new HttpClient(context: securityContext); |
| |
| Future<HttpClientRequest> _scheduleSecureGet() { |
| return schedule/*<Future<HttpClientRequest>>*/(() { |
| return sslClient.getUrl(Uri.parse('https://localhost:$_serverPort/')); |
| }); |
| } |
| |
| test('secure sync handler returns a value to the client', () { |
| _scheduleServer(syncHandler, securityContext: securityContext); |
| |
| return _scheduleSecureGet().then((req) async { |
| var response = await req.close(); |
| expect(response.statusCode, HttpStatus.OK); |
| response.transform(UTF8.decoder).listen((contents) { |
| expect(contents, 'Hello from /'); |
| }); |
| }); |
| }); |
| |
| test('secure async handler returns a value to the client', () { |
| _scheduleServer(asyncHandler, securityContext: securityContext); |
| |
| return _scheduleSecureGet().then((req) async { |
| var response = await req.close(); |
| expect(response.statusCode, HttpStatus.OK); |
| response.transform(UTF8.decoder).listen((contents) { |
| expect(contents, 'Hello from /'); |
| }); |
| }); |
| }); |
| }); |
| } |
| |
| int _serverPort; |
| |
| Future _scheduleServer(Handler handler, {SecurityContext securityContext}) { |
| return schedule(() => shelf_io |
| .serve(handler, 'localhost', 0, securityContext: securityContext) |
| .then((server) { |
| currentSchedule.onComplete.schedule(() { |
| _serverPort = null; |
| return server.close(force: true); |
| }); |
| |
| _serverPort = server.port; |
| })); |
| } |
| |
| Future<http.Response> _scheduleGet({Map<String, String> headers}) { |
| if (headers == null) headers = {}; |
| |
| return schedule/*<Future<http.Response>>*/( |
| () => http.get('http://localhost:$_serverPort/', headers: headers)); |
| } |
| |
| Future<http.StreamedResponse> _schedulePost( |
| {Map<String, String> headers, String body}) { |
| return schedule/*<Future<http.StreamedResponse>>*/(() { |
| var request = |
| new http.Request('POST', Uri.parse('http://localhost:$_serverPort/')); |
| |
| if (headers != null) request.headers.addAll(headers); |
| if (body != null) request.body = body; |
| |
| return request.send(); |
| }); |
| } |