Skip to content

Adding client tests #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 23, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions test/client.dart
Original file line number Diff line number Diff line change
@@ -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;
390 changes: 390 additions & 0 deletions test/client_test.dart
Original file line number Diff line number Diff line change
@@ -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());
});
});
}
12 changes: 12 additions & 0 deletions test/hybrid/client_html.dart
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 9 additions & 0 deletions test/hybrid/client_io.dart
Original file line number Diff line number Diff line change
@@ -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';
144 changes: 144 additions & 0 deletions test/hybrid/server.dart
Original file line number Diff line number Diff line change
@@ -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 = <String>[
// 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());
}
46 changes: 7 additions & 39 deletions test/utils.dart
Original file line number Diff line number Diff line change
@@ -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,51 +66,19 @@ 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<http.ClientException>());
expect(error.message, message);
if (message != null) {
expect(error.message, message);
}
return true;
});

/// A matcher that matches function or future that throws a
/// [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));