OLD | NEW |
(Empty) | |
| 1 library googleapis.common_internal_test; |
| 2 import 'dart:async'; |
| 3 import 'dart:convert'; |
| 4 |
| 5 import 'package:crypto/crypto.dart' as crypto; |
| 6 import 'package:googleapis/common/common.dart'; |
| 7 import 'package:googleapis/src/common_internal.dart'; |
| 8 import 'package:http/http.dart' as http; |
| 9 import 'package:unittest/unittest.dart'; |
| 10 class HttpServerMock extends http.BaseClient { |
| 11 Function _callback; |
| 12 bool _expectJson; |
| 13 |
| 14 void register(Function callback, bool expectJson) { |
| 15 _callback = callback; |
| 16 _expectJson = expectJson; |
| 17 } |
| 18 |
| 19 Future<http.StreamedResponse> send(http.BaseRequest request) { |
| 20 if (_expectJson) { |
| 21 return request.finalize() |
| 22 .transform(UTF8.decoder) |
| 23 .join('') |
| 24 .then((String jsonString) { |
| 25 if (jsonString.isEmpty) { |
| 26 return _callback(request, null); |
| 27 } else { |
| 28 return _callback(request, JSON.decode(jsonString)); |
| 29 } |
| 30 }); |
| 31 } else { |
| 32 var stream = request.finalize(); |
| 33 if (stream == null) { |
| 34 return _callback(request, []); |
| 35 } else { |
| 36 return stream.toBytes().then((data) { |
| 37 return _callback(request, data); |
| 38 }); |
| 39 } |
| 40 } |
| 41 } |
| 42 } |
| 43 |
| 44 http.StreamedResponse stringResponse(int status, Map headers, String body) { |
| 45 var stream = new Stream.fromIterable([UTF8.encode(body)]); |
| 46 return new http.StreamedResponse(stream, status, headers: headers); |
| 47 } |
| 48 |
| 49 http.StreamedResponse binaryResponse(int status, |
| 50 Map<String,String> headers, |
| 51 List<int> bytes) { |
| 52 var stream = new Stream.fromIterable([bytes]); |
| 53 return new http.StreamedResponse(stream, status, headers: headers); |
| 54 } |
| 55 |
| 56 Stream<List<int>> byteStream(String s) { |
| 57 var bodyController = new StreamController(); |
| 58 bodyController.add(UTF8.encode(s)); |
| 59 bodyController.close(); |
| 60 return bodyController.stream; |
| 61 } |
| 62 |
| 63 class _ApiRequestError extends TypeMatcher { |
| 64 const _ApiRequestError() : super("ApiRequestError"); |
| 65 bool matches(item, Map matchState) => item is ApiRequestError; |
| 66 } |
| 67 |
| 68 class _DetailedApiRequestError extends TypeMatcher { |
| 69 const _DetailedApiRequestError() : super("DetailedApiRequestError"); |
| 70 bool matches(item, Map matchState) => item is DetailedApiRequestError; |
| 71 } |
| 72 |
| 73 class TestError {} |
| 74 |
| 75 class _TestError extends TypeMatcher { |
| 76 const _TestError() : super("TestError"); |
| 77 bool matches(item, Map matchState) => item is TestError; |
| 78 } |
| 79 |
| 80 const isApiRequestError = const _ApiRequestError(); |
| 81 const isDetailedApiRequestError = const _DetailedApiRequestError(); |
| 82 const isTestError = const _TestError(); |
| 83 |
| 84 |
| 85 main() { |
| 86 group('common-external', () { |
| 87 test('escaper', () { |
| 88 expect(Escaper.ecapePathComponent('a/b%c '), equals('a%2Fb%25c%20')); |
| 89 expect(Escaper.ecapeVariable('a/b%c '), equals('a%2Fb%25c%20')); |
| 90 expect(Escaper.ecapeVariableReserved('a/b%c+ '), equals('a/b%25c+%20')); |
| 91 expect(Escaper.escapeQueryComponent('a/b%c '), equals('a%2Fb%25c%20')); |
| 92 }); |
| 93 |
| 94 test('mapMap', () { |
| 95 newTestMap() => { |
| 96 's' : 'string', |
| 97 'i' : 42, |
| 98 }; |
| 99 |
| 100 var copy = mapMap(newTestMap()); |
| 101 expect(copy, hasLength(2)); |
| 102 expect(copy['s'], equals('string')); |
| 103 expect(copy['i'], equals(42)); |
| 104 |
| 105 |
| 106 var mod = mapMap(newTestMap(), (x) => '$x foobar'); |
| 107 expect(mod, hasLength(2)); |
| 108 expect(mod['s'], equals('string foobar')); |
| 109 expect(mod['i'], equals('42 foobar')); |
| 110 }); |
| 111 |
| 112 test('base64-encoder', () { |
| 113 var base64encoder = new Base64Encoder(); |
| 114 |
| 115 testString(String msg, String expectedBase64) { |
| 116 var msgBytes = UTF8.encode(msg); |
| 117 |
| 118 Stream singleByteStream(List<int> msgBytes) { |
| 119 var controller = new StreamController(); |
| 120 for (var byte in msgBytes) { |
| 121 controller.add([byte]); |
| 122 } |
| 123 controller.close(); |
| 124 return controller.stream; |
| 125 } |
| 126 |
| 127 Stream allByteStream(List<int> msgBytes) { |
| 128 var controller = new StreamController(); |
| 129 controller.add(msgBytes); |
| 130 controller.close(); |
| 131 return controller.stream; |
| 132 } |
| 133 |
| 134 singleByteStream(msgBytes) |
| 135 .transform(base64encoder) |
| 136 .join('') |
| 137 .then(expectAsync((String result) { |
| 138 expect(result, equals(expectedBase64)); |
| 139 })); |
| 140 |
| 141 allByteStream(msgBytes) |
| 142 .transform(base64encoder) |
| 143 .join('') |
| 144 .then(expectAsync((String result) { |
| 145 expect(result, equals(expectedBase64)); |
| 146 })); |
| 147 |
| 148 expect(Base64Encoder.lengthOfBase64Stream(msg.length), |
| 149 equals(expectedBase64.length)); |
| 150 } |
| 151 |
| 152 testString('pleasure.', 'cGxlYXN1cmUu'); |
| 153 testString('leasure.', 'bGVhc3VyZS4='); |
| 154 testString('easure.', 'ZWFzdXJlLg=='); |
| 155 testString('asure.', 'YXN1cmUu'); |
| 156 testString('sure.', 'c3VyZS4='); |
| 157 testString('', ''); |
| 158 }); |
| 159 |
| 160 group('chunk-stack', () { |
| 161 var chunkSize = 9; |
| 162 |
| 163 folded(List<List<int>> byteArrays) { |
| 164 return byteArrays.fold([], (buf, e) => buf..addAll(e)); |
| 165 } |
| 166 |
| 167 test('finalize', () { |
| 168 var chunkStack = new ChunkStack(9); |
| 169 chunkStack.finalize(); |
| 170 expect(() => chunkStack.addBytes([1]), throwsA(isStateError)); |
| 171 expect(() => chunkStack.finalize(), throwsA(isStateError)); |
| 172 }); |
| 173 |
| 174 test('empty', () { |
| 175 var chunkStack = new ChunkStack(9); |
| 176 expect(chunkStack.length, equals(0)); |
| 177 chunkStack.finalize(); |
| 178 expect(chunkStack.length, equals(0)); |
| 179 }); |
| 180 |
| 181 test('sub-chunk-size', () { |
| 182 var bytes = [1, 2, 3]; |
| 183 |
| 184 var chunkStack = new ChunkStack(9); |
| 185 chunkStack.addBytes(bytes); |
| 186 expect(chunkStack.length, equals(0)); |
| 187 chunkStack.finalize(); |
| 188 expect(chunkStack.length, equals(1)); |
| 189 expect(chunkStack.totalByteLength, equals(bytes.length)); |
| 190 |
| 191 var chunks = chunkStack.removeSublist(0, chunkStack.length); |
| 192 expect(chunkStack.length, equals(0)); |
| 193 expect(chunks, hasLength(1)); |
| 194 |
| 195 expect(folded(chunks.first.byteArrays), equals(bytes)); |
| 196 expect(chunks.first.offset, equals(0)); |
| 197 expect(chunks.first.length, equals(3)); |
| 198 expect(chunks.first.endOfChunk, equals(bytes.length)); |
| 199 }); |
| 200 |
| 201 test('exact-chunk-size', () { |
| 202 var bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9]; |
| 203 |
| 204 var chunkStack = new ChunkStack(9); |
| 205 chunkStack.addBytes(bytes); |
| 206 expect(chunkStack.length, equals(1)); |
| 207 chunkStack.finalize(); |
| 208 expect(chunkStack.length, equals(1)); |
| 209 expect(chunkStack.totalByteLength, equals(bytes.length)); |
| 210 |
| 211 var chunks = chunkStack.removeSublist(0, chunkStack.length); |
| 212 expect(chunkStack.length, equals(0)); |
| 213 expect(chunks, hasLength(1)); |
| 214 |
| 215 expect(folded(chunks.first.byteArrays), equals(bytes)); |
| 216 expect(chunks.first.offset, equals(0)); |
| 217 expect(chunks.first.length, equals(bytes.length)); |
| 218 expect(chunks.first.endOfChunk, equals(bytes.length)); |
| 219 }); |
| 220 |
| 221 test('super-chunk-size', () { |
| 222 var bytes0 = [1, 2, 3, 4]; |
| 223 var bytes1 = [1, 2, 3, 4]; |
| 224 var bytes2 = [5, 6, 7, 8, 9, 10, 11]; |
| 225 var bytes = folded([bytes0, bytes1, bytes2]); |
| 226 |
| 227 var chunkStack = new ChunkStack(9); |
| 228 chunkStack.addBytes(bytes0); |
| 229 chunkStack.addBytes(bytes1); |
| 230 chunkStack.addBytes(bytes2); |
| 231 expect(chunkStack.length, equals(1)); |
| 232 chunkStack.finalize(); |
| 233 expect(chunkStack.length, equals(2)); |
| 234 expect(chunkStack.totalByteLength, equals(bytes.length)); |
| 235 |
| 236 var chunks = chunkStack.removeSublist(0, chunkStack.length); |
| 237 expect(chunkStack.length, equals(0)); |
| 238 expect(chunks, hasLength(2)); |
| 239 |
| 240 expect(folded(chunks.first.byteArrays), |
| 241 equals(bytes.sublist(0, chunkSize))); |
| 242 expect(chunks.first.offset, equals(0)); |
| 243 expect(chunks.first.length, equals(chunkSize)); |
| 244 expect(chunks.first.endOfChunk, equals(chunkSize)); |
| 245 |
| 246 expect(folded(chunks.last.byteArrays), |
| 247 equals(bytes.sublist(chunkSize))); |
| 248 expect(chunks.last.offset, equals(chunkSize)); |
| 249 expect(chunks.last.length, equals(bytes.length - chunkSize)); |
| 250 expect(chunks.last.endOfChunk, equals(bytes.length)); |
| 251 }); |
| 252 }); |
| 253 |
| 254 test('media', () { |
| 255 // Tests for [MediaRange] |
| 256 var partialRange = new ByteRange(1, 100); |
| 257 expect(partialRange.start, equals(1)); |
| 258 expect(partialRange.end, equals(100)); |
| 259 |
| 260 var fullRange = new ByteRange(0, -1); |
| 261 expect(fullRange.start, equals(0)); |
| 262 expect(fullRange.end, equals(-1)); |
| 263 |
| 264 expect(() => new ByteRange(0, 0), throws); |
| 265 expect(() => new ByteRange(-1, 0), throws); |
| 266 expect(() => new ByteRange(-1, 1), throws); |
| 267 |
| 268 // Tests for [DownloadOptions] |
| 269 expect(DownloadOptions.Metadata.isMetadataDownload, isTrue); |
| 270 |
| 271 expect(DownloadOptions.FullMedia.isFullDownload, isTrue); |
| 272 expect(DownloadOptions.FullMedia.isMetadataDownload, isFalse); |
| 273 |
| 274 // Tests for [Media] |
| 275 var stream = new StreamController().stream; |
| 276 expect(() => new Media(null, 0, contentType: 'foobar'), |
| 277 throwsA(isArgumentError)); |
| 278 expect(() => new Media(stream, 0, contentType: null), |
| 279 throwsA(isArgumentError)); |
| 280 expect(() => new Media(stream, -1, contentType: 'foobar'), |
| 281 throwsA(isArgumentError)); |
| 282 |
| 283 var lengthUnknownMedia = new Media(stream, null); |
| 284 expect(lengthUnknownMedia.stream, equals(stream)); |
| 285 expect(lengthUnknownMedia.length, equals(null)); |
| 286 |
| 287 var media = new Media(stream, 10, contentType: 'foobar'); |
| 288 expect(media.stream, equals(stream)); |
| 289 expect(media.length, equals(10)); |
| 290 expect(media.contentType, equals('foobar')); |
| 291 |
| 292 // Tests for [ResumableUploadOptions] |
| 293 expect(() => new ResumableUploadOptions(numberOfAttempts: 0), |
| 294 throwsA(isArgumentError)); |
| 295 expect(() => new ResumableUploadOptions(chunkSize: 1), |
| 296 throwsA(isArgumentError)); |
| 297 }); |
| 298 |
| 299 group('api-requester', () { |
| 300 var httpMock, rootUrl, basePath; |
| 301 ApiRequester requester; |
| 302 |
| 303 var responseHeaders = { |
| 304 'content-type' : 'application/json; charset=utf-8', |
| 305 }; |
| 306 |
| 307 setUp(() { |
| 308 httpMock = new HttpServerMock(); |
| 309 rootUrl = 'http://example.com/'; |
| 310 basePath = '/base/'; |
| 311 requester = new ApiRequester(httpMock, rootUrl, basePath); |
| 312 }); |
| 313 |
| 314 |
| 315 // Tests for Request, Response |
| 316 |
| 317 group('metadata-request-response', () { |
| 318 test('empty-request-empty-response', () { |
| 319 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 320 expect(request.method, equals('GET')); |
| 321 expect('${request.url}', |
| 322 equals('http://example.com/base/abc?alt=json')); |
| 323 return stringResponse(200, responseHeaders, ''); |
| 324 }), true); |
| 325 requester.request('abc', 'GET').then(expectAsync((response) { |
| 326 expect(response, isNull); |
| 327 })); |
| 328 }); |
| 329 |
| 330 test('json-map-request-json-map-response', () { |
| 331 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 332 expect(request.method, equals('GET')); |
| 333 expect('${request.url}', |
| 334 equals('http://example.com/base/abc?alt=json')); |
| 335 expect(json is Map, isTrue); |
| 336 expect(json, hasLength(1)); |
| 337 expect(json['foo'], equals('bar')); |
| 338 return stringResponse(200, responseHeaders, '{"foo2" : "bar2"}'); |
| 339 }), true); |
| 340 requester.request('abc', |
| 341 'GET', |
| 342 body: JSON.encode({'foo' : 'bar'})).then( |
| 343 expectAsync((response) { |
| 344 expect(response is Map, isTrue); |
| 345 expect(response, hasLength(1)); |
| 346 expect(response['foo2'], equals('bar2')); |
| 347 })); |
| 348 }); |
| 349 |
| 350 test('json-list-request-json-list-response', () { |
| 351 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 352 expect(request.method, equals('GET')); |
| 353 expect('${request.url}', |
| 354 equals('http://example.com/base/abc?alt=json')); |
| 355 expect(json is List, isTrue); |
| 356 expect(json, hasLength(2)); |
| 357 expect(json[0], equals('a')); |
| 358 expect(json[1], equals(1)); |
| 359 return stringResponse(200, responseHeaders, '["b", 2]'); |
| 360 }), true); |
| 361 requester.request('abc', |
| 362 'GET', |
| 363 body: JSON.encode(['a', 1])).then( |
| 364 expectAsync((response) { |
| 365 expect(response is List, isTrue); |
| 366 expect(response[0], equals('b')); |
| 367 expect(response[1], equals(2)); |
| 368 })); |
| 369 }); |
| 370 }); |
| 371 |
| 372 group('media-download', () { |
| 373 test('media-download', () { |
| 374 var data256 = new List.generate(256, (i) => i); |
| 375 httpMock.register(expectAsync((http.BaseRequest request, data) { |
| 376 expect(request.method, equals('GET')); |
| 377 expect('${request.url}', |
| 378 equals('http://example.com/base/abc?alt=media')); |
| 379 expect(data, isEmpty); |
| 380 var headers = { |
| 381 'content-length' : '${data256.length}', |
| 382 'content-type' : 'foobar', |
| 383 }; |
| 384 return binaryResponse(200, headers, data256); |
| 385 }), false); |
| 386 requester.request('abc', |
| 387 'GET', |
| 388 body: '', |
| 389 downloadOptions: DownloadOptions.FullMedia).then( |
| 390 expectAsync((Media media) { |
| 391 expect(media.contentType, equals('foobar')); |
| 392 expect(media.length, equals(data256.length)); |
| 393 media.stream.fold([], (b, d) => b..addAll(d)).then(expectAsync((d) { |
| 394 expect(d, equals(data256)); |
| 395 })); |
| 396 })); |
| 397 }); |
| 398 |
| 399 test('media-download-partial', () { |
| 400 var data256 = new List.generate(256, (i) => i); |
| 401 var data64 = data256.sublist(128, 128 + 64); |
| 402 |
| 403 httpMock.register(expectAsync((http.BaseRequest request, data) { |
| 404 expect(request.method, equals('GET')); |
| 405 expect('${request.url}', |
| 406 equals('http://example.com/base/abc?alt=media')); |
| 407 expect(data, isEmpty); |
| 408 expect(request.headers['range'], |
| 409 equals('bytes=128-191')); |
| 410 var headers = { |
| 411 'content-length' : '${data64.length}', |
| 412 'content-type' : 'foobar', |
| 413 'content-range' : 'bytes 128-191/256', |
| 414 }; |
| 415 return binaryResponse(200, headers, data64); |
| 416 }), false); |
| 417 var range = new ByteRange(128, 128 + 64 - 1); |
| 418 var options = new PartialDownloadOptions(range); |
| 419 requester.request('abc', |
| 420 'GET', |
| 421 body: '', |
| 422 downloadOptions: options).then( |
| 423 expectAsync((Media media) { |
| 424 expect(media.contentType, equals('foobar')); |
| 425 expect(media.length, equals(data64.length)); |
| 426 media.stream.fold([], (b, d) => b..addAll(d)).then(expectAsync((d) { |
| 427 expect(d, equals(data64)); |
| 428 })); |
| 429 })); |
| 430 }); |
| 431 |
| 432 test('json-upload-media-download', () { |
| 433 var data256 = new List.generate(256, (i) => i); |
| 434 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 435 expect(request.method, equals('GET')); |
| 436 expect('${request.url}', |
| 437 equals('http://example.com/base/abc?alt=media')); |
| 438 expect(json is List, isTrue); |
| 439 expect(json, hasLength(2)); |
| 440 expect(json[0], equals('a')); |
| 441 expect(json[1], equals(1)); |
| 442 |
| 443 var headers = { |
| 444 'content-length' : '${data256.length}', |
| 445 'content-type' : 'foobar', |
| 446 }; |
| 447 return binaryResponse(200, headers, data256); |
| 448 }), true); |
| 449 requester.request('abc', |
| 450 'GET', |
| 451 body: JSON.encode(['a', 1]), |
| 452 downloadOptions: DownloadOptions.FullMedia).then( |
| 453 expectAsync((Media media) { |
| 454 expect(media.contentType, equals('foobar')); |
| 455 expect(media.length, equals(data256.length)); |
| 456 media.stream.fold([], (b, d) => b..addAll(d)).then(expectAsync((d) { |
| 457 expect(d, equals(data256)); |
| 458 })); |
| 459 })); |
| 460 }); |
| 461 }); |
| 462 |
| 463 // Tests for media uploads |
| 464 |
| 465 group('media-upload', () { |
| 466 Stream streamFromByteArrays(byteArrays) { |
| 467 var controller = new StreamController(); |
| 468 for (var array in byteArrays) { |
| 469 controller.add(array); |
| 470 } |
| 471 controller.close(); |
| 472 return controller.stream; |
| 473 } |
| 474 Media mediaFromByteArrays(byteArrays, {bool withLen: true}) { |
| 475 int len = 0; |
| 476 byteArrays.forEach((array) { len += array.length; }); |
| 477 if (!withLen) len = null; |
| 478 return new Media(streamFromByteArrays(byteArrays), |
| 479 len, |
| 480 contentType: 'foobar'); |
| 481 } |
| 482 validateServerRequest(e, http.BaseRequest request, List<int> data) { |
| 483 return new Future.sync(() { |
| 484 var h = e['headers']; |
| 485 var r = e['response']; |
| 486 |
| 487 expect(request.url.toString(), equals(e['url'])); |
| 488 expect(request.method, equals(e['method'])); |
| 489 h.forEach((k, v) { |
| 490 expect(request.headers[k], equals(v)); |
| 491 }); |
| 492 |
| 493 expect(data, equals(e['data'])); |
| 494 return r; |
| 495 }); |
| 496 } |
| 497 serverRequestValidator(List expectations) { |
| 498 int i = 0; |
| 499 return (http.BaseRequest request, List<int> data) { |
| 500 return validateServerRequest(expectations[i++], request, data); |
| 501 }; |
| 502 } |
| 503 |
| 504 test('simple', () { |
| 505 var bytes = new List.generate(10 * 256 * 1024 + 1, (i) => i % 256); |
| 506 var expectations = [ |
| 507 { |
| 508 'url' : 'http://example.com/xyz?uploadType=media&alt=json', |
| 509 'method' : 'POST', |
| 510 'data' : bytes, |
| 511 'headers' : { |
| 512 'content-length' : '${bytes.length}', |
| 513 'content-type' : 'foobar', |
| 514 }, |
| 515 'response' : stringResponse(200, responseHeaders, '') |
| 516 }, |
| 517 ]; |
| 518 |
| 519 httpMock.register( |
| 520 expectAsync(serverRequestValidator(expectations)), false); |
| 521 var media = mediaFromByteArrays([bytes]); |
| 522 requester.request('/xyz', |
| 523 'POST', |
| 524 uploadMedia: media).then( |
| 525 expectAsync((response) {})); |
| 526 }); |
| 527 |
| 528 test('multipart-upload', () { |
| 529 var bytes = new List.generate(10 * 256 * 1024 + 1, (i) => i % 256); |
| 530 var contentBytes = |
| 531 '--314159265358979323846\r\n' |
| 532 'Content-Type: $CONTENT_TYPE_JSON_UTF8\r\n\r\n' |
| 533 'BODY' |
| 534 '\r\n--314159265358979323846\r\n' |
| 535 'Content-Type: foobar\r\n' |
| 536 'Content-Transfer-Encoding: base64\r\n\r\n' |
| 537 '${crypto.CryptoUtils.bytesToBase64(bytes)}' |
| 538 '\r\n--314159265358979323846--'; |
| 539 |
| 540 var expectations = [ |
| 541 { |
| 542 'url' : 'http://example.com/xyz?uploadType=multipart&alt=json', |
| 543 'method' : 'POST', |
| 544 'data' : UTF8.encode('$contentBytes'), |
| 545 'headers' : { |
| 546 'content-length' : '${contentBytes.length}', |
| 547 'content-type' : |
| 548 'multipart/related; boundary="314159265358979323846"', |
| 549 }, |
| 550 'response' : stringResponse(200, responseHeaders, '') |
| 551 }, |
| 552 ]; |
| 553 |
| 554 httpMock.register( |
| 555 expectAsync(serverRequestValidator(expectations)), false); |
| 556 var media = mediaFromByteArrays([bytes]); |
| 557 requester.request('/xyz', |
| 558 'POST', |
| 559 body: 'BODY', |
| 560 uploadMedia: media).then( |
| 561 expectAsync((response) {})); |
| 562 }); |
| 563 |
| 564 group('resumable-upload', () { |
| 565 // TODO: respect [stream] |
| 566 buildExpectations(List<int> bytes, int chunkSize, bool stream, |
| 567 {int numberOfServerErrors: 0}) { |
| 568 int totalLength = bytes.length; |
| 569 int numberOfChunks = totalLength ~/ chunkSize; |
| 570 int numberOfBytesInLastChunk = totalLength % chunkSize; |
| 571 |
| 572 if (numberOfBytesInLastChunk > 0) { |
| 573 numberOfChunks++; |
| 574 } else { |
| 575 numberOfBytesInLastChunk = chunkSize; |
| 576 } |
| 577 |
| 578 var expectations = []; |
| 579 |
| 580 // First request is making a POST and gets the upload URL. |
| 581 expectations.add({ |
| 582 'url' : 'http://example.com/xyz?uploadType=resumable&alt=json', |
| 583 'method' : 'POST', |
| 584 'data' : [], |
| 585 'headers' : { |
| 586 'content-length' : '0', |
| 587 'content-type' : 'application/json; charset=utf-8', |
| 588 'x-upload-content-type' : 'foobar', |
| 589 }..addAll(stream ? {} : { |
| 590 'x-upload-content-length' : '$totalLength', |
| 591 }), |
| 592 'response' : stringResponse( |
| 593 200, {'location' : 'http://upload.com/'}, '') |
| 594 }); |
| 595 |
| 596 var lastEnd = 0; |
| 597 for (int i = 0; i < numberOfChunks; i++) { |
| 598 bool isLast = i == (numberOfChunks - 1); |
| 599 var lengthMarker = stream && !isLast ? '*' : '$totalLength'; |
| 600 |
| 601 int bytesToExpect = chunkSize; |
| 602 if (isLast) { |
| 603 bytesToExpect = numberOfBytesInLastChunk; |
| 604 } |
| 605 |
| 606 var start = i * chunkSize; |
| 607 var end = start + bytesToExpect; |
| 608 var sublist = bytes.sublist(start, end); |
| 609 |
| 610 var firstContentRange = |
| 611 'bytes $start-${end-1}/$lengthMarker'; |
| 612 var firstRange = |
| 613 'bytes=0-${end-1}'; |
| 614 |
| 615 // We issue [numberOfServerErrors] 503 errors first, and then a |
| 616 // successfull response. |
| 617 for (var j = 0; j < (numberOfServerErrors + 1); j++) { |
| 618 bool successfullResponse = j == numberOfServerErrors; |
| 619 |
| 620 var response; |
| 621 if (successfullResponse) { |
| 622 var headers = isLast |
| 623 ? { 'content-type' : 'application/json; charset=utf-8' } |
| 624 : {'range' : firstRange }; |
| 625 response = stringResponse(isLast ? 200 : 308, headers, ''); |
| 626 } else { |
| 627 var headers = {}; |
| 628 response = stringResponse(503, headers, ''); |
| 629 } |
| 630 |
| 631 expectations.add({ |
| 632 'url' : 'http://upload.com/', |
| 633 'method' : 'PUT', |
| 634 'data' : sublist, |
| 635 'headers' : { |
| 636 'content-length' : '${sublist.length}', |
| 637 'content-range' : firstContentRange, |
| 638 'content-type' : 'foobar', |
| 639 }, |
| 640 'response' : response, |
| 641 }); |
| 642 } |
| 643 } |
| 644 return expectations; |
| 645 } |
| 646 |
| 647 List<List<int>> makeParts(List<int> bytes, List<int> splits) { |
| 648 var parts = []; |
| 649 int lastEnd = 0; |
| 650 for (int i = 0; i < splits.length; i++) { |
| 651 parts.add(bytes.sublist(lastEnd, splits[i])); |
| 652 lastEnd = splits[i]; |
| 653 } |
| 654 return parts; |
| 655 } |
| 656 |
| 657 runTest(int chunkSizeInBlocks, int length, List splits, bool stream, |
| 658 {int numberOfServerErrors: 0, resumableOptions, |
| 659 int expectedErrorStatus, int messagesNrOfFailure}) { |
| 660 int chunkSize = chunkSizeInBlocks * 256 * 1024; |
| 661 |
| 662 int i = 0; |
| 663 var bytes = new List.generate(length, (i) => i % 256); |
| 664 var parts = makeParts(bytes, splits); |
| 665 |
| 666 // Simulation of our server |
| 667 var expectations = buildExpectations( |
| 668 bytes, chunkSize, false, |
| 669 numberOfServerErrors: numberOfServerErrors); |
| 670 // If the server simulates 50X errors and the client resumes only |
| 671 // a limited amount of time, we'll trunkate the number of requests |
| 672 // the server expects. |
| 673 // [The client will give up and if the server expects more, the test |
| 674 // would timeout.] |
| 675 if (expectedErrorStatus != null) { |
| 676 expectations = expectations.sublist(0, messagesNrOfFailure); |
| 677 } |
| 678 httpMock.register( |
| 679 expectAsync(serverRequestValidator(expectations), |
| 680 count: expectations.length), |
| 681 false); |
| 682 |
| 683 // Our client |
| 684 var media = mediaFromByteArrays(parts); |
| 685 if (resumableOptions == null) { |
| 686 resumableOptions = |
| 687 new ResumableUploadOptions(chunkSize: chunkSize); |
| 688 } |
| 689 var result = requester.request('/xyz', |
| 690 'POST', |
| 691 uploadMedia: media, |
| 692 uploadOptions: resumableOptions); |
| 693 if (expectedErrorStatus != null) { |
| 694 result.catchError(expectAsync((error) { |
| 695 expect(error is DetailedApiRequestError, isTrue); |
| 696 expect(error.status, equals(expectedErrorStatus)); |
| 697 })); |
| 698 } else { |
| 699 result.then(expectAsync((_) {})); |
| 700 } |
| 701 } |
| 702 |
| 703 Function backoffWrapper(int callCount) { |
| 704 return expectAsync((int failedAttempts) { |
| 705 var exp = ResumableUploadOptions.ExponentialBackoff; |
| 706 Duration duration = exp(failedAttempts); |
| 707 expect(duration.inSeconds, equals(1 << (failedAttempts - 1))); |
| 708 return const Duration(milliseconds: 1); |
| 709 }, count: callCount); |
| 710 } |
| 711 |
| 712 test('length-small-block', () { |
| 713 runTest(1, 10, [10], false); |
| 714 }); |
| 715 |
| 716 test('length-small-block-parts', () { |
| 717 runTest(1, 20, [1, 2, 3, 4, 5, 6, 7, 19, 20], false); |
| 718 }); |
| 719 |
| 720 test('length-big-block', () { |
| 721 runTest(1, 1024 * 1024, [1024*1024], false); |
| 722 }); |
| 723 |
| 724 test('length-big-block-parts', () { |
| 725 runTest(1, 1024 * 1024, |
| 726 [1, |
| 727 256*1024-1, |
| 728 256*1024, |
| 729 256*1024+1, |
| 730 1024*1024-1, |
| 731 1024*1024], false); |
| 732 }); |
| 733 |
| 734 test('stream-small-block', () { |
| 735 runTest(1, 10, [10], true); |
| 736 }); |
| 737 |
| 738 test('stream-small-block-parts', () { |
| 739 runTest(1, 20, [1, 2, 3, 4, 5, 6, 7, 19, 20], true); |
| 740 }); |
| 741 |
| 742 test('stream-big-block', () { |
| 743 runTest(1, 1024 * 1024, [1024*1024], true); |
| 744 }); |
| 745 |
| 746 test('stream-big-block-parts', () { |
| 747 runTest(1, 1024 * 1024, |
| 748 [1, |
| 749 256*1024-1, |
| 750 256*1024, |
| 751 256*1024+1, |
| 752 1024*1024-1, |
| 753 1024*1024], true); |
| 754 }); |
| 755 |
| 756 test('stream-big-block-parts--with-server-error-recovery', () { |
| 757 var numFailedAttempts = 4 * 3; |
| 758 var options = new ResumableUploadOptions( |
| 759 chunkSize: 256 * 1024, numberOfAttempts: 4, |
| 760 backoffFunction: backoffWrapper(numFailedAttempts)); |
| 761 runTest(1, 1024 * 1024, |
| 762 [1, |
| 763 256*1024-1, |
| 764 256*1024, |
| 765 256*1024+1, |
| 766 1024*1024-1, |
| 767 1024*1024], |
| 768 true, |
| 769 numberOfServerErrors: 3, |
| 770 resumableOptions: options); |
| 771 }); |
| 772 |
| 773 test('stream-big-block-parts--server-error', () { |
| 774 var numFailedAttempts = 2; |
| 775 var options = new ResumableUploadOptions( |
| 776 chunkSize: 256 * 1024, numberOfAttempts: 3, |
| 777 backoffFunction: backoffWrapper(numFailedAttempts)); |
| 778 runTest(1, 1024 * 1024, |
| 779 [1, |
| 780 256*1024-1, |
| 781 256*1024, |
| 782 256*1024+1, |
| 783 1024*1024-1, |
| 784 1024*1024], |
| 785 true, |
| 786 numberOfServerErrors: 3, |
| 787 resumableOptions: options, |
| 788 expectedErrorStatus: 503, |
| 789 messagesNrOfFailure: 4); |
| 790 }); |
| 791 }); |
| 792 }); |
| 793 |
| 794 // Tests for error responses |
| 795 group('request-errors', () { |
| 796 makeTestError() { |
| 797 // All errors from the [http.Client] propagate through. |
| 798 // We use [TestError] to simulate it. |
| 799 httpMock.register(expectAsync((http.BaseRequest request, string) { |
| 800 return new Future.error(new TestError()); |
| 801 }), false); |
| 802 } |
| 803 |
| 804 makeDetailed400Error() { |
| 805 httpMock.register(expectAsync((http.BaseRequest request, string) { |
| 806 return stringResponse(400, |
| 807 responseHeaders, |
| 808 '{"error" : {"code" : 42, "message": "foo"}}'); |
| 809 }), false); |
| 810 } |
| 811 |
| 812 makeNormal199Error() { |
| 813 httpMock.register(expectAsync((http.BaseRequest request, string) { |
| 814 return stringResponse(199, {}, ''); |
| 815 }), false); |
| 816 } |
| 817 |
| 818 makeInvalidContentTypeError() { |
| 819 httpMock.register(expectAsync((http.BaseRequest request, string) { |
| 820 var responseHeaders = { 'content-type' : 'image/png'}; |
| 821 return stringResponse(200, responseHeaders, ''); |
| 822 }), false); |
| 823 } |
| 824 |
| 825 |
| 826 test('normal-http-client', () { |
| 827 makeTestError(); |
| 828 expect(requester.request('abc', 'GET'), throwsA(isTestError)); |
| 829 }); |
| 830 |
| 831 test('normal-detailed-400', () { |
| 832 makeDetailed400Error(); |
| 833 requester.request('abc', 'GET') |
| 834 .catchError(expectAsync((error, stack) { |
| 835 expect(error, isDetailedApiRequestError); |
| 836 DetailedApiRequestError e = error; |
| 837 expect(e.status, equals(42)); |
| 838 expect(e.message, equals('foo')); |
| 839 })); |
| 840 }); |
| 841 |
| 842 test('normal-199', () { |
| 843 makeNormal199Error(); |
| 844 expect(requester.request('abc', 'GET'), throwsA(isApiRequestError)); |
| 845 }); |
| 846 |
| 847 test('normal-invalid-content-type', () { |
| 848 makeInvalidContentTypeError(); |
| 849 expect(requester.request('abc', 'GET'), throwsA(isApiRequestError)); |
| 850 }); |
| 851 |
| 852 var options = DownloadOptions.FullMedia; |
| 853 test('media-http-client', () { |
| 854 makeTestError(); |
| 855 expect(requester.request('abc', 'GET', downloadOptions: options), |
| 856 throwsA(isTestError)); |
| 857 }); |
| 858 |
| 859 test('media-detailed-400', () { |
| 860 makeDetailed400Error(); |
| 861 requester.request('abc', 'GET') |
| 862 .catchError(expectAsync((error, stack) { |
| 863 expect(error, isDetailedApiRequestError); |
| 864 DetailedApiRequestError e = error; |
| 865 expect(e.status, equals(42)); |
| 866 expect(e.message, equals('foo')); |
| 867 })); |
| 868 }); |
| 869 |
| 870 test('media-199', () { |
| 871 makeNormal199Error(); |
| 872 expect(requester.request('abc', 'GET', downloadOptions: options), |
| 873 throwsA(isApiRequestError)); |
| 874 }); |
| 875 }); |
| 876 |
| 877 |
| 878 // Tests for path/query parameters |
| 879 |
| 880 test('request-parameters-query', () { |
| 881 var queryParams = { |
| 882 'a' : ['a1', 'a2'], |
| 883 's' : ['s1'] |
| 884 }; |
| 885 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 886 expect(request.method, equals('GET')); |
| 887 expect('${request.url}', |
| 888 equals('http://example.com/base/abc?a=a1&a=a2&s=s1&alt=json')); |
| 889 return stringResponse(200, responseHeaders, ''); |
| 890 }), true); |
| 891 requester.request('abc', 'GET', queryParams: queryParams) |
| 892 .then(expectAsync((response) { |
| 893 expect(response, isNull); |
| 894 })); |
| 895 }); |
| 896 |
| 897 test('request-parameters-path', () { |
| 898 httpMock.register(expectAsync((http.BaseRequest request, json) { |
| 899 expect(request.method, equals('GET')); |
| 900 expect('${request.url}', equals( |
| 901 'http://example.com/base/s/foo/a1/a2/bar/s1/e?alt=json')); |
| 902 return stringResponse(200, responseHeaders, ''); |
| 903 }), true); |
| 904 requester.request('s/foo/a1/a2/bar/s1/e', 'GET') |
| 905 .then(expectAsync((response) { |
| 906 expect(response, isNull); |
| 907 })); |
| 908 }); |
| 909 }); |
| 910 }); |
| 911 } |
OLD | NEW |