OLD | NEW |
| (Empty) |
1 library googleapis_beta.common_internal_test; | |
2 import 'dart:async'; | |
3 import 'dart:convert'; | |
4 | |
5 import 'package:crypto/crypto.dart' as crypto; | |
6 import 'package:googleapis_beta/common/common.dart'; | |
7 import 'package:googleapis_beta/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 |