fix(mobile): exifInfo not updated on sync (#17407)

* fix(mobile): exifInfo not updated on sync

* add tests

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-04-07 20:51:37 +05:30 committed by GitHub
parent 042da669d1
commit 43d585ce55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 148 additions and 4 deletions

View File

@ -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,
);

View File

@ -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);
});

View File

@ -0,0 +1,4 @@
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
class MockAssetsApi extends Mock implements AssetsApi {}

View File

@ -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(

View File

@ -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));
});
});
});
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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<Object>? ?? [];
final receivedDatetime = receivedAssets.cast<Asset>().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<Object>? ?? [];
final receivedCoords = receivedAssets.cast<Asset>().map(
(a) =>
LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0),
);
expect(receivedCoords.every((l) => l == latLng), isTrue);
});
});
}