mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 22:51:21 -07:00
refactor: asset update method (#30201)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
co-authored by
shenlong-tanwen
parent
bc6bf388c0
commit
04a38ba91c
@@ -5,12 +5,13 @@ import '../../utils.dart';
|
||||
class LocalAssetFactory {
|
||||
const LocalAssetFactory();
|
||||
|
||||
static LocalAsset create({String? id, String? name}) {
|
||||
static LocalAsset create({String? id, String? name, String? remoteId}) {
|
||||
id = TestUtils.uuid(id);
|
||||
|
||||
return LocalAsset(
|
||||
id: id,
|
||||
name: name ?? 'local_$id.jpg',
|
||||
remoteId: remoteId,
|
||||
type: AssetType.image,
|
||||
createdAt: TestUtils.yesterday(),
|
||||
updatedAt: TestUtils.now(),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
|
||||
import '../../utils.dart';
|
||||
|
||||
class RemoteAlbumFactory {
|
||||
const RemoteAlbumFactory();
|
||||
|
||||
static RemoteAlbum create({
|
||||
String? id,
|
||||
String? name,
|
||||
String? ownerId,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? thumbnailAssetId,
|
||||
bool isActivityEnabled = false,
|
||||
AlbumAssetOrder order = AlbumAssetOrder.desc,
|
||||
int assetCount = 0,
|
||||
String? ownerName,
|
||||
bool isShared = false,
|
||||
}) {
|
||||
id = TestUtils.uuid(id);
|
||||
return RemoteAlbum(
|
||||
id: id,
|
||||
name: name ?? 'remote_album_$id',
|
||||
ownerId: TestUtils.uuid(ownerId),
|
||||
description: description ?? '',
|
||||
createdAt: TestUtils.date(createdAt),
|
||||
updatedAt: TestUtils.date(updatedAt),
|
||||
thumbnailAssetId: thumbnailAssetId,
|
||||
isActivityEnabled: isActivityEnabled,
|
||||
order: order,
|
||||
assetCount: assetCount,
|
||||
ownerName: ownerName ?? 'owner_$id',
|
||||
isShared: isShared,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ class RemoteAssetFactory {
|
||||
String? name,
|
||||
String? ownerId,
|
||||
bool isFavorite = false,
|
||||
AssetVisibility visibility = AssetVisibility.timeline,
|
||||
AssetVisibility visibility = .timeline,
|
||||
AssetType type = .image,
|
||||
String? stackId,
|
||||
DateTime? deletedAt,
|
||||
String? localId,
|
||||
}) {
|
||||
id = TestUtils.uuid(id);
|
||||
|
||||
@@ -20,13 +23,15 @@ class RemoteAssetFactory {
|
||||
name: name ?? 'remote_$id.jpg',
|
||||
ownerId: TestUtils.uuid(ownerId),
|
||||
checksum: 'checksum-$id',
|
||||
type: .image,
|
||||
type: type,
|
||||
createdAt: TestUtils.yesterday(),
|
||||
updatedAt: TestUtils.now(),
|
||||
isFavorite: isFavorite,
|
||||
visibility: visibility,
|
||||
stackId: stackId,
|
||||
isEdited: false,
|
||||
deletedAt: deletedAt,
|
||||
localId: localId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+233
-4
@@ -1,25 +1,43 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/tag.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:mocktail/mocktail.dart' as mock;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../domain/service.mock.dart';
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
import '../repository.mocks.dart';
|
||||
import '../service.mocks.dart';
|
||||
import 'factories/local_album_factory.dart';
|
||||
import 'factories/local_asset_factory.dart';
|
||||
import 'factories/remote_album_factory.dart';
|
||||
import 'factories/user_factory.dart';
|
||||
|
||||
class RepositoryMocks {
|
||||
final localAlbum = LocalAlbumRepositoryStub(MockLocalAlbumRepository());
|
||||
final localAsset = LocalAssetRepositoryStub(MockDriftLocalAssetRepository());
|
||||
final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository());
|
||||
final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository());
|
||||
final trashedAsset = MockTrashedLocalAssetRepository();
|
||||
final toast = MockToastRepository();
|
||||
final remoteAlbum = MockRemoteAlbumRepository();
|
||||
final albumApi = MockDriftAlbumApiRepository();
|
||||
|
||||
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
||||
final assetApi = AssetApiRepositoryStub(MockAssetApiRepository());
|
||||
final assetMedia = AssetMediaRepositoryStub(MockAssetMediaRepository());
|
||||
final download = DownloadRepositoryStub(MockDownloadRepository());
|
||||
|
||||
RepositoryMocks() {
|
||||
resetAll();
|
||||
@@ -29,11 +47,34 @@ class RepositoryMocks {
|
||||
_registerFallbacks();
|
||||
localAlbum.reset();
|
||||
localAsset.reset();
|
||||
remoteAsset.reset();
|
||||
remoteExif.reset();
|
||||
reset(trashedAsset);
|
||||
reset(remoteAlbum);
|
||||
reset(albumApi);
|
||||
nativeApi.reset();
|
||||
assetApi.reset();
|
||||
assetMedia.reset();
|
||||
download.reset();
|
||||
reset(toast);
|
||||
_stubLocalAlbumRepository();
|
||||
_stubLocalAssetRepository();
|
||||
_stubRemoteAssetRepository();
|
||||
_stubRemoteExifRepository();
|
||||
_stubNativeSyncApi();
|
||||
_stubAssetApiRepository();
|
||||
_stubAssetMediaRepository();
|
||||
_stubDownloadRepository();
|
||||
}
|
||||
|
||||
void _stubRemoteAssetRepository() {
|
||||
when(remoteAsset.getExif).thenAnswer((_) async => null);
|
||||
when(remoteAsset.getAssetEdits).thenAnswer((_) async => const []);
|
||||
when(remoteAsset.update).thenAnswer((_) async {});
|
||||
}
|
||||
|
||||
void _stubRemoteExifRepository() {
|
||||
when(remoteExif.update).thenAnswer((_) async {});
|
||||
}
|
||||
|
||||
void _stubLocalAlbumRepository() {
|
||||
@@ -49,12 +90,31 @@ class RepositoryMocks {
|
||||
void _stubNativeSyncApi() {
|
||||
when(nativeApi.hashAssets).thenAnswer((_) async => []);
|
||||
}
|
||||
|
||||
void _stubAssetApiRepository() {
|
||||
when(assetApi.update).thenAnswer((_) async => {});
|
||||
}
|
||||
|
||||
void _stubAssetMediaRepository() {
|
||||
when(assetMedia.shareAssets).thenAnswer((_) async => 1);
|
||||
}
|
||||
|
||||
void _stubDownloadRepository() {
|
||||
when(download.downloadAllAssets).thenAnswer((_) async => const []);
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceMocks {
|
||||
final partner = PartnerServiceStub(MockPartnerService());
|
||||
final user = UserServiceStub(MockUserService());
|
||||
final asset = AssetServiceStub(MockAssetService());
|
||||
final album = RemoteAlbumServiceStub(MockRemoteAlbumService());
|
||||
final cleanup = CleanupServiceStub(MockCleanupService());
|
||||
final tag = TagServiceStub(MockTagService());
|
||||
final backgroundSync = MockBackgroundSyncManager();
|
||||
final upload = MockForegroundUploadService();
|
||||
final cast = MockGCastService();
|
||||
final serverInfo = MockServerInfoService();
|
||||
|
||||
ServiceMocks() {
|
||||
resetAll();
|
||||
@@ -65,9 +125,21 @@ class ServiceMocks {
|
||||
partner.reset();
|
||||
user.reset();
|
||||
asset.reset();
|
||||
album.reset();
|
||||
cleanup.reset();
|
||||
tag.reset();
|
||||
reset(cast);
|
||||
reset(serverInfo);
|
||||
reset(backgroundSync);
|
||||
reset(upload);
|
||||
_stubUserService();
|
||||
_stubPartnerService();
|
||||
_stubAssetService();
|
||||
_stubRemoteAlbumService();
|
||||
_stubCleanupService();
|
||||
_stubTagService();
|
||||
_stubBackgroundSync();
|
||||
_stubForegroundUpload();
|
||||
}
|
||||
|
||||
void _stubUserService() {
|
||||
@@ -88,7 +160,43 @@ class ServiceMocks {
|
||||
}
|
||||
|
||||
void _stubAssetService() {
|
||||
when(asset.updateFavorite).thenAnswer((_) async {});
|
||||
when(asset.update).thenAnswer((_) async {});
|
||||
when(asset.stack).thenAnswer((_) async {});
|
||||
when(asset.unstack).thenAnswer((_) async {});
|
||||
when(asset.restoreTrash).thenAnswer((_) async {});
|
||||
when(asset.trash).thenAnswer((_) async {});
|
||||
when(asset.delete).thenAnswer((_) async {});
|
||||
when(asset.applyEdits).thenAnswer((_) async {});
|
||||
}
|
||||
|
||||
void _stubRemoteAlbumService() {
|
||||
when(album.removeAssets).thenAnswer((_) async => 0);
|
||||
when(album.updateAlbum).thenAnswer((_) async => RemoteAlbumFactory.create());
|
||||
}
|
||||
|
||||
void _stubCleanupService() {
|
||||
when(cleanup.deleteLocalAssets).thenAnswer((_) async => 0);
|
||||
}
|
||||
|
||||
void _stubTagService() {
|
||||
when(tag.bulkTagAssets).thenAnswer((_) async => 0);
|
||||
when(tag.upsertTags).thenAnswer((_) async => const []);
|
||||
when(tag.getAllTags).thenAnswer((_) async => const {});
|
||||
}
|
||||
|
||||
void _stubBackgroundSync() {
|
||||
when(() => backgroundSync.syncLocal()).thenAnswer((_) async {});
|
||||
when(() => backgroundSync.hashAssets()).thenAnswer((_) async {});
|
||||
}
|
||||
|
||||
void _stubForegroundUpload() {
|
||||
when(
|
||||
() => upload.uploadManual(
|
||||
any(),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
callbacks: any(named: 'callbacks'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +204,24 @@ void _registerFallbacks() {
|
||||
registerFallbackValue(LocalAlbumFactory.create());
|
||||
registerFallbackValue(LocalAssetFactory.create());
|
||||
registerFallbackValue(Uint8List(0));
|
||||
registerFallbackValue(AssetVisibility.timeline);
|
||||
registerFallbackValue(const LatLng(0, 0));
|
||||
registerFallbackValue(<AssetEdit>[]);
|
||||
registerFallbackValue(const Option<bool>.none());
|
||||
registerFallbackValue(const Option<AssetVisibility>.none());
|
||||
registerFallbackValue(const Option<LatLng>.none());
|
||||
registerFallbackValue(const Option<String>.none());
|
||||
registerFallbackValue(const Option<DateTime>.none());
|
||||
registerFallbackValue(<BaseAsset>[]);
|
||||
registerFallbackValue(<RemoteAsset>[]);
|
||||
registerFallbackValue(<LocalAsset>[]);
|
||||
registerFallbackValue(ShareAssetType.original);
|
||||
registerFallbackValue(const UploadCallbacks());
|
||||
registerFallbackValue(_FakeBuildContext());
|
||||
}
|
||||
|
||||
class _FakeBuildContext extends Fake implements BuildContext {}
|
||||
|
||||
extension type const Stub<T extends Mock>(T mockedClass) {
|
||||
void reset() => mock.reset(mockedClass);
|
||||
}
|
||||
@@ -119,6 +243,33 @@ extension type const LocalAssetRepositoryStub(MockDriftLocalAssetRepository repo
|
||||
() => repo.updateHashes(any());
|
||||
}
|
||||
|
||||
extension type const RemoteAssetRepositoryStub(MockRemoteAssetRepository repo)
|
||||
implements Stub<MockRemoteAssetRepository> {
|
||||
Future<ExifInfo?> Function() get getExif =>
|
||||
() => repo.getExif(any());
|
||||
|
||||
Future<List<AssetEdit>> Function() get getAssetEdits =>
|
||||
() => repo.getAssetEdits(any());
|
||||
|
||||
Future<void> Function() get update =>
|
||||
() => repo.update(
|
||||
any(),
|
||||
isFavorite: any(named: 'isFavorite'),
|
||||
visibility: any(named: 'visibility'),
|
||||
createdAt: any(named: 'createdAt'),
|
||||
);
|
||||
}
|
||||
|
||||
extension type const RemoteExifRepositoryStub(MockRemoteExifRepository repo) implements Stub<MockRemoteExifRepository> {
|
||||
Future<void> Function() get update =>
|
||||
() => repo.update(
|
||||
any(),
|
||||
dateTimeOriginal: any(named: 'dateTimeOriginal'),
|
||||
timeZone: any(named: 'timeZone'),
|
||||
location: any(named: 'location'),
|
||||
);
|
||||
}
|
||||
|
||||
extension type const PartnerServiceStub(MockPartnerService service) implements Stub<MockPartnerService> {
|
||||
Stream<Iterable<User>> Function() get getCandidates =>
|
||||
() => service.getCandidates(any());
|
||||
@@ -165,11 +316,89 @@ extension type const UserServiceStub(MockUserService service) implements Stub<Mo
|
||||
}
|
||||
|
||||
extension type const AssetServiceStub(MockAssetService service) implements Stub<MockAssetService> {
|
||||
Future<void> Function() get updateFavorite =>
|
||||
() => service.updateFavorite(any(), any());
|
||||
Future<void> Function() get update =>
|
||||
() => service.update(
|
||||
any(),
|
||||
isFavorite: any(named: 'isFavorite'),
|
||||
visibility: any(named: 'visibility'),
|
||||
dateTime: any(named: 'dateTime'),
|
||||
location: any(named: 'location'),
|
||||
);
|
||||
|
||||
Future<void> Function() get stack =>
|
||||
() => service.stack(any(), any());
|
||||
|
||||
Future<void> Function() get unstack =>
|
||||
() => service.unstack(any());
|
||||
|
||||
Future<void> Function() get restoreTrash =>
|
||||
() => service.restoreTrash(any());
|
||||
|
||||
Future<void> Function() get trash =>
|
||||
() => service.trash(any());
|
||||
|
||||
Future<void> Function() get delete =>
|
||||
() => service.delete(any());
|
||||
|
||||
Future<void> Function() get applyEdits =>
|
||||
() => service.applyEdits(any(), any());
|
||||
}
|
||||
|
||||
extension type const RemoteAlbumServiceStub(MockRemoteAlbumService service) implements Stub<MockRemoteAlbumService> {
|
||||
Future<int> Function() get removeAssets =>
|
||||
() => service.removeAssets(
|
||||
albumId: any(named: 'albumId'),
|
||||
assetIds: any(named: 'assetIds'),
|
||||
);
|
||||
|
||||
Future<RemoteAlbum> Function() get updateAlbum =>
|
||||
() => service.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId'));
|
||||
}
|
||||
|
||||
extension type const CleanupServiceStub(MockCleanupService service) implements Stub<MockCleanupService> {
|
||||
Future<int> Function() get deleteLocalAssets =>
|
||||
() => service.deleteLocalAssets(any());
|
||||
}
|
||||
|
||||
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
||||
Future<List<HashResult>> Function() get hashAssets =>
|
||||
() => api.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess'));
|
||||
}
|
||||
|
||||
extension type const AssetApiRepositoryStub(MockAssetApiRepository api) implements Stub<MockAssetApiRepository> {
|
||||
Future<void> Function() get update =>
|
||||
() => api.update(
|
||||
any(),
|
||||
isFavorite: any(named: 'isFavorite'),
|
||||
visibility: any(named: 'visibility'),
|
||||
dateTimeOriginal: any(named: 'dateTimeOriginal'),
|
||||
location: any(named: 'location'),
|
||||
);
|
||||
}
|
||||
|
||||
extension type const AssetMediaRepositoryStub(MockAssetMediaRepository api) implements Stub<MockAssetMediaRepository> {
|
||||
Future<int> Function() get shareAssets =>
|
||||
() => api.shareAssets(
|
||||
any(),
|
||||
any(),
|
||||
fileType: any(named: 'fileType'),
|
||||
cancelCompleter: any(named: 'cancelCompleter'),
|
||||
onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'),
|
||||
);
|
||||
}
|
||||
|
||||
extension type const DownloadRepositoryStub(MockDownloadRepository repo) implements Stub<MockDownloadRepository> {
|
||||
Future<List<bool>> Function() get downloadAllAssets =>
|
||||
() => repo.downloadAllAssets(any());
|
||||
}
|
||||
|
||||
extension type const TagServiceStub(MockTagService service) implements Stub<MockTagService> {
|
||||
Future<int> Function() get bulkTagAssets =>
|
||||
() => service.bulkTagAssets(any(), any());
|
||||
|
||||
Future<List<Tag>> Function() get upsertTags =>
|
||||
() => service.upsertTags(any());
|
||||
|
||||
Future<Set<Tag>> Function() get getAllTags =>
|
||||
() => service.getAllTags();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../domain/service.mock.dart';
|
||||
import '../../../service.mocks.dart';
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/partner.action.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../domain/service.mock.dart';
|
||||
import '../../../service.mocks.dart';
|
||||
import '../../factories/user_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -29,6 +32,7 @@ class PresentationContext {
|
||||
service = ServiceMocks(),
|
||||
repository = RepositoryMocks() {
|
||||
setup();
|
||||
addTearDown(dispose);
|
||||
}
|
||||
|
||||
static const String serverEndpoint = 'http://localhost:3000';
|
||||
@@ -43,6 +47,9 @@ class PresentationContext {
|
||||
currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)),
|
||||
assetServiceProvider.overrideWithValue(service.asset.service),
|
||||
partnerServiceProvider.overrideWithValue(service.partner.service),
|
||||
gCastServiceProvider.overrideWithValue(service.cast),
|
||||
serverInfoServiceProvider.overrideWithValue(service.serverInfo),
|
||||
inLockedViewProvider.overrideWithValue(false),
|
||||
];
|
||||
|
||||
static Future<PresentationContext> create() async {
|
||||
@@ -61,9 +68,7 @@ class PresentationContext {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
addTearDown(() {
|
||||
service.resetAll();
|
||||
});
|
||||
service.resetAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../../repository.mocks.dart';
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
late AssetService sut;
|
||||
late RepositoryMocks mocks;
|
||||
late MockAssetApiRepository apiRepository;
|
||||
late MockRemoteAssetRepository remoteRepository;
|
||||
late MockRemoteExifRepository exifRepository;
|
||||
|
||||
setUp(() {
|
||||
mocks = RepositoryMocks();
|
||||
apiRepository = mocks.assetApi.api;
|
||||
remoteRepository = mocks.remoteAsset.repo;
|
||||
exifRepository = mocks.remoteExif.repo;
|
||||
|
||||
sut = AssetService(
|
||||
remoteRepository: remoteRepository,
|
||||
exifRepository: exifRepository,
|
||||
localRepository: MockDriftLocalAssetRepository(),
|
||||
apiRepository: apiRepository,
|
||||
);
|
||||
});
|
||||
|
||||
group('AssetService.updateDateTime', () {
|
||||
const ids = ['asset_id_1'];
|
||||
|
||||
test('sends the picked value to the api with its offset intact', () async {
|
||||
const picked = '2026-06-10T19:15:00.000+06:00';
|
||||
await sut.update(ids, dateTime: const .some(picked));
|
||||
|
||||
verify(() => apiRepository.update(ids, dateTimeOriginal: const .some(picked))).called(1);
|
||||
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||
verify(
|
||||
() => exifRepository.update(
|
||||
ids,
|
||||
dateTimeOriginal: .some(DateTime.parse(picked)),
|
||||
timeZone: const .some('UTC+06:00'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('handles negative offsets', () async {
|
||||
const picked = '2026-01-05T08:00:00.000-05:30';
|
||||
await sut.update(ids, dateTime: const .some(picked));
|
||||
|
||||
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||
verify(
|
||||
() => exifRepository.update(
|
||||
ids,
|
||||
dateTimeOriginal: .some(DateTime.parse(picked)),
|
||||
timeZone: const .some('UTC-05:30'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('writes no timezone when the value has no offset', () async {
|
||||
const picked = '2026-06-10T13:15:00.000Z';
|
||||
await sut.update(ids, dateTime: const .some(picked));
|
||||
|
||||
verify(() => remoteRepository.update(ids, createdAt: .some(DateTime.parse(picked)))).called(1);
|
||||
verify(
|
||||
() => exifRepository.update(ids, dateTimeOriginal: .some(DateTime.parse(picked)), timeZone: const .none()),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('is a no-op when there are no asset ids', () async {
|
||||
await sut.update(const [], dateTime: const .some('2026-06-10T19:15:00.000+06:00'));
|
||||
|
||||
verifyZeroInteractions(apiRepository);
|
||||
verifyZeroInteractions(remoteRepository);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../service.mocks.dart';
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
late RemoteAlbumService sut;
|
||||
final mocks = RepositoryMocks();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(<String>[]);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
sut = RemoteAlbumService(mocks.remoteAlbum, mocks.albumApi, MockForegroundUploadService());
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
mocks.resetAll();
|
||||
});
|
||||
|
||||
group('RemoteAlbumService', () {
|
||||
group('removeAssets', () {
|
||||
test('persists only the assets the server actually removed, not the whole request', () async {
|
||||
const albumId = 'album-1';
|
||||
const requested = ['asset-1', 'asset-2', 'asset-3'];
|
||||
const removed = ['asset-1', 'asset-3'];
|
||||
|
||||
// The server rejected 'asset-2'
|
||||
when(
|
||||
() => mocks.albumApi.removeAssets(albumId, requested),
|
||||
).thenAnswer((_) async => (removed: removed, failed: ['asset-2']));
|
||||
when(() => mocks.remoteAlbum.removeAssets(albumId, any())).thenAnswer((_) async {});
|
||||
|
||||
final count = await sut.removeAssets(albumId: albumId, assetIds: requested);
|
||||
|
||||
final persisted =
|
||||
verify(() => mocks.remoteAlbum.removeAssets(albumId, captureAny())).captured.single as List<String>;
|
||||
expect(persisted, removed);
|
||||
expect(persisted, isNot(contains('asset-2')));
|
||||
|
||||
expect(count, removed.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -52,7 +52,7 @@ void main() {
|
||||
final offlinePhoto = LocalAssetFactory.create();
|
||||
final remotePhoto = RemoteAssetFactory.create();
|
||||
|
||||
final AssetFilter<LocalAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
||||
final AssetFilter<BaseAsset> syncedPhotos = AssetFilter(<BaseAsset>[
|
||||
syncedPhoto,
|
||||
offlinePhoto,
|
||||
remotePhoto,
|
||||
|
||||
Reference in New Issue
Block a user