import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; class _Storage implements PersistentStorage { final failure = FileSystemException('fixture storage is read-only'); bool failWrites = true; int writes = 0; int? failWriteNumber; final saved = {}; @override Future initialize() async {} @override Future retrieveTaskRecord(String id) async => saved[id]; @override Future> retrieveAllTaskRecords() async => saved.values.toList(); @override Future storeTaskRecord(TaskRecord record) async { writes++; if (failWrites || writes == failWriteNumber) throw failure; saved[record.taskId] = record; } @override Future removePausedTask(String? id) async {} @override Future removeResumeData(String? id) async {} @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test( 'failed native status persistence fails flush and later writes still run', () async { final previous = debugDefaultTargetPlatformOverride; debugDefaultTargetPlatformOverride = TargetPlatform.android; addTearDown(() => debugDefaultTargetPlatformOverride = previous); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; const connectivity = MethodChannel( 'dev.fluttercommunity.plus/connectivity', ); const connectivityEvents = MethodChannel( 'dev.fluttercommunity.plus/connectivity_status', ); messenger.setMockMethodCallHandler(connectivity, (_) async => ['wifi']); messenger.setMockMethodCallHandler(connectivityEvents, (_) async => null); addTearDown(() { messenger.setMockMethodCallHandler(connectivity, null); messenger.setMockMethodCallHandler(connectivityEvents, null); }); final storage = _Storage(); final unhandled = []; final done = Completer(); Object? observed; Object? batchFailure; var retried = false; runZonedGuarded(() async { final downloader = FileDownloader(persistentStorage: storage); await downloader.ready; final engine = downloader.downloaderForTesting; engine.trackedGroups.add('fixture'); engine.groupStatusCallbacks['fixture'] = (_) {}; final task = DownloadTask( taskId: 'read-only-status', url: 'https://example.test/fixture.mp4', group: 'fixture', ); try { engine.processStatusUpdate( TaskStatusUpdate(task, TaskStatus.running), ); try { await engine.flushDatabaseUpdates(); } catch (error) { observed = error; } storage.failWrites = false; engine.processStatusUpdate( TaskStatusUpdate(task, TaskStatus.running), ); await engine.flushDatabaseUpdates(); retried = storage.saved[task.taskId]?.status == TaskStatus.running; // One background message can queue status plus progress. A later // successful write must not turn the whole flush into false success. storage.failWriteNumber = storage.writes + 1; engine.processStatusUpdate( TaskStatusUpdate(task, TaskStatus.enqueued), ); engine.processStatusUpdate( TaskStatusUpdate(task, TaskStatus.running), ); try { await engine.flushDatabaseUpdates(); } catch (error) { batchFailure = error; } await Future.delayed(Duration.zero); } finally { downloader.destroy(); done.complete(); } }, (error, stack) => unhandled.add(error)); await done.future; expect(observed, same(storage.failure)); expect(batchFailure, same(storage.failure)); expect(unhandled, isEmpty); expect(retried, isTrue); }, ); }