From d90c7ad7006e58654c25509c07ea36fcbe010283 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 22 Jul 2026 13:40:56 +0200 Subject: [PATCH] test: cover local backup validation errors and media restore counting Add coverage for invalidFile/incompatibleFormat/corrupted detection, importFailedNoMutation wrapping of unexpected repository failures, and missingMediaCount reflecting actual restore/checksum outcomes. Co-Authored-By: Claude Sonnet 5 --- test/application/use_cases_test.dart | 209 +++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index e166073..e0cf883 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -2549,6 +2549,190 @@ void main() { ); }); + test('Local backup codec rejects wrong kind and incompatible format', () { + final codec = LocalBackupCodec(); + + final wrongKind = utf8.encode( + jsonEncode({ + 'kind': 'other.app.backup', + 'formatVersion': 1, + 'minSupportedFormatVersion': 1, + 'exportedAt': DateTime.utc(2026, 7, 22).toIso8601String(), + 'data': { + 'mediaAssets': const [], + 'exercises': const [], + 'programs': const [], + 'workoutTemplates': const [], + 'workoutHistories': const [], + }, + 'mediaFiles': const [], + }), + ); + expect( + () => codec.decode(Uint8List.fromList(wrongKind)), + throwsA( + isA().having( + (error) => error.error, + 'error', + LocalBackupValidationError.invalidFile, + ), + ), + ); + + final missingVersion = utf8.encode( + jsonEncode({ + 'kind': 'gametime.localBackup', + 'exportedAt': DateTime.utc(2026, 7, 22).toIso8601String(), + 'data': { + 'mediaAssets': const [], + 'exercises': const [], + 'programs': const [], + 'workoutTemplates': const [], + 'workoutHistories': const [], + }, + 'mediaFiles': const [], + }), + ); + expect( + () => codec.decode(Uint8List.fromList(missingVersion)), + throwsA( + isA().having( + (error) => error.error, + 'error', + LocalBackupValidationError.incompatibleFormat, + ), + ), + ); + }); + + test('Local backup codec rejects malformed data collections', () { + final codec = LocalBackupCodec(); + + final malformedData = utf8.encode( + jsonEncode({ + 'kind': 'gametime.localBackup', + 'formatVersion': 1, + 'minSupportedFormatVersion': 1, + 'exportedAt': DateTime.utc(2026, 7, 22).toIso8601String(), + 'data': 'not-a-map', + 'mediaFiles': const [], + }), + ); + expect( + () => codec.decode(Uint8List.fromList(malformedData)), + throwsA( + isA().having( + (error) => error.error, + 'error', + LocalBackupValidationError.corrupted, + ), + ), + ); + }); + + test( + 'Data import use case wraps unexpected repository failure as importFailedNoMutation', + () async { + final repository = _ThrowingLocalDataBackupRepository(); + final useCase = DataImportUseCase( + repository: repository, + mediaStore: _FakeLocalBackupMediaStore(), + clock: _FakeClock(DateTime.utc(2026, 7, 22, 12)), + ); + final bytes = const LocalBackupCodec().encode(_backupSnapshot()); + + await expectLater( + useCase.importFrom(bytes, mode: LocalBackupImportMode.merge), + throwsA( + isA().having( + (error) => error.error, + 'error', + LocalBackupValidationError.importFailedNoMutation, + ), + ), + ); + }, + ); + + test( + 'Data import use case counts media assets not successfully restored', + () async { + final repository = _FakeLocalDataBackupRepository(); + final mediaStore = _FakeLocalBackupMediaStore( + restored: const { + 'media-restored': RestoredMediaFile( + mediaAssetId: 'media-restored', + localUri: 'file:///restored/media-restored.png', + sizeBytes: 4, + ), + }, + ); + final useCase = DataImportUseCase( + repository: repository, + mediaStore: mediaStore, + clock: _FakeClock(DateTime.utc(2026, 7, 22, 12)), + ); + final bytes = const LocalBackupCodec().encode( + _backupSnapshot( + mediaAssets: [ + _backupResource( + id: 'media-restored', + updatedAt: DateTime.utc(2026, 7, 22, 10), + payload: { + 'id': 'media-restored', + 'metadata': _backupMetadata( + 'media-restored', + DateTime.utc(2026, 7, 22, 10), + ), + 'localUri': 'file:///old/media-restored.png', + }, + ), + _backupResource( + id: 'media-corrupted', + updatedAt: DateTime.utc(2026, 7, 22, 10), + payload: { + 'id': 'media-corrupted', + 'metadata': _backupMetadata( + 'media-corrupted', + DateTime.utc(2026, 7, 22, 10), + ), + 'localUri': 'file:///old/media-corrupted.png', + }, + ), + ], + mediaFiles: const [ + EmbeddedBackupMediaFile( + mediaAssetId: 'media-restored', + role: 'original', + fileName: 'media-restored.png', + mimeType: 'image/png', + sizeBytes: 4, + sha256: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + base64: 'dGVzdA==', + ), + EmbeddedBackupMediaFile( + mediaAssetId: 'media-corrupted', + role: 'original', + fileName: 'media-corrupted.png', + mimeType: 'image/png', + sizeBytes: 4, + sha256: 'checksum-mismatch', + base64: 'dGVzdA==', + ), + ], + ), + ); + + final result = await useCase.importFrom( + bytes, + mode: LocalBackupImportMode.merge, + ); + + expect(result.missingMediaCount, 1); + }, + ); + test('Program use case duplicates deeply with copy name conflicts', () async { final programRepository = _FakeProgramRepository(); final templateRepository = _FakeWorkoutTemplateRepository(); @@ -3645,6 +3829,31 @@ final class _FakeLocalDataBackupRepository } } +final class _ThrowingLocalDataBackupRepository + implements LocalDataBackupRepository { + @override + Future applyImportSnapshot({ + required LocalDataExportSnapshot snapshot, + required LocalBackupImportMode mode, + required DateTime importedAt, + }) async { + throw Exception('transaction failed'); + } + + @override + Future hasAnyUserData() async => false; + + @override + Future hasOpenActiveWorkoutSession() async => false; + + @override + Future readExportSnapshot( + DateTime exportedAt, + ) async { + return _backupSnapshot(); + } +} + final class _FakeLocalBackupMediaStore implements LocalBackupMediaStore { _FakeLocalBackupMediaStore({this.files = const [], this.restored = const {}});