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