mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 10:12:33 -04:00 
			
		
		
		
	* feat: use drift for logging * fix: tests * feat: use the truncate limit from constants.ts as default * chore: move setupAll to top level and restructure * chore: code review changes * fix: inherits * feat: raise log line limit to 2000 * limit getAll to 250 lines * delete DLog and make LogRepository not a singleton * fix: drift build settings and `make migration` * fix: tests * remove sensitive log --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
		
			
				
	
	
		
			286 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			286 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:drift/drift.dart';
 | |
| import 'package:drift/native.dart';
 | |
| import 'package:flutter/widgets.dart';
 | |
| import 'package:flutter_test/flutter_test.dart';
 | |
| import 'package:immich_mobile/constants/enums.dart';
 | |
| import 'package:immich_mobile/domain/models/store.model.dart';
 | |
| import 'package:immich_mobile/domain/models/user.model.dart';
 | |
| import 'package:immich_mobile/domain/services/log.service.dart';
 | |
| import 'package:immich_mobile/domain/services/store.service.dart';
 | |
| import 'package:immich_mobile/entities/asset.entity.dart';
 | |
| import 'package:immich_mobile/entities/etag.entity.dart';
 | |
| import 'package:immich_mobile/entities/store.entity.dart';
 | |
| import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
 | |
| import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
 | |
| import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
 | |
| import 'package:immich_mobile/repositories/asset.repository.dart';
 | |
| import 'package:immich_mobile/repositories/partner_api.repository.dart';
 | |
| import 'package:immich_mobile/services/sync.service.dart';
 | |
| import 'package:mocktail/mocktail.dart';
 | |
| 
 | |
| import '../../domain/service.mock.dart';
 | |
| import '../../fixtures/asset.stub.dart';
 | |
| import '../../infrastructure/repository.mock.dart';
 | |
| import '../../repository.mocks.dart';
 | |
| import '../../service.mocks.dart';
 | |
| import '../../test_utils.dart';
 | |
| 
 | |
| void main() {
 | |
|   int assetIdCounter = 0;
 | |
|   Asset makeAsset({
 | |
|     required String checksum,
 | |
|     String? localId,
 | |
|     String? remoteId,
 | |
|     int ownerId = 590700560494856554, // hash of "1"
 | |
|   }) {
 | |
|     final DateTime date = DateTime(2000);
 | |
|     return Asset(
 | |
|       id: assetIdCounter++,
 | |
|       checksum: checksum,
 | |
|       localId: localId,
 | |
|       remoteId: remoteId,
 | |
|       ownerId: ownerId,
 | |
|       fileCreatedAt: date,
 | |
|       fileModifiedAt: date,
 | |
|       updatedAt: date,
 | |
|       durationInSeconds: 0,
 | |
|       type: AssetType.image,
 | |
|       fileName: localId ?? remoteId ?? "",
 | |
|       isFavorite: false,
 | |
|       isArchived: false,
 | |
|       isTrashed: false,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   final owner = UserDto(
 | |
|     id: "1",
 | |
|     updatedAt: DateTime.now(),
 | |
|     email: "a@b.c",
 | |
|     name: "first last",
 | |
|     isAdmin: false,
 | |
|     profileChangedAt: DateTime.now(),
 | |
|   );
 | |
| 
 | |
|   setUpAll(() async {
 | |
|     final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
 | |
|     final LogRepository logRepository = LogRepository(loggerDb);
 | |
| 
 | |
|     WidgetsFlutterBinding.ensureInitialized();
 | |
|     final db = await TestUtils.initIsar();
 | |
| 
 | |
|     db.writeTxnSync(() => db.clearSync());
 | |
|     await StoreService.init(storeRepository: IsarStoreRepository(db));
 | |
|     await Store.put(StoreKey.currentUser, owner);
 | |
|     await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db));
 | |
|   });
 | |
| 
 | |
|   group('Test SyncService grouped', () {
 | |
|     final MockHashService hs = MockHashService();
 | |
|     final MockEntityService entityService = MockEntityService();
 | |
|     final MockAlbumRepository albumRepository = MockAlbumRepository();
 | |
|     final MockAssetRepository assetRepository = MockAssetRepository();
 | |
|     final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
 | |
|     final MockIsarUserRepository userRepository = MockIsarUserRepository();
 | |
|     final MockETagRepository eTagRepository = MockETagRepository();
 | |
|     final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository();
 | |
|     final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
 | |
|     final MockAppSettingService appSettingService = MockAppSettingService();
 | |
|     final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository();
 | |
|     final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository();
 | |
|     final MockUserApiRepository userApiRepository = MockUserApiRepository();
 | |
|     final MockPartnerRepository partnerRepository = MockPartnerRepository();
 | |
|     final MockUserService userService = MockUserService();
 | |
| 
 | |
|     final owner = UserDto(
 | |
|       id: "1",
 | |
|       updatedAt: DateTime.now(),
 | |
|       email: "a@b.c",
 | |
|       name: "first last",
 | |
|       isAdmin: false,
 | |
|       profileChangedAt: DateTime(2021),
 | |
|     );
 | |
| 
 | |
|     late SyncService s;
 | |
| 
 | |
|     final List<Asset> initialAssets = [
 | |
|       makeAsset(checksum: "a", remoteId: "0-1"),
 | |
|       makeAsset(checksum: "b", remoteId: "2-1"),
 | |
|       makeAsset(checksum: "c", localId: "1", remoteId: "1-1"),
 | |
|       makeAsset(checksum: "d", localId: "2"),
 | |
|       makeAsset(checksum: "e", localId: "3"),
 | |
|     ];
 | |
|     setUp(() {
 | |
|       s = SyncService(
 | |
|         hs,
 | |
|         entityService,
 | |
|         albumMediaRepository,
 | |
|         albumApiRepository,
 | |
|         albumRepository,
 | |
|         assetRepository,
 | |
|         exifInfoRepository,
 | |
|         partnerRepository,
 | |
|         userRepository,
 | |
|         userService,
 | |
|         eTagRepository,
 | |
|         appSettingService,
 | |
|         localFilesManagerRepository,
 | |
|         partnerApiRepository,
 | |
|         userApiRepository,
 | |
|       );
 | |
|       when(() => userService.getMyUser()).thenReturn(owner);
 | |
|       when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now()));
 | |
|       when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});
 | |
|       when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {});
 | |
|       when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []);
 | |
|       when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]);
 | |
|       when(() => userRepository.getAll()).thenAnswer((_) async => [owner]);
 | |
|       when(
 | |
|         () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
 | |
|       ).thenAnswer((_) async => initialAssets);
 | |
|       when(
 | |
|         () => assetRepository.getAllByOwnerIdChecksum(any(), any()),
 | |
|       ).thenAnswer((_) async => [initialAssets[3], null, null]);
 | |
|       when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
 | |
|       when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {});
 | |
|       when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []);
 | |
|       when(
 | |
|         () => assetRepository.transaction<void>(any()),
 | |
|       ).thenAnswer((call) => (call.positionalArguments.first as Function).call());
 | |
|       when(
 | |
|         () => assetRepository.transaction<Null>(any()),
 | |
|       ).thenAnswer((call) => (call.positionalArguments.first as Function).call());
 | |
|       when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]);
 | |
|       registerFallbackValue(Direction.sharedByMe);
 | |
|       when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []);
 | |
|     });
 | |
|     test('test inserting existing assets', () async {
 | |
|       final List<Asset> remoteAssets = [
 | |
|         makeAsset(checksum: "a", remoteId: "0-1"),
 | |
|         makeAsset(checksum: "b", remoteId: "2-1"),
 | |
|         makeAsset(checksum: "c", remoteId: "1-1"),
 | |
|       ];
 | |
|       final bool c1 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c1, isFalse);
 | |
|       verifyNever(() => assetRepository.updateAll(any()));
 | |
|     });
 | |
| 
 | |
|     test('test inserting new assets', () async {
 | |
|       final List<Asset> remoteAssets = [
 | |
|         makeAsset(checksum: "a", remoteId: "0-1"),
 | |
|         makeAsset(checksum: "b", remoteId: "2-1"),
 | |
|         makeAsset(checksum: "c", remoteId: "1-1"),
 | |
|         makeAsset(checksum: "d", remoteId: "1-2"),
 | |
|         makeAsset(checksum: "f", remoteId: "1-4"),
 | |
|         makeAsset(checksum: "g", remoteId: "3-1"),
 | |
|       ];
 | |
|       final bool c1 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c1, isTrue);
 | |
|       final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
 | |
|       verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]));
 | |
|     });
 | |
| 
 | |
|     test('test syncing duplicate assets', () async {
 | |
|       final List<Asset> remoteAssets = [
 | |
|         makeAsset(checksum: "a", remoteId: "0-1"),
 | |
|         makeAsset(checksum: "b", remoteId: "1-1"),
 | |
|         makeAsset(checksum: "c", remoteId: "2-1"),
 | |
|         makeAsset(checksum: "h", remoteId: "2-1b"),
 | |
|         makeAsset(checksum: "i", remoteId: "2-1c"),
 | |
|         makeAsset(checksum: "j", remoteId: "2-1d"),
 | |
|       ];
 | |
|       final bool c1 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c1, isTrue);
 | |
|       when(
 | |
|         () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
 | |
|       ).thenAnswer((_) async => remoteAssets);
 | |
|       final bool c2 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c2, isFalse);
 | |
|       final currentState = [...remoteAssets];
 | |
|       when(
 | |
|         () => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
 | |
|       ).thenAnswer((_) async => currentState);
 | |
|       remoteAssets.removeAt(4);
 | |
|       final bool c3 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c3, isTrue);
 | |
|       remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
 | |
|       remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
 | |
|       final bool c4 = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: _failDiff,
 | |
|         loadAssets: (u, d) => remoteAssets,
 | |
|       );
 | |
|       expect(c4, isTrue);
 | |
|     });
 | |
| 
 | |
|     test('test efficient sync', () async {
 | |
|       when(
 | |
|         () => assetRepository.deleteAllByRemoteId([
 | |
|           initialAssets[1].remoteId!,
 | |
|           initialAssets[2].remoteId!,
 | |
|         ], state: AssetState.remote),
 | |
|       ).thenAnswer((_) async {
 | |
|         return;
 | |
|       });
 | |
|       when(
 | |
|         () => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged),
 | |
|       ).thenAnswer((_) async => [initialAssets[2]]);
 | |
|       when(
 | |
|         () => assetRepository.getAllByOwnerIdChecksum(any(), any()),
 | |
|       ).thenAnswer((_) async => [initialAssets[0], null, null]); //afg
 | |
|       final List<Asset> toUpsert = [
 | |
|         makeAsset(checksum: "a", remoteId: "0-1"), // changed
 | |
|         makeAsset(checksum: "f", remoteId: "0-2"), // new
 | |
|         makeAsset(checksum: "g", remoteId: "0-3"), // new
 | |
|       ];
 | |
|       toUpsert[0].isFavorite = true;
 | |
|       final List<String> toDelete = ["2-1", "1-1"];
 | |
|       final expected = [...toUpsert];
 | |
|       expected[0].id = initialAssets[0].id;
 | |
|       final bool c = await s.syncRemoteAssetsToDb(
 | |
|         users: [owner],
 | |
|         getChangedAssets: (user, since) async => (toUpsert, toDelete),
 | |
|         loadAssets: (user, date) => throw Exception(),
 | |
|       );
 | |
|       expect(c, isTrue);
 | |
|       verify(() => assetRepository.updateAll(expected));
 | |
|     });
 | |
| 
 | |
|     group("upsertAssetsWithExif", () {
 | |
|       test('test upsert with EXIF data', () async {
 | |
|         final assets = [AssetStub.image1, AssetStub.image2];
 | |
| 
 | |
|         expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null));
 | |
|         await s.upsertAssetsWithExif(assets);
 | |
|         verify(
 | |
|           () => exifInfoRepository.updateAll(
 | |
|             any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))),
 | |
|           ),
 | |
|         );
 | |
|         expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id));
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| Future<(List<Asset>?, List<String>?)> _failDiff(List<UserDto> user, DateTime time) => Future.value((null, null));
 |