diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 92f04e2304..d187284d07 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -256,7 +256,7 @@ class AssetService { for (var element in assets) { element.fileCreatedAt = DateTime.parse(updatedDt); - element.exifInfo ??= element.exifInfo + element.exifInfo = element.exifInfo ?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt)); } @@ -283,7 +283,7 @@ class AssetService { ); for (var element in assets) { - element.exifInfo ??= element.exifInfo?.copyWith( + element.exifInfo = element.exifInfo?.copyWith( latitude: location.latitude, longitude: location.longitude, ); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index f2b16b080a..1e3c2a070b 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -798,7 +798,7 @@ class SyncService { await _assetRepository.transaction(() async { await _assetRepository.updateAll(assets); for (final Asset added in assets) { - added.exifInfo ??= added.exifInfo?.copyWith(assetId: added.id); + added.exifInfo = added.exifInfo?.copyWith(assetId: added.id); } await _exifInfoRepository.updateAll(exifInfos); }); diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart new file mode 100644 index 0000000000..d502ea0675 --- /dev/null +++ b/mobile/test/api.mocks.dart @@ -0,0 +1,4 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; + +class MockAssetsApi extends Mock implements AssetsApi {} diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index b69b392129..771b2dda96 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; final class AssetStub { @@ -17,6 +18,7 @@ final class AssetStub { isFavorite: true, isArchived: false, isTrashed: false, + exifInfo: const ExifInfo(isFlipped: false), ); static final image2 = Asset( @@ -33,6 +35,7 @@ final class AssetStub { isFavorite: false, isArchived: false, isTrashed: false, + exifInfo: const ExifInfo(isFlipped: true), ); static final image3 = Asset( diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 0197008dd1..eab6b6f61a 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -16,6 +16,7 @@ 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'; @@ -258,6 +259,19 @@ void main() { 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); + expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id)); + }); + }); }); } diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index e4e99ffcb8..1c698297dc 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -3,14 +3,15 @@ import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_media.interface.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/interfaces/auth_api.interface.dart'; import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; -import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/interfaces/partner.interface.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:mocktail/mocktail.dart'; class MockAlbumRepository extends Mock implements IAlbumRepository {} @@ -25,6 +26,11 @@ class MockETagRepository extends Mock implements IETagRepository {} class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {} +class MockBackupAlbumRepository extends Mock + implements IBackupAlbumRepository {} + +class MockAssetApiRepository extends Mock implements IAssetApiRepository {} + class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {} class MockFileMediaRepository extends Mock implements IFileMediaRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 8ee1c58609..d31a7e5d50 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -1,5 +1,7 @@ +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/background.service.dart'; +import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/network.service.dart'; @@ -9,6 +11,10 @@ import 'package:openapi/api.dart'; class MockApiService extends Mock implements ApiService {} +class MockAlbumService extends Mock implements AlbumService {} + +class MockBackupService extends Mock implements BackupService {} + class MockSyncService extends Mock implements SyncService {} class MockHashService extends Mock implements HashService {} diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart new file mode 100644 index 0000000000..63546e39f1 --- /dev/null +++ b/mobile/test/services/asset.service_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/services/asset.service.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; + +import '../api.mocks.dart'; +import '../domain/service.mock.dart'; +import '../fixtures/asset.stub.dart'; +import '../infrastructure/repository.mock.dart'; +import '../repository.mocks.dart'; +import '../service.mocks.dart'; + +class FakeAssetBulkUpdateDto extends Fake implements AssetBulkUpdateDto {} + +void main() { + late AssetService sut; + + late MockAssetRepository assetRepository; + late MockAssetApiRepository assetApiRepository; + late MockExifInfoRepository exifInfoRepository; + late MockETagRepository eTagRepository; + late MockBackupAlbumRepository backupAlbumRepository; + late MockUserRepository userRepository; + late MockAssetMediaRepository assetMediaRepository; + late MockApiService apiService; + + late MockSyncService syncService; + late MockAlbumService albumService; + late MockBackupService backupService; + late MockUserService userService; + + setUp(() { + assetRepository = MockAssetRepository(); + assetApiRepository = MockAssetApiRepository(); + exifInfoRepository = MockExifInfoRepository(); + userRepository = MockUserRepository(); + eTagRepository = MockETagRepository(); + backupAlbumRepository = MockBackupAlbumRepository(); + apiService = MockApiService(); + assetMediaRepository = MockAssetMediaRepository(); + + syncService = MockSyncService(); + userService = MockUserService(); + albumService = MockAlbumService(); + backupService = MockBackupService(); + + sut = AssetService( + assetApiRepository, + assetRepository, + exifInfoRepository, + userRepository, + eTagRepository, + backupAlbumRepository, + apiService, + syncService, + backupService, + albumService, + userService, + assetMediaRepository, + ); + + registerFallbackValue(FakeAssetBulkUpdateDto()); + }); + + group("Edit ExifInfo", () { + late AssetsApi assetsApi; + setUp(() { + assetsApi = MockAssetsApi(); + when(() => apiService.assetsApi).thenReturn(assetsApi); + when(() => assetsApi.updateAssets(any())) + .thenAnswer((_) async => Future.value()); + }); + + test("asset is updated with DateTime", () async { + final assets = [AssetStub.image1, AssetStub.image2]; + final dateTime = DateTime.utc(2025, 6, 4, 2, 57); + await sut.changeDateTime(assets, dateTime.toIso8601String()); + + verify(() => assetsApi.updateAssets(any())).called(1); + final upsertExifCallback = + verify(() => syncService.upsertAssetsWithExif(captureAny())); + upsertExifCallback.called(1); + final receivedAssets = + upsertExifCallback.captured.firstOrNull as List? ?? []; + final receivedDatetime = receivedAssets.cast().map( + (a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0), + ); + expect(receivedDatetime.every((d) => d == dateTime), isTrue); + }); + + test("asset is updated with LatLng", () async { + final assets = [AssetStub.image1, AssetStub.image2]; + final latLng = const LatLng(37.7749, -122.4194); + await sut.changeLocation(assets, latLng); + + verify(() => assetsApi.updateAssets(any())).called(1); + final upsertExifCallback = + verify(() => syncService.upsertAssetsWithExif(captureAny())); + upsertExifCallback.called(1); + final receivedAssets = + upsertExifCallback.captured.firstOrNull as List? ?? []; + final receivedCoords = receivedAssets.cast().map( + (a) => + LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), + ); + expect(receivedCoords.every((l) => l == latLng), isTrue); + }); + }); +}