diff --git a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart index 245cc86a98..b77161049a 100644 --- a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart @@ -1,975 +1,541 @@ -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; + +import '../../medium/repository_context.dart'; void main() { - final now = DateTime(2024, 1, 15); - late Drift db; - late DriftLocalAssetRepository repository; + late MediumRepositoryContext ctx; + late DriftLocalAssetRepository sut; - setUp(() { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - repository = DriftLocalAssetRepository(db); + setUp(() async { + ctx = await MediumRepositoryContext.create(); + sut = DriftLocalAssetRepository(ctx.db); }); tearDown(() async { - await db.close(); + await ctx.dispose(); }); - Future insertLocalAsset({ - required String id, - String? checksum, - DateTime? createdAt, - AssetType type = AssetType.image, - bool isFavorite = false, - String? iCloudId, - DateTime? adjustmentTime, - double? latitude, - double? longitude, - }) async { - final created = createdAt ?? now; - await db - .into(db.localAssetEntity) - .insert( - LocalAssetEntityCompanion.insert( - id: id, - name: 'asset_$id.jpg', - checksum: Value(checksum), - type: type, - createdAt: Value(created), - updatedAt: Value(created), - isFavorite: Value(isFavorite), - iCloudId: Value(iCloudId), - adjustmentTime: Value(adjustmentTime), - latitude: Value(latitude), - longitude: Value(longitude), - ), - ); - } - - Future insertRemoteAsset({ - required String id, - required String checksum, - required String ownerId, - DateTime? deletedAt, - }) async { - await db - .into(db.remoteAssetEntity) - .insert( - RemoteAssetEntityCompanion.insert( - id: id, - name: 'remote_$id.jpg', - checksum: checksum, - type: AssetType.image, - createdAt: Value(now), - updatedAt: Value(now), - ownerId: ownerId, - visibility: AssetVisibility.timeline, - deletedAt: Value(deletedAt), - ), - ); - } - - Future insertRemoteAssetCloudId({ - required String assetId, - required String? cloudId, - DateTime? createdAt, - DateTime? adjustmentTime, - double? latitude, - double? longitude, - }) async { - await db - .into(db.remoteAssetCloudIdEntity) - .insert( - RemoteAssetCloudIdEntityCompanion.insert( - assetId: assetId, - cloudId: Value(cloudId), - createdAt: Value(createdAt), - adjustmentTime: Value(adjustmentTime), - latitude: Value(latitude), - longitude: Value(longitude), - ), - ); - } - - Future insertUser(String id, String email) async { - await db.into(db.userEntity).insert(UserEntityCompanion.insert(id: id, email: email, name: email)); - } - group('getRemovalCandidates', () { - final userId = 'user-123'; - final otherUserId = 'user-456'; - final cutoffDate = DateTime(2024, 1, 10); - final beforeCutoff = DateTime(2024, 1, 5); - final afterCutoff = DateTime(2024, 1, 12); + final cutoffDate = DateTime(2024, 1, 1); + final beforeCutoff = DateTime(2023, 12, 31); + final afterCutoff = DateTime(2024, 1, 2); + late UserEntityData user; - setUp(() async { - await insertUser(userId, 'user@test.com'); - await insertUser(otherUserId, 'other@test.com'); + setUp(() { + user = ctx.user; }); - Future insertLocalAlbum({required String id, required String name, required bool isIosSharedAlbum}) async { - await db - .into(db.localAlbumEntity) - .insert( - LocalAlbumEntityCompanion.insert( - id: id, - name: name, - updatedAt: Value(now), - backupSelection: BackupSelection.none, - isIosSharedAlbum: Value(isIosSharedAlbum), - ), - ); - } - - Future insertLocalAlbumAsset({required String albumId, required String assetId}) async { - await db - .into(db.localAlbumAssetEntity) - .insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); - } - test('returns only assets that match all criteria', () async { + final otherUser = await ctx.insertUser(); + // Asset 1: Should be included - backed up, before cutoff, correct owner, not deleted, not favorite - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final includedAsset = await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); // Asset 2: Should NOT be included - not backed up (no remote asset) - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); + await ctx.insertLocalAsset(createdAt: beforeCutoff); // Asset 3: Should NOT be included - after cutoff date - await insertLocalAsset( - id: 'local-3', - checksum: 'checksum-3', - createdAt: afterCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId); + await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); // Asset 4: Should NOT be included - different owner - await insertLocalAsset( - id: 'local-4', - checksum: 'checksum-4', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-4', checksum: 'checksum-4', ownerId: otherUserId); + final otherRemoteAsset = await ctx.insertRemoteAsset(ownerId: otherUser.id); + await ctx.insertLocalAsset(checksum: otherRemoteAsset.checksum, createdAt: beforeCutoff); // Asset 5: Should NOT be included - remote asset is deleted - await insertLocalAsset( - id: 'local-5', - checksum: 'checksum-5', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-5', checksum: 'checksum-5', ownerId: userId, deletedAt: now); + final deletedAsset = await ctx.insertRemoteAsset(ownerId: user.id, deletedAt: DateTime(2024, 1, 1)); + await ctx.insertLocalAsset(checksum: deletedAsset.checksum, createdAt: beforeCutoff); // Asset 6: Should NOT be included - is favorite (when keepFavorites=true) - await insertLocalAsset( - id: 'local-6', - checksum: 'checksum-6', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: true, - ); - await insertRemoteAsset(id: 'remote-6', checksum: 'checksum-6', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + final favoriteAsset = await ctx.insertRemoteAsset(ownerId: user.id, isFavorite: true); + await ctx.insertLocalAsset(checksum: favoriteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepFavorites: true); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-1'); + expect(result.assets.first.id, includedAsset.id); }); test('includes favorites when keepFavorites is false', () async { - await insertLocalAsset( - id: 'local-favorite', - checksum: 'checksum-fav', + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final favoriteAsset = await ctx.insertLocalAsset( + checksum: remoteAsset.checksum, createdAt: beforeCutoff, - type: AssetType.image, isFavorite: true, ); - await insertRemoteAsset(id: 'remote-favorite', checksum: 'checksum-fav', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepFavorites: false); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-favorite'); - expect(result.assets[0].isFavorite, true); + expect(result.assets.first.id, favoriteAsset.id); + expect(result.assets.first.isFavorite, true); }); test('keepMediaType photosOnly returns only videos for deletion', () async { + final photoAsset = await ctx.insertRemoteAsset(ownerId: user.id); // Photo - should be kept - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + await ctx.insertLocalAsset(checksum: photoAsset.checksum, createdAt: beforeCutoff); + final videoRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); // Video - should be deleted - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoLocalAsset = await ctx.insertLocalAsset( + checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepMediaType: AssetKeepType.photosOnly); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-video'); - expect(result.assets[0].type, AssetType.video); + expect(result.assets.first.id, videoLocalAsset.id); + expect(result.assets.first.type, AssetType.video); }); test('keepMediaType videosOnly returns only photos for deletion', () async { // Photo - should be deleted - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + final photoRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final photoAsset = await ctx.insertLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); // Video - should be kept - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', - createdAt: beforeCutoff, - type: AssetType.video, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.videosOnly); + final videoRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + await ctx.insertLocalAsset(checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepMediaType: AssetKeepType.videosOnly); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-photo'); - expect(result.assets[0].type, AssetType.image); + expect(result.assets.first.id, photoAsset.id); + expect(result.assets.first.type, AssetType.image); }); test('returns both photos and videos with keepMediaType.all', () async { // Photo - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + final photoRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final photoAsset = await ctx.insertLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); // Video - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final videoAsset = await ctx.insertLocalAsset( + checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.none); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepMediaType: AssetKeepType.none); expect(result.assets.length, 2); final ids = result.assets.map((a) => a.id).toSet(); - expect(ids, containsAll(['local-photo', 'local-video'])); + expect(ids, containsAll([photoAsset.id, videoAsset.id])); }); test('excludes assets in iOS shared albums', () async { // Regular album - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final regularAlbum = await ctx.insertLocalAlbum(); // iOS shared album - await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true); + final sharedAlbum = await ctx.insertLocalAlbum(isIosSharedAlbum: true); // Asset in regular album (should be included) - await insertLocalAsset( - id: 'local-regular', - checksum: 'checksum-regular', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-regular', checksum: 'checksum-regular', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-regular'); + final regularRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final regularAsset = await ctx.insertLocalAsset(checksum: regularRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: regularAlbum.id, assetId: regularAsset.id); // Asset in iOS shared album (should be excluded) - await insertLocalAsset( - id: 'local-shared', - checksum: 'checksum-shared', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-shared', checksum: 'checksum-shared', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-shared'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final sharedRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final sharedAsset = await ctx.insertLocalAsset(checksum: sharedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: sharedAlbum.id, assetId: sharedAsset.id); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-regular'); + expect(result.assets.first.id, regularAsset.id); }); test('includes assets at exact cutoff date', () async { - await insertLocalAsset( - id: 'local-exact', - checksum: 'checksum-exact', - createdAt: cutoffDate, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-exact', checksum: 'checksum-exact', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final localAsset = await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: cutoffDate); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-exact'); + expect(result.assets.first.id, localAsset.id); }); test('returns empty list when no assets match criteria', () async { // Only assets after cutoff - await insertLocalAsset( - id: 'local-after', - checksum: 'checksum-after', - createdAt: afterCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-after', checksum: 'checksum-after', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets, isEmpty); }); test('handles multiple assets with same checksum', () async { // Two local assets with same checksum (edge case, but should handle it) - await insertLocalAsset( - id: 'local-dup1', - checksum: 'checksum-dup', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertLocalAsset( - id: 'local-dup2', - checksum: 'checksum-dup', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-dup', checksum: 'checksum-dup', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets.length, 2); - expect(result.assets.map((a) => a.checksum).toSet(), equals({'checksum-dup'})); + expect(result.assets.map((a) => a.checksum).toSet(), equals({remoteAsset.checksum})); }); test('includes assets not in any album', () async { // Asset not in any album should be included - await insertLocalAsset( - id: 'local-no-album', - checksum: 'checksum-no-album', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final localAsset = await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-no-album'); + expect(result.assets.first.id, localAsset.id); }); test('excludes asset that is in both regular and iOS shared album', () async { // Regular album - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final regularAlbum = await ctx.insertLocalAlbum(); // iOS shared album - await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true); + final sharedAlbum = await ctx.insertLocalAlbum(isIosSharedAlbum: true); // Asset in BOTH albums - should be excluded because it's in an iOS shared album - await insertLocalAsset( - id: 'local-both', - checksum: 'checksum-both', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-both'); - await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-both'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final localAsset = await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: regularAlbum.id, assetId: localAsset.id); + await ctx.insertLocalAlbumAsset(albumId: sharedAlbum.id, assetId: localAsset.id); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets, isEmpty); }); test('excludes assets with null checksum (not backed up)', () async { // Asset with null checksum cannot be matched to remote asset - await db - .into(db.localAssetEntity) - .insert( - LocalAssetEntityCompanion.insert( - id: 'local-null-checksum', - name: 'asset_null.jpg', - checksum: const Value.absent(), // null checksum - type: AssetType.image, - createdAt: Value(beforeCutoff), - updatedAt: Value(beforeCutoff), - isFavorite: const Value(false), - ), - ); - - final result = await repository.getRemovalCandidates(userId, cutoffDate); + await ctx.insertLocalAsset(checksumOption: const Option.none()); + final result = await sut.getRemovalCandidates(user.id, cutoffDate); expect(result.assets, isEmpty); }); test('excludes assets in user-excluded albums', () async { // Create two regular albums - await insertLocalAlbum(id: 'album-include', name: 'Include Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-exclude', name: 'Exclude Album', isIosSharedAlbum: false); + final includeAlbum = await ctx.insertLocalAlbum(); + final excludeAlbum = await ctx.insertLocalAlbum(); // Asset in included album - should be included - await insertLocalAsset( - id: 'local-in-included', - checksum: 'checksum-included', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-included', checksum: 'checksum-included', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-include', assetId: 'local-in-included'); + final includedRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final includedAsset = await ctx.insertLocalAsset(checksum: includedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: includeAlbum.id, assetId: includedAsset.id); // Asset in excluded album - should NOT be included - await insertLocalAsset( - id: 'local-in-excluded', - checksum: 'checksum-excluded', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-excluded', checksum: 'checksum-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-exclude', assetId: 'local-in-excluded'); + final excludedRemoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final excludedAsset = await ctx.insertLocalAsset(checksum: excludedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: excludeAlbum.id, assetId: excludedAsset.id); - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-exclude'}); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepAlbumIds: {excludeAlbum.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-in-included'); + expect(result.assets.first.id, includedAsset.id); }); test('excludes assets that are in any of multiple excluded albums', () async { // Create multiple albums - await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-2', name: 'Album 2', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-3', name: 'Album 3', isIosSharedAlbum: false); + final album1 = await ctx.insertLocalAlbum(); + final album2 = await ctx.insertLocalAlbum(); + final album3 = await ctx.insertLocalAlbum(); // Asset in album-1 (excluded) - should NOT be included - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + final remote1 = await ctx.insertRemoteAsset(ownerId: user.id); + final local1 = await ctx.insertLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: album1.id, assetId: local1.id); // Asset in album-2 (excluded) - should NOT be included - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-2', assetId: 'local-2'); + final remote2 = await ctx.insertRemoteAsset(ownerId: user.id); + final local2 = await ctx.insertLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: album2.id, assetId: local2.id); // Asset in album-3 (not excluded) - should be included - await insertLocalAsset( - id: 'local-3', - checksum: 'checksum-3', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-3', assetId: 'local-3'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-1', 'album-2'}); + final remote3 = await ctx.insertRemoteAsset(ownerId: user.id); + final local3 = await ctx.insertLocalAsset(checksum: remote3.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: album3.id, assetId: local3.id); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepAlbumIds: {album1.id, album2.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-3'); + expect(result.assets.first.id, local3.id); }); test('excludes asset that is in both excluded and non-excluded album', () async { - await insertLocalAlbum(id: 'album-included', name: 'Included Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + final includedAlbum = await ctx.insertLocalAlbum(); + final excludedAlbum = await ctx.insertLocalAlbum(); // Asset in BOTH albums - should be excluded because it's in an excluded album - await insertLocalAsset( - id: 'local-both', - checksum: 'checksum-both', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-included', assetId: 'local-both'); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-both'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final localAsset = await ctx.insertLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: includedAlbum.id, assetId: localAsset.id); + await ctx.insertLocalAlbumAsset(albumId: excludedAlbum.id, assetId: localAsset.id); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepAlbumIds: {excludedAlbum.id}); expect(result.assets, isEmpty); }); test('includes all assets when excludedAlbumIds is empty', () async { - await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); + final album1 = await ctx.insertLocalAlbum(); - await insertLocalAsset( - id: 'local-1', - checksum: 'checksum-1', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + final remote1 = await ctx.insertRemoteAsset(ownerId: user.id); + final local1 = await ctx.insertLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: album1.id, assetId: local1.id); - await insertLocalAsset( - id: 'local-2', - checksum: 'checksum-2', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); + final remote2 = await ctx.insertRemoteAsset(ownerId: user.id); + await ctx.insertLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); // Empty excludedAlbumIds should include all eligible assets - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); - + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepAlbumIds: {}); expect(result.assets.length, 2); }); test('excludes asset not in any album when album is excluded', () async { - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + final excludedAlbum = await ctx.insertLocalAlbum(); // Asset NOT in any album - should be included - await insertLocalAsset( - id: 'local-no-album', - checksum: 'checksum-no-album', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); + final noAlbumRemote = await ctx.insertRemoteAsset(ownerId: user.id); + final noAlbumAsset = await ctx.insertLocalAsset(checksum: noAlbumRemote.checksum, createdAt: beforeCutoff); // Asset in excluded album - should NOT be included - await insertLocalAsset( - id: 'local-in-excluded', - checksum: 'checksum-in-excluded', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-in-excluded', checksum: 'checksum-in-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-in-excluded'); - - final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + final excludedRemote = await ctx.insertRemoteAsset(ownerId: user.id); + final excludedAsset = await ctx.insertLocalAsset(checksum: excludedRemote.checksum, createdAt: beforeCutoff); + await ctx.insertLocalAlbumAsset(albumId: excludedAlbum.id, assetId: excludedAsset.id); + final result = await sut.getRemovalCandidates(user.id, cutoffDate, keepAlbumIds: {excludedAlbum.id}); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-no-album'); + expect(result.assets.first.id, noAlbumAsset.id); }); test('combines excludedAlbumIds with keepMediaType correctly', () async { - await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); - await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + final excludedAlbum = await ctx.insertLocalAlbum(); + final regularAlbum = await ctx.insertLocalAlbum(); // Photo in excluded album - should NOT be included (album excluded) - await insertLocalAsset( - id: 'local-photo-excluded', - checksum: 'checksum-photo-excluded', + final photoExcludedRemote = await ctx.insertRemoteAsset(ownerId: user.id); + final photoExcludedAsset = await ctx.insertLocalAsset( + checksum: photoExcludedRemote.checksum, createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-photo-excluded', checksum: 'checksum-photo-excluded', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-photo-excluded'); + await ctx.insertLocalAlbumAsset(albumId: excludedAlbum.id, assetId: photoExcludedAsset.id); // Video in regular album - should be included (keepMediaType photosOnly = delete videos) - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', + final videoRemote = await ctx.insertRemoteAsset(ownerId: user.id); + final videoAsset = await ctx.insertLocalAsset( + checksum: videoRemote.checksum, createdAt: beforeCutoff, type: AssetType.video, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-video'); + await ctx.insertLocalAlbumAsset(albumId: regularAlbum.id, assetId: videoAsset.id); // Photo in regular album - should NOT be included (keepMediaType photosOnly = keep photos) - await insertLocalAsset( - id: 'local-photo-regular', - checksum: 'checksum-photo-regular', + final photoRegularRemote = await ctx.insertRemoteAsset(ownerId: user.id); + final photoRegularAsset = await ctx.insertLocalAsset( + checksum: photoRegularRemote.checksum, createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, ); - await insertRemoteAsset(id: 'remote-photo-regular', checksum: 'checksum-photo-regular', ownerId: userId); - await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-photo-regular'); + await ctx.insertLocalAlbumAsset(albumId: regularAlbum.id, assetId: photoRegularAsset.id); - final result = await repository.getRemovalCandidates( - userId, + final result = await sut.getRemovalCandidates( + user.id, cutoffDate, keepMediaType: AssetKeepType.photosOnly, - keepAlbumIds: {'album-excluded'}, + keepAlbumIds: {excludedAlbum.id}, ); expect(result.assets.length, 1); - expect(result.assets[0].id, 'local-video'); + expect(result.assets.first.id, videoAsset.id); }); }); group('reconcileHashesFromCloudId', () { - final userId = 'user-123'; - final createdAt = DateTime(2024, 1, 10); - final adjustmentTime = DateTime(2024, 1, 11); - const latitude = 37.7749; - const longitude = -122.4194; + late UserEntityData user; - setUp(() async { - await insertUser(userId, 'user@test.com'); + setUp(() { + user = ctx.user; }); test('updates local asset checksum when all metadata matches', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final remoteCloudAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'hash-abc123'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); }); test('does not update when local asset already has checksum', () async { - await insertLocalAsset( - id: 'local-1', - checksum: 'existing-checksum', - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final remoteCloudAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id); + + final localAsset = await ctx.insertLocalAsset( + checksum: 'existing', + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'existing-checksum'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, 'existing'); }); test('does not update when adjustment_time does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId( + id: remoteAsset.id, adjustmentTime: DateTime(2024, 1, 12), - latitude: latitude, - longitude: longitude, + ); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: DateTime(2026, 1, 12), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when latitude does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id, latitude: const Option.none()); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, latitude: 40.7128, - longitude: longitude, + longitude: cloudIdAsset.longitude, ); - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when longitude does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id, longitude: (-74.006).toOption()); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: 0.0, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: -74.0060, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when createdAt does not match', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id, createdAt: DateTime(2024, 1, 5)); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: DateTime(2024, 6, 1), + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: DateTime(2024, 1, 5), - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when iCloudId is null', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), iCloudId: null, - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('does not update when cloudId does not match iCloudId', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: 'different-cloud-id', + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-456', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('handles partial null metadata fields matching correctly', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: null, - latitude: latitude, - longitude: longitude, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId( + id: remoteAsset.id, + adjustmentTimeOption: const Option.none(), + ); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTimeOption: const Option.none(), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: null, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); - expect(updated?.checksum, 'hash-abc123'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); }); test('does not update when one has null and other has value', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, + final remoteAsset = await ctx.insertRemoteAsset(ownerId: user.id); + final cloudIdAsset = await ctx.insertRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.insertLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, latitude: null, - longitude: longitude, + longitude: cloudIdAsset.longitude, ); - await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); - - await insertRemoteAssetCloudId( - assetId: 'remote-1', - cloudId: 'cloud-123', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); - - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); test('handles no matching assets gracefully', () async { - await insertLocalAsset( - id: 'local-1', - checksum: null, - iCloudId: 'cloud-999', - createdAt: createdAt, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - ); + final localAsset = await ctx.insertLocalAsset(checksumOption: const Option.none(), iCloudId: 'cloud-no-match'); - await repository.reconcileHashesFromCloudId(); - - final updated = await repository.getById('local-1'); + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); expect(updated?.checksum, isNull); }); }); diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart new file mode 100644 index 0000000000..6e8bb3df98 --- /dev/null +++ b/mobile/test/medium/repository_context.dart @@ -0,0 +1,220 @@ +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:uuid/uuid.dart'; + +class MediumRepositoryContext { + final Drift db; + late UserEntityData user; + final Random _random = Random(); + + MediumRepositoryContext._() + : db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + + static Future create() async { + final ctx = MediumRepositoryContext._(); + await ctx.setup(); + return ctx; + } + + Future setup() async { + user = await insertUser(); + } + + Future dispose() async { + await db.close(); + } + + static Value _resolveUndefined(T? plain, Option? option, T fallback) { + if (plain != null) { + return Value(plain); + } + + return _resolveOption(option, fallback); + } + + static Value _resolveOption(Option? option, T fallback) { + if (option != null) { + return option.fold(Value.new, Value.absent); + } + + return Value(fallback); + } + + Future insertUser({ + String? id, + String? email, + AvatarColor? avatarColor, + DateTime? profileChangedAt, + bool? hasProfileImage, + }) async { + id = id ?? const Uuid().v4(); + return await db + .into(db.userEntity) + .insertReturning( + UserEntityCompanion( + id: Value(id), + email: Value(email ?? '$id@test.com'), + name: Value(email ?? 'user_$id'), + avatarColor: Value(avatarColor ?? AvatarColor.values[_random.nextInt(AvatarColor.values.length)]), + profileChangedAt: Value(profileChangedAt ?? DateTime.now()), + hasProfileImage: Value(hasProfileImage ?? false), + ), + ); + } + + Future insertLocalAsset({ + String? id, + String? name, + String? checksum, + Option? checksumOption, + DateTime? createdAt, + AssetType? type, + bool? isFavorite, + String? iCloudId, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + double? latitude, + double? longitude, + int? width, + int? height, + int? durationInSeconds, + int? orientation, + DateTime? updatedAt, + }) async { + id = id ?? const Uuid().v4(); + return db + .into(db.localAssetEntity) + .insertReturning( + LocalAssetEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_$id.jpg'), + height: Value(height ?? _random.nextInt(1000)), + width: Value(width ?? _random.nextInt(1000)), + durationInSeconds: Value(durationInSeconds ?? 0), + orientation: Value(orientation ?? 0), + updatedAt: Value(updatedAt ?? DateTime.now()), + checksum: _resolveUndefined(checksum, checksumOption, const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + type: Value(type ?? AssetType.image), + isFavorite: Value(isFavorite ?? false), + iCloudId: Value(iCloudId ?? const Uuid().v4()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: Value(latitude ?? _random.nextDouble() * 180 - 90), + longitude: Value(longitude ?? _random.nextDouble() * 360 - 180), + ), + ); + } + + Future insertRemoteAsset({ + String? id, + String? checksum, + String? ownerId, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + AssetType? type, + AssetVisibility? visibility, + int? durationInSeconds, + int? width, + int? height, + bool? isFavorite, + bool? isEdited, + String? livePhotoVideoId, + String? stackId, + String? thumbHash, + String? libraryId, + }) async { + id = id ?? const Uuid().v4(); + createdAt = createdAt ?? DateTime.now(); + return db + .into(db.remoteAssetEntity) + .insertReturning( + RemoteAssetEntityCompanion( + id: Value(id), + name: Value('remote_$id.jpg'), + checksum: Value(checksum ?? const Uuid().v4()), + type: Value(type ?? AssetType.image), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt ?? DateTime.now()), + ownerId: Value(ownerId ?? const Uuid().v4()), + visibility: Value(visibility ?? AssetVisibility.timeline), + deletedAt: Value(deletedAt), + durationInSeconds: Value(durationInSeconds ?? 0), + width: Value(width ?? _random.nextInt(1000)), + height: Value(height ?? _random.nextInt(1000)), + isFavorite: Value(isFavorite ?? false), + isEdited: Value(isEdited ?? false), + livePhotoVideoId: Value(livePhotoVideoId), + stackId: Value(stackId), + localDateTime: Value(createdAt.toLocal()), + thumbHash: Value(thumbHash ?? const Uuid().v4()), + libraryId: Value(libraryId ?? const Uuid().v4()), + ), + ); + } + + Future insertRemoteAssetCloudId({ + String? id, + String? cloudId, + DateTime? createdAt, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + Option? latitude, + Option? longitude, + }) { + return db + .into(db.remoteAssetCloudIdEntity) + .insertReturning( + RemoteAssetCloudIdEntityCompanion( + assetId: Value(id ?? const Uuid().v4()), + cloudId: Value(cloudId ?? const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: _resolveOption(latitude, _random.nextDouble() * 180 - 90), + longitude: _resolveOption(longitude, _random.nextDouble() * 360 - 180), + ), + ); + } + + Future insertLocalAlbum({ + String? id, + String? name, + DateTime? updatedAt, + BackupSelection? backupSelection, + bool? isIosSharedAlbum, + String? linkedRemoteAlbumId, + }) { + id = id ?? const Uuid().v4(); + return db + .into(db.localAlbumEntity) + .insertReturning( + LocalAlbumEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_album_$id'), + updatedAt: Value(updatedAt ?? DateTime.now()), + backupSelection: Value(backupSelection ?? BackupSelection.none), + isIosSharedAlbum: Value(isIosSharedAlbum ?? false), + linkedRemoteAlbumId: Value(linkedRemoteAlbumId), + ), + ); + } + + Future insertLocalAlbumAsset({required String albumId, required String assetId}) { + return db + .into(db.localAlbumAssetEntity) + .insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); + } +}