diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index f35c74c..efa20e7 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -616,6 +616,11 @@ Future _resolveDatabasePath() async { ); } +// These two functions are only called from unit tests (database_path_test.dart). +// They expose internals that cannot be reached via the public API. +Future resolveDatabasePathForTesting() => _resolveDatabasePath(); +void resetDatabasePathForTesting() => _dbPath = null; + LazyDatabase _openConnection() { return LazyDatabase(() async { final file = File(await _resolveDatabasePath()); diff --git a/test/unit/database_path_test.dart b/test/unit/database_path_test.dart index ad60e4c..69ddbfa 100644 --- a/test/unit/database_path_test.dart +++ b/test/unit/database_path_test.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:fake_async/fake_async.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; @@ -19,6 +22,30 @@ class _UnavailablePathProvider extends Fake } } +// Fake PathProviderPlatform that fails the first [failCount] calls, then +// returns a fixed path. Used to exercise the retry loop in +// _resolveDatabasePath() without waiting for real timers. +class _SucceedAfterNPathProvider extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + _SucceedAfterNPathProvider({required this.failCount}); + + final int failCount; + int _callCount = 0; + + @override + Future getApplicationSupportPath() async { + _callCount++; + if (_callCount <= failCount) { + throw PlatformException( + code: 'channel-error', + message: 'Simulated: path_provider channel not ready', + ); + } + return '/tmp/test_app_support'; + } +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -38,4 +65,69 @@ void main() { await expectLater(initDatabasePath(), completes); }, ); + + // Tests for _resolveDatabasePath() — the lazy retry path called on first DB + // access when initDatabasePath() already failed. fake_async lets us advance + // the back-off timers without waiting real-world milliseconds. + + test( + '_resolveDatabasePath retries and eventually succeeds after transient failures', + () { + resetDatabasePathForTesting(); + final prev = PathProviderPlatform.instance; + // Fail 3 times, succeed on the 4th call. The delays in + // _resolveDatabasePath are [200, 500, 1000, 2000, 4000] ms, so three + // failures cost 200+500+1000 = 1700 ms before the fourth attempt. + PathProviderPlatform.instance = _SucceedAfterNPathProvider(failCount: 3); + addTearDown(() { + PathProviderPlatform.instance = prev; + resetDatabasePathForTesting(); + }); + + fakeAsync((fake) { + String? result; + unawaited(resolveDatabasePathForTesting().then((r) => result = r)); + + // Advance fake time through the three back-off delays. + fake.elapse(const Duration(milliseconds: 200 + 500 + 1000 + 1)); + + expect(result, isNotNull); + expect(result, endsWith('sharedinbox.db')); + }); + }, + ); + + test( + '_resolveDatabasePath throws PlatformException after exhausting all retries', + () { + resetDatabasePathForTesting(); + final prev = PathProviderPlatform.instance; + PathProviderPlatform.instance = _UnavailablePathProvider(); + addTearDown(() { + PathProviderPlatform.instance = prev; + resetDatabasePathForTesting(); + }); + + fakeAsync((fake) { + Object? caughtError; + unawaited( + resolveDatabasePathForTesting().catchError((Object e) { + caughtError = e; + return ''; // ignored; satisfies the Future return type + }), + ); + + // Advance past all five back-off delays: 200+500+1000+2000+4000 ms. + fake.elapse( + const Duration(milliseconds: 200 + 500 + 1000 + 2000 + 4000 + 1), + ); + + expect(caughtError, isA()); + expect( + (caughtError! as PlatformException).message, + contains('cannot open database'), + ); + }); + }, + ); }