|
12 | 12 | // See the License for the specific language governing permissions and |
13 | 13 | // limitations under the License. |
14 | 14 |
|
| 15 | +import 'dart:convert'; |
| 16 | +import 'dart:io'; |
| 17 | + |
15 | 18 | import 'package:google_cloud_firestore/google_cloud_firestore.dart'; |
16 | 19 | import 'package:test/test.dart' hide throwsArgumentError; |
17 | 20 |
|
18 | 21 | import '../fixtures/helpers.dart'; |
19 | 22 |
|
| 23 | +void _writeRetryError( |
| 24 | + HttpRequest request, |
| 25 | + int code, |
| 26 | + String status, |
| 27 | + String message, |
| 28 | +) { |
| 29 | + request.response |
| 30 | + ..statusCode = code |
| 31 | + ..headers.contentType = ContentType.json |
| 32 | + ..write( |
| 33 | + jsonEncode({ |
| 34 | + 'error': {'code': code, 'status': status, 'message': message}, |
| 35 | + }), |
| 36 | + ); |
| 37 | + request.response.close(); |
| 38 | +} |
| 39 | + |
| 40 | +void _writeCommitSuccess(HttpRequest request) { |
| 41 | + request.response |
| 42 | + ..statusCode = 200 |
| 43 | + ..headers.contentType = ContentType.json |
| 44 | + ..write( |
| 45 | + jsonEncode({ |
| 46 | + 'commitTime': '2024-01-01T00:00:00.000Z', |
| 47 | + 'writeResults': [ |
| 48 | + {'updateTime': '2024-01-01T00:00:00.000Z'}, |
| 49 | + ], |
| 50 | + }), |
| 51 | + ); |
| 52 | + request.response.close(); |
| 53 | +} |
| 54 | + |
20 | 55 | void main() { |
21 | 56 | group('WriteBatch', () { |
22 | 57 | late Firestore firestore; |
@@ -341,4 +376,118 @@ void main() { |
341 | 376 | }); |
342 | 377 | }); |
343 | 378 | }); |
| 379 | + |
| 380 | + group('WriteBatch retry', () { |
| 381 | + late HttpServer server; |
| 382 | + late Firestore firestore; |
| 383 | + late int callCount; |
| 384 | + late void Function(HttpRequest request, int callNumber) handler; |
| 385 | + |
| 386 | + setUp(() async { |
| 387 | + callCount = 0; |
| 388 | + handler = (_, _) {}; |
| 389 | + |
| 390 | + server = await HttpServer.bind('localhost', 0); |
| 391 | + server.listen((request) async { |
| 392 | + await request.drain<void>(); |
| 393 | + callCount++; |
| 394 | + handler(request, callCount); |
| 395 | + }); |
| 396 | + |
| 397 | + firestore = Firestore( |
| 398 | + settings: Settings( |
| 399 | + projectId: 'test-project', |
| 400 | + environmentOverride: { |
| 401 | + 'FIRESTORE_EMULATOR_HOST': 'localhost:${server.port}', |
| 402 | + 'GOOGLE_CLOUD_PROJECT': 'test-project', |
| 403 | + }, |
| 404 | + ), |
| 405 | + ); |
| 406 | + }); |
| 407 | + |
| 408 | + tearDown(() async { |
| 409 | + await firestore.terminate(); |
| 410 | + await server.close(force: true); |
| 411 | + }); |
| 412 | + |
| 413 | + test('retries on UNAVAILABLE and succeeds', () async { |
| 414 | + handler = (request, callNumber) { |
| 415 | + if (callNumber == 1) { |
| 416 | + _writeRetryError(request, 503, 'UNAVAILABLE', 'Service unavailable'); |
| 417 | + } else { |
| 418 | + _writeCommitSuccess(request); |
| 419 | + } |
| 420 | + }; |
| 421 | + |
| 422 | + await firestore.doc('test/retry').set({'value': 1}); |
| 423 | + expect(callCount, 2); |
| 424 | + }); |
| 425 | + |
| 426 | + test('retries on ABORTED and succeeds', () async { |
| 427 | + handler = (request, callNumber) { |
| 428 | + if (callNumber == 1) { |
| 429 | + _writeRetryError(request, 409, 'ABORTED', 'Transaction lock timeout'); |
| 430 | + } else { |
| 431 | + _writeCommitSuccess(request); |
| 432 | + } |
| 433 | + }; |
| 434 | + |
| 435 | + await firestore.doc('test/retry').set({'value': 1}); |
| 436 | + expect(callCount, 2); |
| 437 | + }); |
| 438 | + |
| 439 | + test('succeeds after multiple transient failures', () async { |
| 440 | + handler = (request, callNumber) { |
| 441 | + if (callNumber <= 3) { |
| 442 | + _writeRetryError(request, 503, 'UNAVAILABLE', 'Service unavailable'); |
| 443 | + } else { |
| 444 | + _writeCommitSuccess(request); |
| 445 | + } |
| 446 | + }; |
| 447 | + |
| 448 | + await firestore.doc('test/retry').set({'value': 1}); |
| 449 | + expect(callCount, 4); |
| 450 | + }); |
| 451 | + |
| 452 | + test('does not retry on PERMISSION_DENIED', () async { |
| 453 | + handler = (request, _) { |
| 454 | + _writeRetryError( |
| 455 | + request, |
| 456 | + 403, |
| 457 | + 'PERMISSION_DENIED', |
| 458 | + 'Missing permissions', |
| 459 | + ); |
| 460 | + }; |
| 461 | + |
| 462 | + await expectLater( |
| 463 | + () => firestore.doc('test/retry').set({'value': 1}), |
| 464 | + throwsA( |
| 465 | + isA<FirestoreException>().having( |
| 466 | + (e) => e.errorCode, |
| 467 | + 'errorCode', |
| 468 | + FirestoreClientErrorCode.permissionDenied, |
| 469 | + ), |
| 470 | + ), |
| 471 | + ); |
| 472 | + expect(callCount, 1); |
| 473 | + }); |
| 474 | + |
| 475 | + test('does not retry on INVALID_ARGUMENT', () async { |
| 476 | + handler = (request, _) { |
| 477 | + _writeRetryError(request, 400, 'INVALID_ARGUMENT', 'Invalid field'); |
| 478 | + }; |
| 479 | + |
| 480 | + await expectLater( |
| 481 | + () => firestore.doc('test/retry').set({'value': 1}), |
| 482 | + throwsA( |
| 483 | + isA<FirestoreException>().having( |
| 484 | + (e) => e.errorCode, |
| 485 | + 'errorCode', |
| 486 | + FirestoreClientErrorCode.invalidArgument, |
| 487 | + ), |
| 488 | + ), |
| 489 | + ); |
| 490 | + expect(callCount, 1); |
| 491 | + }); |
| 492 | + }); |
344 | 493 | } |
0 commit comments