diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 5a197dc88a..e66eccbdc0 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -280,8 +280,7 @@ class SyncStreamService { return; // SyncCompleteV1 is used to signal the completion of the sync process. Cleanup stale assets and signal completion case SyncEntityType.syncCompleteV1: - return; - // return _syncStreamRepository.pruneAssets(); + return _syncStreamRepository.pruneAssets(); // Request to reset the client state. Clear everything related to remote entities case SyncEntityType.syncResetV1: return _syncStreamRepository.reset(); diff --git a/mobile/test/medium/repositories/sync_stream_repository_test.dart b/mobile/test/medium/repositories/sync_stream_repository_test.dart new file mode 100644 index 0000000000..79b343ce50 --- /dev/null +++ b/mobile/test/medium/repositories/sync_stream_repository_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late SyncStreamRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = SyncStreamRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('pruneAssets', () { + test('deletes foreign orphans and keeps owned, partner, and in-album assets', () async { + final me = await ctx.newUser(); + final partner = await ctx.newUser(); + final stranger = await ctx.newUser(); + await ctx.newAuthUser(id: me.id); + await ctx.newPartner(sharedById: partner.id, sharedWithId: me.id); + + final own = await ctx.newRemoteAsset(ownerId: me.id); + final fromPartner = await ctx.newRemoteAsset(ownerId: partner.id); + final shared = await ctx.newRemoteAsset(ownerId: stranger.id); + await ctx.newRemoteAsset(ownerId: stranger.id); + + final album = await ctx.newRemoteAlbum(ownerId: me.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: shared.id); + + await sut.pruneAssets(); + + final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get(); + expect(remaining.map((a) => a.id), unorderedEquals([own.id, fromPartner.id, shared.id])); + }); + + test('does nothing when there is no authenticated user', () async { + final stranger = await ctx.newUser(); + final orphan = await ctx.newRemoteAsset(ownerId: stranger.id); + + await sut.pruneAssets(); + + final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get(); + expect(remaining.map((a) => a.id), [orphan.id]); + }); + + test('prunes every stale foreign asset in a large data set', () async { + final stranger = await ctx.newUser(); + await ctx.newAuthUser(); + for (var i = 0; i < 600; i++) { + await ctx.newRemoteAsset(ownerId: stranger.id); + } + + await sut.pruneAssets(); + + final remaining = await ctx.db.select(ctx.db.remoteAssetEntity).get(); + expect(remaining, isEmpty); + }); + }); +} diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart index 00390d455f..f41a46e0f4 100644 --- a/mobile/test/medium/repository_context.dart +++ b/mobile/test/medium/repository_context.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.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'; @@ -72,6 +73,20 @@ class MediumRepositoryContext { ); } + Future newAuthUser({String? id, String? email, AvatarColor? avatarColor}) async { + id ??= TestUtils.uuid(); + return await db + .into(db.authUserEntity) + .insertReturning( + AuthUserEntityCompanion( + id: .new(id), + email: .new(email ?? '$id@test.com'), + name: .new('user_$id'), + avatarColor: .new(avatarColor ?? TestUtils.randElement(AvatarColor.values)), + ), + ); + } + Future newPartner({required String sharedById, required String sharedWithId, bool? inTimeline}) { return db .into(db.partnerEntity)