mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 12:15:47 -04:00
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:
parent
042da669d1
commit
43d585ce55
@ -256,7 +256,7 @@ class AssetService {
|
|||||||
|
|
||||||
for (var element in assets) {
|
for (var element in assets) {
|
||||||
element.fileCreatedAt = DateTime.parse(updatedDt);
|
element.fileCreatedAt = DateTime.parse(updatedDt);
|
||||||
element.exifInfo ??= element.exifInfo
|
element.exifInfo = element.exifInfo
|
||||||
?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt));
|
?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ class AssetService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (var element in assets) {
|
for (var element in assets) {
|
||||||
element.exifInfo ??= element.exifInfo?.copyWith(
|
element.exifInfo = element.exifInfo?.copyWith(
|
||||||
latitude: location.latitude,
|
latitude: location.latitude,
|
||||||
longitude: location.longitude,
|
longitude: location.longitude,
|
||||||
);
|
);
|
||||||
|
@ -798,7 +798,7 @@ class SyncService {
|
|||||||
await _assetRepository.transaction(() async {
|
await _assetRepository.transaction(() async {
|
||||||
await _assetRepository.updateAll(assets);
|
await _assetRepository.updateAll(assets);
|
||||||
for (final Asset added in 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);
|
await _exifInfoRepository.updateAll(exifInfos);
|
||||||
});
|
});
|
||||||
|
4
mobile/test/api.mocks.dart
Normal file
4
mobile/test/api.mocks.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class MockAssetsApi extends Mock implements AssetsApi {}
|
3
mobile/test/fixtures/asset.stub.dart
vendored
3
mobile/test/fixtures/asset.stub.dart
vendored
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
final class AssetStub {
|
final class AssetStub {
|
||||||
@ -17,6 +18,7 @@ final class AssetStub {
|
|||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
|
exifInfo: const ExifInfo(isFlipped: false),
|
||||||
);
|
);
|
||||||
|
|
||||||
static final image2 = Asset(
|
static final image2 = Asset(
|
||||||
@ -33,6 +35,7 @@ final class AssetStub {
|
|||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
|
exifInfo: const ExifInfo(isFlipped: true),
|
||||||
);
|
);
|
||||||
|
|
||||||
static final image3 = Asset(
|
static final image3 = Asset(
|
||||||
|
@ -16,6 +16,7 @@ import 'package:immich_mobile/services/sync.service.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../domain/service.mock.dart';
|
import '../../domain/service.mock.dart';
|
||||||
|
import '../../fixtures/asset.stub.dart';
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../../repository.mocks.dart';
|
import '../../repository.mocks.dart';
|
||||||
import '../../service.mocks.dart';
|
import '../../service.mocks.dart';
|
||||||
@ -258,6 +259,19 @@ void main() {
|
|||||||
expect(c, isTrue);
|
expect(c, isTrue);
|
||||||
verify(() => assetRepository.updateAll(expected));
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.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.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/asset_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth_api.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/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.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.interface.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockAlbumRepository extends Mock implements IAlbumRepository {}
|
class MockAlbumRepository extends Mock implements IAlbumRepository {}
|
||||||
@ -25,6 +26,11 @@ class MockETagRepository extends Mock implements IETagRepository {}
|
|||||||
|
|
||||||
class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
|
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 MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
|
||||||
|
|
||||||
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
|
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
|
||||||
|
@ -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/api.service.dart';
|
||||||
import 'package:immich_mobile/services/background.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/entity.service.dart';
|
||||||
import 'package:immich_mobile/services/hash.service.dart';
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/services/network.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 MockApiService extends Mock implements ApiService {}
|
||||||
|
|
||||||
|
class MockAlbumService extends Mock implements AlbumService {}
|
||||||
|
|
||||||
|
class MockBackupService extends Mock implements BackupService {}
|
||||||
|
|
||||||
class MockSyncService extends Mock implements SyncService {}
|
class MockSyncService extends Mock implements SyncService {}
|
||||||
|
|
||||||
class MockHashService extends Mock implements HashService {}
|
class MockHashService extends Mock implements HashService {}
|
||||||
|
111
mobile/test/services/asset.service_test.dart
Normal file
111
mobile/test/services/asset.service_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user