feat(mobile): write the previous checksum when the upload finishes (#31577)
* write the previous checksum when the upload finishes * ask the server for the checksum when the upload has none
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.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/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
@@ -28,41 +29,80 @@ void main() {
|
||||
return (checksum: data.checksum, previousChecksum: data.previousChecksum);
|
||||
}
|
||||
|
||||
for (final (platform, scenario, edited, expected) in [
|
||||
for (final (platform, scenario, before, edited, after) in [
|
||||
(
|
||||
TargetPlatform.android,
|
||||
'updatedAt changed clears the checksum and remembers it',
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
_localAsset('edited', updatedAt: DateTime(2025)),
|
||||
(checksum: null, previousChecksum: 'a'),
|
||||
),
|
||||
(
|
||||
TargetPlatform.android,
|
||||
'adjustmentTime changed keeps the checksum',
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
_localAsset('edited', adjustmentTime: DateTime(2025)),
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
),
|
||||
(
|
||||
TargetPlatform.android,
|
||||
'updatedAt changed again before the upload keeps the uploaded checksum',
|
||||
(checksum: 'b', previousChecksum: 'a'),
|
||||
_localAsset('edited', updatedAt: DateTime(2025)),
|
||||
(checksum: null, previousChecksum: 'a'),
|
||||
),
|
||||
(
|
||||
TargetPlatform.iOS,
|
||||
'adjustmentTime changed clears the checksum and remembers it',
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
_localAsset('edited', adjustmentTime: DateTime(2025)),
|
||||
(checksum: null, previousChecksum: 'a'),
|
||||
),
|
||||
(
|
||||
TargetPlatform.iOS,
|
||||
'updatedAt changed keeps the checksum',
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
_localAsset('edited', updatedAt: DateTime(2025)),
|
||||
(checksum: 'a', previousChecksum: null),
|
||||
),
|
||||
(
|
||||
TargetPlatform.iOS,
|
||||
'adjustmentTime changed again before the upload keeps the uploaded checksum',
|
||||
(checksum: 'b', previousChecksum: 'a'),
|
||||
_localAsset('edited', adjustmentTime: DateTime(2025)),
|
||||
(checksum: null, previousChecksum: 'a'),
|
||||
),
|
||||
]) {
|
||||
test('${platform.name}: $scenario', () async {
|
||||
debugDefaultTargetPlatformOverride = platform;
|
||||
await ctx.newLocalAsset(id: 'edited', checksum: 'a', updatedAt: DateTime(2024), adjustmentTime: DateTime(2024));
|
||||
await ctx.newLocalAsset(
|
||||
id: 'edited',
|
||||
checksum: before.checksum,
|
||||
previousChecksum: before.previousChecksum,
|
||||
updatedAt: DateTime(2024),
|
||||
adjustmentTime: DateTime(2024),
|
||||
);
|
||||
|
||||
await sut.upsert(album, toUpsert: [edited]);
|
||||
|
||||
expect(await row('edited'), expected);
|
||||
expect(await row('edited'), after);
|
||||
});
|
||||
}
|
||||
|
||||
test('android: previous follows the uploads across two edits', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final assets = LocalAssetRepository(ctx.db);
|
||||
await ctx.newLocalAsset(id: 'edited', checksum: 'a', updatedAt: DateTime(2024), adjustmentTime: DateTime(2024));
|
||||
|
||||
await assets.updatePreviousChecksum('edited', 'a');
|
||||
await sut.upsert(album, toUpsert: [_localAsset('edited', updatedAt: DateTime(2025))]);
|
||||
expect(await row('edited'), (checksum: null, previousChecksum: 'a'));
|
||||
|
||||
await assets.updateHashes({'edited': 'b'});
|
||||
await assets.updatePreviousChecksum('edited', 'b');
|
||||
await sut.upsert(album, toUpsert: [_localAsset('edited', updatedAt: DateTime(2026))]);
|
||||
expect(await row('edited'), (checksum: null, previousChecksum: 'b'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
mockBackupRepository = MockBackupRepository();
|
||||
mockAssetMediaRepository = MockAssetMediaRepository();
|
||||
mockAssetService = MockAssetService();
|
||||
when(() => mockAssetService.stackEditedUpload(any(), any())).thenAnswer((_) async {});
|
||||
when(() => mockAssetService.stackEditedUpload(any(), any(), any())).thenAnswer((_) async {});
|
||||
|
||||
sut = BackgroundUploadService(
|
||||
mockUploadRepository,
|
||||
@@ -416,7 +416,7 @@ void main() {
|
||||
|
||||
group('onUploadStatus', () {
|
||||
test('stacks a plain photo after its upload', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final asset = LocalAssetStub.image1.copyWith(checksum: 'sha');
|
||||
final mockEntity = MockAssetEntity();
|
||||
final mockFile = File('/path/to/photo.jpg');
|
||||
final void Function(TaskStatusUpdate) onStatus = verify(
|
||||
@@ -431,12 +431,12 @@ void main() {
|
||||
final task = await sut.getUploadTask(asset);
|
||||
onStatus(TaskStatusUpdate(task!, TaskStatus.complete, null, '{"id": "remote"}'));
|
||||
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.id, 'remote')).called(1);
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.id, 'remote', 'sha')).called(1);
|
||||
verifyNoMoreInteractions(mockAssetService);
|
||||
});
|
||||
|
||||
test('stacks the still of a live photo, not its video', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final asset = LocalAssetStub.image1.copyWith(checksum: 'sha');
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/still.heic');
|
||||
final videoFile = File('/path/to/motion.mov');
|
||||
@@ -456,7 +456,7 @@ void main() {
|
||||
onStatus(TaskStatusUpdate(video!, TaskStatus.complete, null, '{"id": "video"}'));
|
||||
onStatus(TaskStatusUpdate(still!, TaskStatus.complete, null, '{"id": "still"}'));
|
||||
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.id, 'still')).called(1);
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.id, 'still', 'sha')).called(1);
|
||||
verifyNoMoreInteractions(mockAssetService);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ void main() {
|
||||
mockConnectivityApi = MockConnectivityApi();
|
||||
mockAssetMediaRepository = MockAssetMediaRepository();
|
||||
mockAssetService = MockAssetService();
|
||||
when(() => mockAssetService.stackEditedUpload(any(), any())).thenAnswer((_) async {});
|
||||
when(() => mockAssetService.stackEditedUpload(any(), any(), any())).thenAnswer((_) async {});
|
||||
|
||||
sut = ForegroundUploadService(
|
||||
mockUploadRepository,
|
||||
@@ -203,7 +203,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('stacks a plain photo after its upload', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final asset = LocalAssetStub.image1.copyWith(checksum: 'sha');
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/photo.jpg');
|
||||
|
||||
@@ -216,12 +216,12 @@ void main() {
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.localId!, 'remote-1')).called(1);
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.localId!, 'remote-1', 'sha')).called(1);
|
||||
verifyNoMoreInteractions(mockAssetService);
|
||||
});
|
||||
|
||||
test('stacks the still of a live photo, not its video', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final asset = LocalAssetStub.image1.copyWith(checksum: 'sha');
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/still.heic');
|
||||
final videoFile = File('/path/to/motion.mov');
|
||||
@@ -236,7 +236,7 @@ void main() {
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.localId!, 'remote-2')).called(1);
|
||||
verify(() => mockAssetService.stackEditedUpload(asset.localId!, 'remote-2', 'sha')).called(1);
|
||||
verifyNoMoreInteractions(mockAssetService);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:drift/native.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
@@ -119,4 +120,54 @@ void main() {
|
||||
verifyNever(() => mocks.trashedAsset.applyTrashedAssets(any()));
|
||||
});
|
||||
});
|
||||
|
||||
group('AssetService.stackEditedUpload', () {
|
||||
const stack = StackResponse(id: 'stack', primaryAssetId: 'remote', assetIds: ['remote', 'previous']);
|
||||
|
||||
setUp(() {
|
||||
when(() => mocks.localAsset.repo.updatePreviousChecksum(any(), any())).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
test('stacks over the previous version, then records the uploaded checksum', () async {
|
||||
when(() => mocks.localAsset.repo.getPreviousRemoteId('local')).thenAnswer((_) async => 'previous');
|
||||
when(() => apiRepository.stack(any())).thenAnswer((_) async => stack);
|
||||
|
||||
await sut.stackEditedUpload('local', 'remote', 'sha');
|
||||
|
||||
verifyInOrder([
|
||||
() => apiRepository.stack(['remote', 'previous']),
|
||||
() => mocks.localAsset.repo.updatePreviousChecksum('local', 'sha'),
|
||||
]);
|
||||
verifyNever(() => apiRepository.getChecksum(any()));
|
||||
});
|
||||
|
||||
test('records the uploaded checksum when there is nothing to stack on', () async {
|
||||
when(() => mocks.localAsset.repo.getPreviousRemoteId('local')).thenAnswer((_) async => null);
|
||||
|
||||
await sut.stackEditedUpload('local', 'remote', 'sha');
|
||||
|
||||
verifyNever(() => apiRepository.stack(any()));
|
||||
verify(() => mocks.localAsset.repo.updatePreviousChecksum('local', 'sha')).called(1);
|
||||
});
|
||||
|
||||
test('records the uploaded checksum even when the stack call fails', () async {
|
||||
when(() => mocks.localAsset.repo.getPreviousRemoteId('local')).thenAnswer((_) async => 'previous');
|
||||
when(() => apiRepository.stack(any())).thenThrow(Exception('offline'));
|
||||
|
||||
await expectLater(sut.stackEditedUpload('local', 'remote', 'sha'), throwsException);
|
||||
|
||||
verify(() => mocks.localAsset.repo.updatePreviousChecksum('local', 'sha')).called(1);
|
||||
});
|
||||
|
||||
test('asks the server for the checksum when the upload did not know it', () async {
|
||||
when(() => mocks.localAsset.repo.getPreviousRemoteId('local')).thenAnswer((_) async => 'previous');
|
||||
when(() => apiRepository.getChecksum('remote')).thenAnswer((_) async => 'srv');
|
||||
when(() => apiRepository.stack(any())).thenAnswer((_) async => stack);
|
||||
|
||||
await sut.stackEditedUpload('local', 'remote', null);
|
||||
|
||||
verify(() => apiRepository.stack(['remote', 'previous'])).called(1);
|
||||
verify(() => mocks.localAsset.repo.updatePreviousChecksum('local', 'srv')).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user