@@ -10,6 +10,7 @@ import 'package:zulip/api/route/events.dart';
10
10
import 'package:zulip/api/route/messages.dart' ;
11
11
import 'package:zulip/model/message_list.dart' ;
12
12
import 'package:zulip/model/narrow.dart' ;
13
+ import 'package:zulip/log.dart' ;
13
14
import 'package:zulip/model/store.dart' ;
14
15
import 'package:zulip/notifications/receive.dart' ;
15
16
@@ -393,6 +394,22 @@ void main() {
393
394
check (store.userSettings! .twentyFourHourTime).isTrue ();
394
395
}));
395
396
397
+ String ? lastReportedError;
398
+ String ? takeLastReportedError () {
399
+ final result = lastReportedError;
400
+ lastReportedError = null ;
401
+ return result;
402
+ }
403
+
404
+ /// This is an alternative to [ZulipApp] 's implementation of
405
+ /// [reportErrorToUserBriefly] for testing.
406
+ Future <void > logAndReportErrorToUserBriefly (String ? message, {
407
+ String ? details,
408
+ }) async {
409
+ if (message == null ) return ;
410
+ lastReportedError = '$message \n $details ' ;
411
+ }
412
+
396
413
test ('handles expired queue' , () => awaitFakeAsync ((async ) async {
397
414
await prepareStore ();
398
415
updateMachine.debugPauseLoop ();
@@ -456,19 +473,52 @@ void main() {
456
473
}));
457
474
458
475
group ('retries on errors' , () {
459
- void checkRetry (void Function () prepareError) {
476
+ /// Check if [UpdateMachine.poll] retries as expected when there are
477
+ /// errors.
478
+ ///
479
+ /// This also verifies that the first user-facing error message appears
480
+ /// after the `numFailedRequests` 'th failed request.
481
+ void checkRetry (void Function () prepareError, {
482
+ required int numFailedRequests,
483
+ }) {
484
+ assert (numFailedRequests > 0 );
485
+ reportErrorToUserBriefly = logAndReportErrorToUserBriefly;
486
+ addTearDown (() => reportErrorToUserBriefly = defaultReportErrorToUserBriefly);
487
+
488
+ final expectedErrorMessage =
489
+ 'Error connecting to Zulip. Retrying…\n '
490
+ 'Error connecting to Zulip at ${eg .realmUrl .origin }. Will retry' ;
491
+
460
492
awaitFakeAsync ((async ) async {
461
493
await prepareStore (lastEventId: 1 );
462
494
updateMachine.debugPauseLoop ();
463
495
updateMachine.poll ();
464
496
check (async .pendingTimers).length.equals (0 );
465
497
466
- // Make the request, inducing an error in it.
467
- prepareError ();
468
- updateMachine.debugAdvanceLoop ();
469
- async .elapse (Duration .zero);
470
- checkLastRequest (lastEventId: 1 );
471
- check (store).isLoading.isTrue ();
498
+ for (int i = 0 ; i < numFailedRequests; i++ ) {
499
+ // Make the request, inducing an error in it.
500
+ prepareError ();
501
+ updateMachine.debugAdvanceLoop ();
502
+ async .elapse (Duration .zero);
503
+ checkLastRequest (lastEventId: 1 );
504
+ check (store).isLoading.isTrue ();
505
+
506
+ if (i != numFailedRequests - 1 ) {
507
+ // Skip polling backoff unless this is the final iteration.
508
+ // This allows the next `updateMachine.debugAdvanceLoop` call to
509
+ // trigger the next request without wait.
510
+ async .flushTimers ();
511
+ }
512
+
513
+ if (i < numFailedRequests - 1 ) {
514
+ // The error message should not appear until the `updateMachine`
515
+ // has retried the given number of times.
516
+ check (takeLastReportedError ()).isNull ();
517
+ continue ;
518
+ }
519
+ assert (i == numFailedRequests - 1 );
520
+ check (takeLastReportedError ()).isNotNull ().contains (expectedErrorMessage);
521
+ }
472
522
473
523
// Polling doesn't resume immediately; there's a timer.
474
524
check (async .pendingTimers).length.equals (1 );
@@ -489,20 +539,24 @@ void main() {
489
539
}
490
540
491
541
test ('Server5xxException' , () {
492
- checkRetry (() => connection.prepare (httpStatus: 500 , body: 'splat' ));
542
+ checkRetry (() => connection.prepare (httpStatus: 500 , body: 'splat' ),
543
+ numFailedRequests: UpdateMachine .transientFailureCountNotifyThreshold + 1 );
493
544
});
494
545
495
546
test ('NetworkException' , () {
496
- checkRetry (() => connection.prepare (exception: Exception ("failed" )));
547
+ checkRetry (() => connection.prepare (exception: Exception ("failed" )),
548
+ numFailedRequests: UpdateMachine .transientFailureCountNotifyThreshold + 1 );
497
549
});
498
550
499
551
test ('ZulipApiException' , () {
500
552
checkRetry (() => connection.prepare (httpStatus: 400 , json: {
501
- 'result' : 'error' , 'code' : 'BAD_REQUEST' , 'msg' : 'Bad request' }));
553
+ 'result' : 'error' , 'code' : 'BAD_REQUEST' , 'msg' : 'Bad request' }),
554
+ numFailedRequests: 1 );
502
555
});
503
556
504
557
test ('MalformedServerResponseException' , () {
505
- checkRetry (() => connection.prepare (httpStatus: 200 , body: 'nonsense' ));
558
+ checkRetry (() => connection.prepare (httpStatus: 200 , body: 'nonsense' ),
559
+ numFailedRequests: 1 );
506
560
});
507
561
});
508
562
});
0 commit comments