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().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()), ); 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 = []; 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()), ); 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()), ); expect(calls, 5); }); }); }