diff --git a/test/client.dart b/test/client.dart new file mode 100644 index 0000000000..0fba590a95 --- /dev/null +++ b/test/client.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2017, 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. + +import 'package:http/http.dart'; + +Client platformClient() => null; + +String userAgent() => null; diff --git a/test/client_test.dart b/test/client_test.dart new file mode 100644 index 0000000000..a8e466d0e6 --- /dev/null +++ b/test/client_test.dart @@ -0,0 +1,390 @@ +// 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. + +import 'package:test/test.dart'; + +import 'package:http/http.dart'; + +import 'client.dart' + if (dart.library.io) 'hybrid/client_io.dart' + if (dart.library.html) 'hybrid/client_html.dart'; +import 'utils.dart'; + +void main() { + group('client', () { + // The server url of the spawned server + var serverUrl; + + setUp(() async { + var channel = spawnHybridUri('hybrid/server.dart'); + serverUrl = Uri.parse(await channel.stream.first); + }); + + test('head', () async { + var response = await platformClient().head(serverUrl); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect(body, equals('')); + }); + + test('get', () async { + var response = await platformClient().get(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('post with string', () async { + var response = await platformClient().post( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('post with bytes', () async { + var response = await platformClient().post( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('post with fields', () async { + var response = await platformClient().post( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('put with string', () async { + var response = await platformClient().put( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('put with bytes', () async { + var response = await platformClient().put( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('put with fields', () async { + var response = await platformClient().put( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('patch with string', () async { + var response = await platformClient().patch( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('patch with bytes', () async { + var response = await platformClient().patch( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('patch with fields', () async { + var response = await platformClient().patch( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('delete', () async { + var response = await platformClient().delete(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'DELETE', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('read', () async { + var body = await platformClient().read(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + + expect( + body, + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('read throws an error for a 4** status code', () async { + expect(() => platformClient().read(serverUrl.resolve('/error')), + throwsClientException()); + }); + + test('readBytes', () async { + var body = await platformClient().readBytes(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + + expect( + new String.fromCharCodes(body), + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('readBytes throws an error for a 4** status code', () async { + expect(() => platformClient().readBytes(serverUrl.resolve('/error')), + throwsClientException()); + }); + }); +} diff --git a/test/hybrid/client_html.dart b/test/hybrid/client_html.dart new file mode 100644 index 0000000000..1c1abd9310 --- /dev/null +++ b/test/hybrid/client_html.dart @@ -0,0 +1,12 @@ +// Copyright (c) 2017, 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. + +import 'dart:html' as html; + +import 'package:http/http.dart'; +import 'package:http/browser_client.dart'; + +Client platformClient() => new BrowserClient(); + +String userAgent() => html.window.navigator.userAgent; diff --git a/test/hybrid/client_io.dart b/test/hybrid/client_io.dart new file mode 100644 index 0000000000..fe41139b44 --- /dev/null +++ b/test/hybrid/client_io.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2017, 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. + +import 'package:http/http.dart'; + +Client platformClient() => new Client(); + +String userAgent() => 'Dart'; diff --git a/test/hybrid/server.dart b/test/hybrid/server.dart new file mode 100644 index 0000000000..3b54a8193a --- /dev/null +++ b/test/hybrid/server.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2017, 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. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:http/src/utils.dart'; +import "package:stream_channel/stream_channel.dart"; + +/// The list of headers to ignore when sending back confirmation. +final _ignoreHeaders = [ + // Browser headers (Chrome) + 'accept', + 'accept-language', + 'accept-encoding', + 'connection', + 'origin', + 'referer', + + // Dart IO headers + 'cookie', + 'host', +]; + +/// Creates a server used to test a `http` client. +/// +/// On startup the server will bind to `localhost`. Then it will send the url +/// as a string back through the [channel]. +/// +/// The server has the following explicit endpoints used to test individual +/// functionality. +/// * /error - Will return a 400 status code. +/// * /loop - Which is used to check for max redirects. +/// * /redirect - Which is used to test that a redirect works. +/// * /no-content-length - Which returns a body with no content. +/// +/// All other requests will be responded to. This is used to test the +/// individual HTTP methods. The server will return back the following +/// information in a string. +/// +/// { +/// method: 'METHOD_NAME', +/// path: 'ENDPOINT_PATH', +/// headers: { +/// KEY VALUE STORE OF INDIVIDUAL HEADERS +/// }, +/// body: OPTIONAL +/// } +hybridMain(StreamChannel channel) async { + var server = await HttpServer.bind('localhost', 0); + var serverUrl = Uri.parse('http://localhost:${server.port}'); + + server.listen((request) { + var path = request.uri.path; + var response = request.response; + + if (path == '/error') { + response.statusCode = 400; + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/loop') { + var n = int.parse(request.uri.query); + response.statusCode = 302; + response.headers + .set('location', serverUrl.resolve('/loop?${n + 1}').toString()); + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/redirect') { + response.statusCode = 302; + response.headers.set('location', serverUrl.resolve('/').toString()); + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/no-content-length') { + response.statusCode = 200; + response.contentLength = -1; + response.write('body'); + response.close(); + return; + } + + collectBytes(request).then((requestBodyBytes) { + var outputEncoding; + var encodingName = request.uri.queryParameters['response-encoding']; + if (encodingName != null) { + outputEncoding = requiredEncodingForCharset(encodingName); + } else { + outputEncoding = ASCII; + } + + response.headers.contentType = + new ContentType("application", "json", charset: outputEncoding.name); + + // Add CORS headers for browser testing + response.headers.set('access-control-allow-origin', '*'); + response.headers.set( + 'access-control-allow-headers', 'X-Random-Header,X-Other-Header'); + response.headers.set('access-control-allow-methods', + 'GET, PUT, POST, DELETE, PATCH, HEAD'); + + var requestBody; + if (requestBodyBytes.isEmpty) { + requestBody = null; + } else if (request.headers.contentType != null && + request.headers.contentType.charset != null) { + var encoding = + requiredEncodingForCharset(request.headers.contentType.charset); + requestBody = encoding.decode(requestBodyBytes); + } else { + requestBody = requestBodyBytes; + } + + var content = { + 'method': request.method, + 'path': request.uri.path, + 'headers': {} + }; + if (requestBody != null) content['body'] = requestBody; + request.headers.forEach((name, values) { + // Ignore headers that are generated by the client + if (_ignoreHeaders.contains(name)) return; + + (content['headers'] as Map)[name] = values; + }); + + var body = JSON.encode(content); + response.contentLength = body.length; + response.write(body); + response.close(); + }); + }); + + channel.sink.add(serverUrl.toString()); +} diff --git a/test/utils.dart b/test/utils.dart index 1bc78f7405..505ee5fda3 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -4,9 +4,9 @@ import 'dart:convert'; +import 'package:test/test.dart'; + import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; -import 'package:unittest/unittest.dart'; /// A dummy URL for constructing requests that won't be sent. Uri get dummyUrl => Uri.parse('http://dartlang.org/'); @@ -66,46 +66,14 @@ class _Parse extends Matcher { } } -/// A matcher that validates the body of a multipart request after finalization. -/// The string "{{boundary}}" in [pattern] will be replaced by the boundary -/// string for the request, and LF newlines will be replaced with CRLF. -/// Indentation will be normalized. -Matcher bodyMatches(String pattern) => new _BodyMatches(pattern); - -class _BodyMatches extends Matcher { - final String _pattern; - - _BodyMatches(this._pattern); - - bool matches(item, Map matchState) { - if (item is! http.MultipartRequest) return false; - - var future = item.finalize().toBytes().then((bodyBytes) { - var body = UTF8.decode(bodyBytes); - var contentType = new MediaType.parse(item.headers['content-type']); - var boundary = contentType.parameters['boundary']; - var expected = cleanUpLiteral(_pattern) - .replaceAll("\n", "\r\n") - .replaceAll("{{boundary}}", boundary); - - expect(body, equals(expected)); - expect(item.contentLength, equals(bodyBytes.length)); - }); - - return completes.matches(future, matchState); - } - - Description describe(Description description) { - return description.add('has a body that matches "$_pattern"'); - } -} - /// A matcher that matches a [http.ClientException] with the given [message]. /// /// [message] can be a String or a [Matcher]. -Matcher isClientException(message) => predicate((error) { +Matcher isClientException([message]) => predicate((error) { expect(error, new isInstanceOf()); - expect(error.message, message); + if (message != null) { + expect(error.message, message); + } return true; }); @@ -113,4 +81,4 @@ Matcher isClientException(message) => predicate((error) { /// [http.ClientException] with the given [message]. /// /// [message] can be a String or a [Matcher]. -Matcher throwsClientException(message) => throwsA(isClientException(message)); +Matcher throwsClientException([message]) => throwsA(isClientException(message));