// Author: fruit.com.cn import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/download_license.dart'; import 'package:fruitshorts/core/data/models/download_policy.dart'; import 'package:fruitshorts/core/data/repositories/download_repository.dart'; import 'package:fruitshorts/core/data/services/download_controller.dart'; import 'package:fruitshorts/core/data/services/download_storage.dart'; void main() { test('should_mark_episode_downloaded_and_report_progress_when_download_finishes', () async { final repository = _RecordingRepository(_license('ep-1')); final storage = _FakeStorage(); final controller = DownloadController(repository, storage: storage); await controller.load(); final result = await controller.startDownload('ep-1'); expect(result.ok, isTrue); expect(controller.isDownloaded('ep-1'), isTrue); expect(controller.localPathFor('ep-1'), 'file:///tmp/ep-1.mp4'); expect(repository.statuses, containsAll(['DOWNLOADING', 'COMPLETED'])); }); test('should_keep_local_file_when_backend_delete_fails', () async { final repository = _RecordingRepository(_license('ep-1')); final storage = _FakeStorage(); final controller = DownloadController(repository, storage: storage); await controller.load(); await controller.startDownload('ep-1'); repository.failRemove = true; final ok = await controller.remove('ep-1'); expect(ok, isFalse); expect(controller.isDownloaded('ep-1'), isTrue); }); } DownloadLicense _license(String episodeId) => DownloadLicense( licenseId: 'license-$episodeId', dramaId: 'drama-1', dramaTitle: 'The Signal', episodeId: episodeId, episodeNo: 1, deviceId: 'device-1', quality: 'AUTO', playUrl: '/files/episode/ep-1.mp4?e=1&s=abc', licenseKey: 'key', status: 'ACTIVE', grantedAt: DateTime(2026, 10, 19), expireAt: DateTime(2026, 11, 18), taskStatus: 'QUEUED', progressPercent: 0, downloadedBytes: 0, totalBytes: 0, errorMsg: '', ); class _FakeStorage implements DownloadStorage { final Set files = {}; @override Future> restore() async => { for (final id in files) id: 'file:///tmp/$id.mp4', }; @override Future localPathFor(String episodeId) async => files.contains(episodeId) ? 'file:///tmp/$episodeId.mp4' : null; @override Future download({ required String episodeId, required String url, DownloadProgress? onProgress, }) async { onProgress?.call(50, 100); onProgress?.call(100, 100); files.add(episodeId); return DownloadedMedia( episodeId: episodeId, filePath: 'file:///tmp/$episodeId.mp4', bytes: 100, ); } @override Future delete(String episodeId) async => files.remove(episodeId); @override Future clear() async => files.clear(); } class _RecordingRepository implements DownloadRepository { _RecordingRepository(this.license); DownloadLicense license; bool failRemove = false; final List statuses = []; @override Future fetchPolicy() async => const DownloadPolicy( maxPerDevice: 20, licenseDays: 30, wifiOnly: false, maxQuality: 'AUTO', maxStorageBytes: 0, used: 1, remaining: 19, expirePolicyText: '30 days', ); @override Future> list() async => [license]; @override Future grant(String episodeId, {String? quality}) async => license; @override Future updateSettings({bool? wifiOnly, String? maxQuality}) async {} @override Future refreshUrl(String episodeId) async => license; @override Future reportProgress( String episodeId, { String? deviceId, String? status, int? progressPercent, int? downloadedBytes, int? totalBytes, String? errorMsg, }) async { if (status != null) statuses.add(status); return license; } @override Future remove(String episodeId) async { if (failRemove) throw StateError('remove failed'); } @override Future clear() async => 0; }