| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 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. | |
| 4 | |
| 5 library curl_client_test; | |
| 6 | |
| 7 import 'dart:io'; | |
| 8 import 'dart:isolate'; | |
| 9 import 'dart:json' as json; | |
| 10 import 'dart:uri'; | |
| 11 | |
| 12 import '../../../pkg/unittest/lib/unittest.dart'; | |
| 13 import '../../../pkg/http/lib/http.dart' as http; | |
| 14 import '../../pub/curl_client.dart'; | |
| 15 import '../../pub/io.dart'; | |
| 16 import '../../pub/utils.dart'; | |
| 17 | |
| 18 // TODO(rnystrom): All of the code from here to the first "---..." line was | |
| 19 // copied from pkg/http/test/utils.dart and pkg/http/lib/src/utils.dart. It's | |
| 20 // copied here because http/test/utils.dart is now using "package:" imports and | |
| 21 // this is not. You cannot mix those because you end up with duplicate copies of | |
| 22 // the same library in memory. Since curl_client is going away soon anyway, I'm | |
| 23 // just copying the code here. Delete all of this when curl client is removed. | |
| 24 | |
| 25 /// Returns the [Encoding] that corresponds to [charset]. Throws a | |
| 26 /// [FormatException] if no [Encoding] was found that corresponds to [charset]. | |
| 27 /// [charset] may not be null. | |
| 28 Encoding requiredEncodingForCharset(String charset) { | |
| 29 var encoding = _encodingForCharset(charset); | |
| 30 if (encoding != null) return encoding; | |
| 31 throw new FormatException('Unsupported encoding "$charset".'); | |
| 32 } | |
| 33 | |
| 34 /// Returns the [Encoding] that corresponds to [charset]. Returns null if no | |
| 35 /// [Encoding] was found that corresponds to [charset]. [charset] may not be | |
| 36 /// null. | |
| 37 Encoding _encodingForCharset(String charset) { | |
| 38 charset = charset.toLowerCase(); | |
| 39 if (charset == 'ascii' || charset == 'us-ascii') return Encoding.ASCII; | |
| 40 if (charset == 'utf-8') return Encoding.UTF_8; | |
| 41 if (charset == 'iso-8859-1') return Encoding.ISO_8859_1; | |
| 42 return null; | |
| 43 } | |
| 44 | |
| 45 /// Converts [bytes] into a [String] according to [encoding]. | |
| 46 String decodeString(List<int> bytes, Encoding encoding) { | |
| 47 // TODO(nweiz): implement this once issue 6284 is fixed. | |
| 48 return new String.fromCharCodes(bytes); | |
| 49 } | |
| 50 | |
| 51 /// The current server instance. | |
| 52 HttpServer _server; | |
| 53 | |
| 54 /// The URL for the current server instance. | |
| 55 Uri get serverUrl => Uri.parse('http://localhost:${_server.port}'); | |
| 56 | |
| 57 /// A dummy URL for constructing requests that won't be sent. | |
| 58 Uri get dummyUrl => Uri.parse('http://dartlang.org/'); | |
| 59 | |
| 60 /// Starts a new HTTP server. | |
| 61 void startServer() { | |
| 62 _server = new HttpServer(); | |
| 63 | |
| 64 _server.addRequestHandler((request) => request.path == '/error', | |
| 65 (request, response) { | |
| 66 response.statusCode = 400; | |
| 67 response.contentLength = 0; | |
| 68 response.outputStream.close(); | |
| 69 }); | |
| 70 | |
| 71 _server.addRequestHandler((request) => request.path == '/loop', | |
| 72 (request, response) { | |
| 73 var n = int.parse(Uri.parse(request.uri).query); | |
| 74 response.statusCode = 302; | |
| 75 response.headers.set('location', | |
| 76 serverUrl.resolve('/loop?${n + 1}').toString()); | |
| 77 response.contentLength = 0; | |
| 78 response.outputStream.close(); | |
| 79 }); | |
| 80 | |
| 81 _server.addRequestHandler((request) => request.path == '/redirect', | |
| 82 (request, response) { | |
| 83 response.statusCode = 302; | |
| 84 response.headers.set('location', serverUrl.resolve('/').toString()); | |
| 85 response.contentLength = 0; | |
| 86 response.outputStream.close(); | |
| 87 }); | |
| 88 | |
| 89 _server.defaultRequestHandler = (request, response) { | |
| 90 consumeInputStream(request.inputStream).then((requestBodyBytes) { | |
| 91 response.statusCode = 200; | |
| 92 response.headers.contentType = new ContentType("application", "json"); | |
| 93 | |
| 94 var requestBody; | |
| 95 if (requestBodyBytes.isEmpty) { | |
| 96 requestBody = null; | |
| 97 } else if (request.headers.contentType.charset != null) { | |
| 98 var encoding = requiredEncodingForCharset( | |
| 99 request.headers.contentType.charset); | |
| 100 requestBody = decodeString(requestBodyBytes, encoding); | |
| 101 } else { | |
| 102 requestBody = requestBodyBytes; | |
| 103 } | |
| 104 | |
| 105 var content = { | |
| 106 'method': request.method, | |
| 107 'path': request.path, | |
| 108 'headers': {} | |
| 109 }; | |
| 110 if (requestBody != null) content['body'] = requestBody; | |
| 111 request.headers.forEach((name, values) { | |
| 112 // These headers are automatically generated by dart:io, so we don't | |
| 113 // want to test them here. | |
| 114 if (name == 'cookie' || name == 'host') return; | |
| 115 | |
| 116 content['headers'][name] = values; | |
| 117 }); | |
| 118 | |
| 119 var outputEncoding; | |
| 120 var encodingName = request.queryParameters['response-encoding']; | |
| 121 if (encodingName != null) { | |
| 122 outputEncoding = requiredEncodingForCharset(encodingName); | |
| 123 } else { | |
| 124 outputEncoding = Encoding.ASCII; | |
| 125 } | |
| 126 | |
| 127 var body = json.stringify(content); | |
| 128 response.contentLength = body.length; | |
| 129 response.outputStream.writeString(body, outputEncoding); | |
| 130 response.outputStream.close(); | |
| 131 }); | |
| 132 }; | |
| 133 | |
| 134 _server.listen("127.0.0.1", 0); | |
| 135 } | |
| 136 | |
| 137 /// Stops the current HTTP server. | |
| 138 void stopServer() { | |
| 139 _server.close(); | |
| 140 _server = null; | |
| 141 } | |
| 142 | |
| 143 /// A matcher that matches JSON that parses to a value that matches the inner | |
| 144 /// matcher. | |
| 145 Matcher parse(matcher) => new _Parse(matcher); | |
| 146 | |
| 147 class _Parse extends BaseMatcher { | |
| 148 final Matcher _matcher; | |
| 149 | |
| 150 _Parse(this._matcher); | |
| 151 | |
| 152 bool matches(item, MatchState matchState) { | |
| 153 if (item is! String) return false; | |
| 154 | |
| 155 var parsed; | |
| 156 try { | |
| 157 parsed = json.parse(item); | |
| 158 } catch (e) { | |
| 159 return false; | |
| 160 } | |
| 161 | |
| 162 return _matcher.matches(parsed, matchState); | |
| 163 } | |
| 164 | |
| 165 Description describe(Description description) { | |
| 166 return description.add('parses to a value that ') | |
| 167 .addDescriptionOf(_matcher); | |
| 168 } | |
| 169 } | |
| 170 | |
| 171 /// A matcher for HttpExceptions. | |
| 172 const isHttpException = const _HttpException(); | |
| 173 | |
| 174 /// A matcher for functions that throw HttpException. | |
| 175 const Matcher throwsHttpException = | |
| 176 const Throws(isHttpException); | |
| 177 | |
| 178 class _HttpException extends TypeMatcher { | |
| 179 const _HttpException() : super("HttpException"); | |
| 180 bool matches(item, MatchState matchState) => item is HttpException; | |
| 181 } | |
| 182 | |
| 183 /// A matcher for RedirectLimitExceededExceptions. | |
| 184 const isRedirectLimitExceededException = | |
| 185 const _RedirectLimitExceededException(); | |
| 186 | |
| 187 /// A matcher for functions that throw RedirectLimitExceededException. | |
| 188 const Matcher throwsRedirectLimitExceededException = | |
| 189 const Throws(isRedirectLimitExceededException); | |
| 190 | |
| 191 class _RedirectLimitExceededException extends TypeMatcher { | |
| 192 const _RedirectLimitExceededException() : | |
| 193 super("RedirectLimitExceededException"); | |
| 194 | |
| 195 bool matches(item, MatchState matchState) => | |
| 196 item is RedirectLimitExceededException; | |
| 197 } | |
| 198 | |
| 199 // ---------------------------------------------------------------------------- | |
| 200 | |
| 201 void main() { | |
| 202 setUp(startServer); | |
| 203 tearDown(stopServer); | |
| 204 | |
| 205 test('head', () { | |
| 206 expect(new CurlClient().head(serverUrl).then((response) { | |
| 207 expect(response.statusCode, equals(200)); | |
| 208 expect(response.body, equals('')); | |
| 209 }), completes); | |
| 210 }); | |
| 211 | |
| 212 test('get', () { | |
| 213 expect(new CurlClient().get(serverUrl, headers: { | |
| 214 'X-Random-Header': 'Value', | |
| 215 'X-Other-Header': 'Other Value' | |
| 216 }).then((response) { | |
| 217 expect(response.statusCode, equals(200)); | |
| 218 expect(response.body, parse(equals({ | |
| 219 'method': 'GET', | |
| 220 'path': '/', | |
| 221 'headers': { | |
| 222 'x-random-header': ['Value'], | |
| 223 'x-other-header': ['Other Value'] | |
| 224 }, | |
| 225 }))); | |
| 226 }), completes); | |
| 227 }); | |
| 228 | |
| 229 test('post', () { | |
| 230 expect(new CurlClient().post(serverUrl, headers: { | |
| 231 'X-Random-Header': 'Value', | |
| 232 'X-Other-Header': 'Other Value' | |
| 233 }, fields: { | |
| 234 'some-field': 'value', | |
| 235 'other-field': 'other value' | |
| 236 }).then((response) { | |
| 237 expect(response.statusCode, equals(200)); | |
| 238 expect(response.body, parse(equals({ | |
| 239 'method': 'POST', | |
| 240 'path': '/', | |
| 241 'headers': { | |
| 242 'content-type': [ | |
| 243 'application/x-www-form-urlencoded; charset=UTF-8' | |
| 244 ], | |
| 245 'content-length': ['40'], | |
| 246 'x-random-header': ['Value'], | |
| 247 'x-other-header': ['Other Value'] | |
| 248 }, | |
| 249 'body': 'some-field=value&other-field=other+value' | |
| 250 }))); | |
| 251 }), completes); | |
| 252 }); | |
| 253 | |
| 254 test('post without fields', () { | |
| 255 expect(new CurlClient().post(serverUrl, headers: { | |
| 256 'X-Random-Header': 'Value', | |
| 257 'X-Other-Header': 'Other Value', | |
| 258 'Content-Type': 'text/plain' | |
| 259 }).then((response) { | |
| 260 expect(response.statusCode, equals(200)); | |
| 261 expect(response.body, parse(equals({ | |
| 262 'method': 'POST', | |
| 263 'path': '/', | |
| 264 'headers': { | |
| 265 'content-type': ['text/plain'], | |
| 266 'x-random-header': ['Value'], | |
| 267 'x-other-header': ['Other Value'] | |
| 268 } | |
| 269 }))); | |
| 270 }), completes); | |
| 271 }); | |
| 272 | |
| 273 test('put', () { | |
| 274 expect(new CurlClient().put(serverUrl, headers: { | |
| 275 'X-Random-Header': 'Value', | |
| 276 'X-Other-Header': 'Other Value' | |
| 277 }, fields: { | |
| 278 'some-field': 'value', | |
| 279 'other-field': 'other value' | |
| 280 }).then((response) { | |
| 281 expect(response.statusCode, equals(200)); | |
| 282 expect(response.body, parse(equals({ | |
| 283 'method': 'PUT', | |
| 284 'path': '/', | |
| 285 'headers': { | |
| 286 'content-type': [ | |
| 287 'application/x-www-form-urlencoded; charset=UTF-8' | |
| 288 ], | |
| 289 'content-length': ['40'], | |
| 290 'x-random-header': ['Value'], | |
| 291 'x-other-header': ['Other Value'] | |
| 292 }, | |
| 293 'body': 'some-field=value&other-field=other+value' | |
| 294 }))); | |
| 295 }), completes); | |
| 296 }); | |
| 297 | |
| 298 test('put without fields', () { | |
| 299 expect(new CurlClient().put(serverUrl, headers: { | |
| 300 'X-Random-Header': 'Value', | |
| 301 'X-Other-Header': 'Other Value', | |
| 302 'Content-Type': 'text/plain' | |
| 303 }).then((response) { | |
| 304 expect(response.statusCode, equals(200)); | |
| 305 expect(response.body, parse(equals({ | |
| 306 'method': 'PUT', | |
| 307 'path': '/', | |
| 308 'headers': { | |
| 309 'content-type': ['text/plain'], | |
| 310 'x-random-header': ['Value'], | |
| 311 'x-other-header': ['Other Value'] | |
| 312 } | |
| 313 }))); | |
| 314 }), completes); | |
| 315 }); | |
| 316 | |
| 317 test('delete', () { | |
| 318 expect(new CurlClient().delete(serverUrl, headers: { | |
| 319 'X-Random-Header': 'Value', | |
| 320 'X-Other-Header': 'Other Value' | |
| 321 }).then((response) { | |
| 322 expect(response.statusCode, equals(200)); | |
| 323 expect(response.body, parse(equals({ | |
| 324 'method': 'DELETE', | |
| 325 'path': '/', | |
| 326 'headers': { | |
| 327 'x-random-header': ['Value'], | |
| 328 'x-other-header': ['Other Value'] | |
| 329 } | |
| 330 }))); | |
| 331 }), completes); | |
| 332 }); | |
| 333 | |
| 334 test('read', () { | |
| 335 expect(new CurlClient().read(serverUrl, headers: { | |
| 336 'X-Random-Header': 'Value', | |
| 337 'X-Other-Header': 'Other Value' | |
| 338 }), completion(parse(equals({ | |
| 339 'method': 'GET', | |
| 340 'path': '/', | |
| 341 'headers': { | |
| 342 'x-random-header': ['Value'], | |
| 343 'x-other-header': ['Other Value'] | |
| 344 }, | |
| 345 })))); | |
| 346 }); | |
| 347 | |
| 348 test('read throws an error for a 4** status code', () { | |
| 349 expect(new CurlClient().read(serverUrl.resolve('/error')), | |
| 350 throwsHttpException); | |
| 351 }); | |
| 352 | |
| 353 test('readBytes', () { | |
| 354 var future = new CurlClient().readBytes(serverUrl, headers: { | |
| 355 'X-Random-Header': 'Value', | |
| 356 'X-Other-Header': 'Other Value' | |
| 357 }).then((bytes) => new String.fromCharCodes(bytes)); | |
| 358 | |
| 359 expect(future, completion(parse(equals({ | |
| 360 'method': 'GET', | |
| 361 'path': '/', | |
| 362 'headers': { | |
| 363 'x-random-header': ['Value'], | |
| 364 'x-other-header': ['Other Value'] | |
| 365 }, | |
| 366 })))); | |
| 367 }); | |
| 368 | |
| 369 test('readBytes throws an error for a 4** status code', () { | |
| 370 expect(new CurlClient().readBytes(serverUrl.resolve('/error')), | |
| 371 throwsHttpException); | |
| 372 }); | |
| 373 | |
| 374 test('#send a StreamedRequest', () { | |
| 375 var client = new CurlClient(); | |
| 376 var request = new http.StreamedRequest("POST", serverUrl); | |
| 377 request.headers[HttpHeaders.CONTENT_TYPE] = | |
| 378 'application/json; charset=utf-8'; | |
| 379 | |
| 380 var future = client.send(request).then((response) { | |
| 381 expect(response.statusCode, equals(200)); | |
| 382 return response.stream.bytesToString(); | |
| 383 }).whenComplete(client.close); | |
| 384 | |
| 385 expect(future, completion(parse(equals({ | |
| 386 'method': 'POST', | |
| 387 'path': '/', | |
| 388 'headers': { | |
| 389 'content-type': ['application/json; charset=utf-8'], | |
| 390 'transfer-encoding': ['chunked'] | |
| 391 }, | |
| 392 'body': '{"hello": "world"}' | |
| 393 })))); | |
| 394 | |
| 395 request.sink.add('{"hello": "world"}'.charCodes); | |
| 396 request.sink.close(); | |
| 397 }); | |
| 398 | |
| 399 test('with one redirect', () { | |
| 400 var url = serverUrl.resolve('/redirect'); | |
| 401 expect(new CurlClient().get(url).then((response) { | |
| 402 expect(response.statusCode, equals(200)); | |
| 403 expect(response.body, parse(equals({ | |
| 404 'method': 'GET', | |
| 405 'path': '/', | |
| 406 'headers': {} | |
| 407 }))); | |
| 408 }), completes); | |
| 409 }); | |
| 410 | |
| 411 test('with too many redirects', () { | |
| 412 expect(new CurlClient().get(serverUrl.resolve('/loop?1')), | |
| 413 throwsRedirectLimitExceededException); | |
| 414 }); | |
| 415 | |
| 416 test('with a generic failure', () { | |
| 417 expect(new CurlClient().get('url fail'), | |
| 418 throwsHttpException); | |
| 419 }); | |
| 420 | |
| 421 test('with one redirect via HEAD', () { | |
| 422 var url = serverUrl.resolve('/redirect'); | |
| 423 expect(new CurlClient().head(url).then((response) { | |
| 424 expect(response.statusCode, equals(200)); | |
| 425 }), completes); | |
| 426 }); | |
| 427 | |
| 428 test('with too many redirects via HEAD', () { | |
| 429 expect(new CurlClient().head(serverUrl.resolve('/loop?1')), | |
| 430 throwsRedirectLimitExceededException); | |
| 431 }); | |
| 432 | |
| 433 test('with a generic failure via HEAD', () { | |
| 434 expect(new CurlClient().head('url fail'), | |
| 435 throwsHttpException); | |
| 436 }); | |
| 437 | |
| 438 test('without following redirects', () { | |
| 439 var request = new http.Request('GET', serverUrl.resolve('/redirect')); | |
| 440 request.followRedirects = false; | |
| 441 expect(new CurlClient().send(request).then(http.Response.fromStream) | |
| 442 .then((response) { | |
| 443 expect(response.statusCode, equals(302)); | |
| 444 expect(response.isRedirect, true); | |
| 445 }), completes); | |
| 446 }); | |
| 447 } | |
| OLD | NEW |