169 lines
4.6 KiB
Dart
169 lines
4.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:marianum_mobile/api/retry.dart';
|
|
import 'package:nextcloud/nextcloud.dart';
|
|
|
|
void main() {
|
|
// Instant delays so the tests never wait on real timers.
|
|
Duration noDelay(int _) => Duration.zero;
|
|
|
|
group('isTransientServerError', () {
|
|
test('5xx DynamiteApiException is transient', () {
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(500, {}, '')),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(502, {}, '')),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(503, {}, '')),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('4xx DynamiteApiException is not transient', () {
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(404, {}, '')),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(412, {}, '')),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
isTransientServerError(const DynamiteApiException(429, {}, '')),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
test('other error types are not transient', () {
|
|
expect(isTransientServerError(TimeoutException('slow')), isFalse);
|
|
expect(isTransientServerError(const SocketException('down')), isFalse);
|
|
expect(isTransientServerError(StateError('boom')), isFalse);
|
|
});
|
|
});
|
|
|
|
group('retryOnTransientError', () {
|
|
test('returns the result of a first-try success without retrying', () async {
|
|
var calls = 0;
|
|
final result = await retryOnTransientError(() async {
|
|
calls++;
|
|
return 'ok';
|
|
}, delayFor: noDelay);
|
|
|
|
expect(result, 'ok');
|
|
expect(calls, 1);
|
|
});
|
|
|
|
test('retries transient failures until an attempt succeeds', () async {
|
|
var calls = 0;
|
|
final result = await retryOnTransientError(() async {
|
|
calls++;
|
|
if (calls < 3) throw const DynamiteApiException(500, {}, '');
|
|
return 'ok';
|
|
}, delayFor: noDelay);
|
|
|
|
expect(result, 'ok');
|
|
expect(calls, 3);
|
|
});
|
|
|
|
test('rethrows the last error once maxAttempts is exhausted', () async {
|
|
var calls = 0;
|
|
await expectLater(
|
|
retryOnTransientError(() async {
|
|
calls++;
|
|
throw const DynamiteApiException(503, {}, '');
|
|
}, delayFor: noDelay),
|
|
throwsA(
|
|
isA<DynamiteApiException>().having(
|
|
(e) => e.statusCode,
|
|
'statusCode',
|
|
503,
|
|
),
|
|
),
|
|
);
|
|
expect(calls, 3);
|
|
});
|
|
|
|
test('rethrows non-transient errors immediately', () async {
|
|
var calls = 0;
|
|
await expectLater(
|
|
retryOnTransientError(() async {
|
|
calls++;
|
|
throw const DynamiteApiException(404, {}, '');
|
|
}, delayFor: noDelay),
|
|
throwsA(isA<DynamiteApiException>()),
|
|
);
|
|
expect(calls, 1);
|
|
});
|
|
|
|
test('rethrows unrelated exceptions immediately', () async {
|
|
var calls = 0;
|
|
await expectLater(
|
|
retryOnTransientError(() async {
|
|
calls++;
|
|
throw StateError('boom');
|
|
}, delayFor: noDelay),
|
|
throwsStateError,
|
|
);
|
|
expect(calls, 1);
|
|
});
|
|
|
|
test('reports upcoming attempts via onRetry and delays via delayFor', () async {
|
|
final retriesSeen = <(int, int)>[];
|
|
final delaysRequested = <int>[];
|
|
|
|
await expectLater(
|
|
retryOnTransientError(
|
|
() async => throw const DynamiteApiException(500, {}, ''),
|
|
delayFor: (retry) {
|
|
delaysRequested.add(retry);
|
|
return Duration.zero;
|
|
},
|
|
onRetry: (next, max) => retriesSeen.add((next, max)),
|
|
),
|
|
throwsA(isA<DynamiteApiException>()),
|
|
);
|
|
|
|
expect(retriesSeen, [(2, 3), (3, 3)]);
|
|
expect(delaysRequested, [1, 2]);
|
|
});
|
|
|
|
test('respects a custom shouldRetry predicate', () async {
|
|
var calls = 0;
|
|
final result = await retryOnTransientError(
|
|
() async {
|
|
calls++;
|
|
if (calls < 2) throw StateError('flaky');
|
|
return calls;
|
|
},
|
|
delayFor: noDelay,
|
|
shouldRetry: (e) => e is StateError,
|
|
);
|
|
|
|
expect(result, 2);
|
|
expect(calls, 2);
|
|
});
|
|
|
|
test('respects a custom maxAttempts', () async {
|
|
var calls = 0;
|
|
await expectLater(
|
|
retryOnTransientError(
|
|
() async {
|
|
calls++;
|
|
throw const DynamiteApiException(500, {}, '');
|
|
},
|
|
maxAttempts: 5,
|
|
delayFor: noDelay,
|
|
),
|
|
throwsA(isA<DynamiteApiException>()),
|
|
);
|
|
expect(calls, 5);
|
|
});
|
|
});
|
|
}
|