| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library test_utils; | 5 library test_utils; |
| 6 | 6 |
| 7 import 'dart:async'; | |
| 8 import 'dart:convert'; | 7 import 'dart:convert'; |
| 9 import 'dart:io'; | |
| 10 | 8 |
| 11 import 'package:http/http.dart'; | 9 import 'package:http/http.dart' as http; |
| 12 import 'package:http/src/utils.dart'; | 10 import 'package:http_parser/http_parser.dart'; |
| 13 import 'package:unittest/unittest.dart'; | 11 import 'package:unittest/unittest.dart'; |
| 14 | 12 |
| 15 /// The current server instance. | |
| 16 HttpServer _server; | |
| 17 | |
| 18 /// The URL for the current server instance. | |
| 19 Uri get serverUrl => Uri.parse('http://localhost:${_server.port}'); | |
| 20 | |
| 21 /// A dummy URL for constructing requests that won't be sent. | 13 /// A dummy URL for constructing requests that won't be sent. |
| 22 Uri get dummyUrl => Uri.parse('http://dartlang.org/'); | 14 Uri get dummyUrl => Uri.parse('http://dartlang.org/'); |
| 23 | 15 |
| 24 /// Starts a new HTTP server. | |
| 25 Future startServer() { | |
| 26 return HttpServer.bind("localhost", 0).then((s) { | |
| 27 _server = s; | |
| 28 s.listen((request) { | |
| 29 var path = request.uri.path; | |
| 30 var response = request.response; | |
| 31 | |
| 32 if (path == '/error') { | |
| 33 response.statusCode = 400; | |
| 34 response.contentLength = 0; | |
| 35 response.close(); | |
| 36 return; | |
| 37 } | |
| 38 | |
| 39 if (path == '/loop') { | |
| 40 var n = int.parse(request.uri.query); | |
| 41 response.statusCode = 302; | |
| 42 response.headers.set('location', | |
| 43 serverUrl.resolve('/loop?${n + 1}').toString()); | |
| 44 response.contentLength = 0; | |
| 45 response.close(); | |
| 46 return; | |
| 47 } | |
| 48 | |
| 49 if (path == '/redirect') { | |
| 50 response.statusCode = 302; | |
| 51 response.headers.set('location', serverUrl.resolve('/').toString()); | |
| 52 response.contentLength = 0; | |
| 53 response.close(); | |
| 54 return; | |
| 55 } | |
| 56 | |
| 57 if (path == '/no-content-length') { | |
| 58 response.statusCode = 200; | |
| 59 response.contentLength = -1; | |
| 60 response.write('body'); | |
| 61 response.close(); | |
| 62 return; | |
| 63 } | |
| 64 | |
| 65 new ByteStream(request).toBytes().then((requestBodyBytes) { | |
| 66 var outputEncoding; | |
| 67 var encodingName = request.uri.queryParameters['response-encoding']; | |
| 68 if (encodingName != null) { | |
| 69 outputEncoding = requiredEncodingForCharset(encodingName); | |
| 70 } else { | |
| 71 outputEncoding = ASCII; | |
| 72 } | |
| 73 | |
| 74 response.headers.contentType = | |
| 75 new ContentType( | |
| 76 "application", "json", charset: outputEncoding.name); | |
| 77 response.headers.set('single', 'value'); | |
| 78 | |
| 79 var requestBody; | |
| 80 if (requestBodyBytes.isEmpty) { | |
| 81 requestBody = null; | |
| 82 } else if (request.headers.contentType != null && | |
| 83 request.headers.contentType.charset != null) { | |
| 84 var encoding = requiredEncodingForCharset( | |
| 85 request.headers.contentType.charset); | |
| 86 requestBody = encoding.decode(requestBodyBytes); | |
| 87 } else { | |
| 88 requestBody = requestBodyBytes; | |
| 89 } | |
| 90 | |
| 91 var content = { | |
| 92 'method': request.method, | |
| 93 'path': request.uri.path, | |
| 94 'headers': {} | |
| 95 }; | |
| 96 if (requestBody != null) content['body'] = requestBody; | |
| 97 request.headers.forEach((name, values) { | |
| 98 // These headers are automatically generated by dart:io, so we don't | |
| 99 // want to test them here. | |
| 100 if (name == 'cookie' || name == 'host') return; | |
| 101 | |
| 102 content['headers'][name] = values; | |
| 103 }); | |
| 104 | |
| 105 var body = JSON.encode(content); | |
| 106 response.contentLength = body.length; | |
| 107 response.write(body); | |
| 108 response.close(); | |
| 109 }); | |
| 110 }); | |
| 111 }); | |
| 112 } | |
| 113 | |
| 114 /// Stops the current HTTP server. | |
| 115 void stopServer() { | |
| 116 if (_server != null) { | |
| 117 _server.close(); | |
| 118 _server = null; | |
| 119 } | |
| 120 } | |
| 121 | |
| 122 /// Removes eight spaces of leading indentation from a multiline string. | 16 /// Removes eight spaces of leading indentation from a multiline string. |
| 123 /// | 17 /// |
| 124 /// Note that this is very sensitive to how the literals are styled. They should | 18 /// Note that this is very sensitive to how the literals are styled. They should |
| 125 /// be: | 19 /// be: |
| 126 /// ''' | 20 /// ''' |
| 127 /// Text starts on own line. Lines up with subsequent lines. | 21 /// Text starts on own line. Lines up with subsequent lines. |
| 128 /// Lines are indented exactly 8 characters from the left margin. | 22 /// Lines are indented exactly 8 characters from the left margin. |
| 129 /// Close is on the same line.''' | 23 /// Close is on the same line.''' |
| 130 /// | 24 /// |
| 131 /// This does nothing if text is only a single line. | 25 /// This does nothing if text is only a single line. |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 167 | 61 |
| 168 return _matcher.matches(parsed, matchState); | 62 return _matcher.matches(parsed, matchState); |
| 169 } | 63 } |
| 170 | 64 |
| 171 Description describe(Description description) { | 65 Description describe(Description description) { |
| 172 return description.add('parses to a value that ') | 66 return description.add('parses to a value that ') |
| 173 .addDescriptionOf(_matcher); | 67 .addDescriptionOf(_matcher); |
| 174 } | 68 } |
| 175 } | 69 } |
| 176 | 70 |
| 177 /// A matcher for functions that throw HttpException. | 71 /// A matcher that validates the body of a multipart request after finalization. |
| 178 Matcher get throwsClientException => | 72 /// The string "{{boundary}}" in [pattern] will be replaced by the boundary |
| 179 throwsA(new isInstanceOf<ClientException>()); | 73 /// string for the request, and LF newlines will be replaced with CRLF. |
| 74 /// Indentation will be normalized. |
| 75 Matcher bodyMatches(String pattern) => new _BodyMatches(pattern); |
| 180 | 76 |
| 181 /// A matcher for RedirectLimitExceededExceptions. | 77 class _BodyMatches extends Matcher { |
| 182 const isRedirectLimitExceededException = | 78 final String _pattern; |
| 183 const _RedirectLimitExceededException(); | |
| 184 | 79 |
| 185 /// A matcher for functions that throw RedirectLimitExceededException. | 80 _BodyMatches(this._pattern); |
| 186 const Matcher throwsRedirectLimitExceededException = | |
| 187 const Throws(isRedirectLimitExceededException); | |
| 188 | 81 |
| 189 class _RedirectLimitExceededException extends TypeMatcher { | 82 bool matches(item, Map matchState) { |
| 190 const _RedirectLimitExceededException() : | 83 if (item is! http.MultipartRequest) return false; |
| 191 super("RedirectLimitExceededException"); | |
| 192 | 84 |
| 193 bool matches(item, Map matchState) => | 85 var future = item.finalize().toBytes().then((bodyBytes) { |
| 194 item is RedirectException && item.message == "Redirect limit exceeded"; | 86 var body = UTF8.decode(bodyBytes); |
| 87 var contentType = new MediaType.parse(item.headers['content-type']); |
| 88 var boundary = contentType.parameters['boundary']; |
| 89 var expected = cleanUpLiteral(_pattern) |
| 90 .replaceAll("\n", "\r\n") |
| 91 .replaceAll("{{boundary}}", boundary); |
| 92 |
| 93 expect(body, equals(expected)); |
| 94 expect(item.contentLength, equals(bodyBytes.length)); |
| 95 }); |
| 96 |
| 97 return completes.matches(future, matchState); |
| 98 } |
| 99 |
| 100 Description describe(Description description) { |
| 101 return description.add('has a body that matches "$_pattern"'); |
| 102 } |
| 195 } | 103 } |
| 196 | 104 |
| 197 /// A matcher for SocketExceptions. | 105 /// A matcher that matches a [http.ClientException] with the given [message]. |
| 198 const isSocketException = const _SocketException(); | 106 /// |
| 107 /// [message] can be a String or a [Matcher]. |
| 108 Matcher isClientException(message) => predicate((error) { |
| 109 expect(error, new isInstanceOf<http.ClientException>()); |
| 110 expect(error.message, message); |
| 111 return true; |
| 112 }); |
| 199 | 113 |
| 200 /// A matcher for functions that throw SocketException. | 114 /// A matcher that matches function or future that throws a |
| 201 const Matcher throwsSocketException = | 115 /// [http.ClientException] with the given [message]. |
| 202 const Throws(isSocketException); | 116 /// |
| 203 | 117 /// [message] can be a String or a [Matcher]. |
| 204 class _SocketException extends TypeMatcher { | 118 Matcher throwsClientException(message) => throwsA(isClientException(message)); |
| 205 const _SocketException() : super("SocketException"); | |
| 206 bool matches(item, Map matchState) => item is SocketException; | |
| 207 } | |
| OLD | NEW |