mirror of
https://github.com/immich-app/immich.git
synced 2025-12-12 17:23:11 +03:00
418 lines
17 KiB
Dart
418 lines
17 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
|
import 'package:immich_mobile/entities/store.entity.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
|
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
|
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
|
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
|
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
import 'package:immich_mobile/services/upload.service.dart';
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
import '../domain/service.mock.dart';
|
|
import '../fixtures/asset.stub.dart';
|
|
import '../infrastructure/repository.mock.dart';
|
|
import '../mocks/asset_entity.mock.dart';
|
|
import '../repository.mocks.dart';
|
|
|
|
// Test ServerInfo stub
|
|
const _serverInfo = ServerInfo(
|
|
serverVersion: ServerVersion(major: 2, minor: 4, patch: 0),
|
|
latestVersion: ServerVersion(major: 2, minor: 4, patch: 0),
|
|
serverFeatures: ServerFeatures(trash: true, map: true, oauthEnabled: false, passwordLogin: true, ocr: false),
|
|
serverConfig: ServerConfig(
|
|
trashDays: 30,
|
|
oauthButtonText: 'Login with OAuth',
|
|
externalDomain: '',
|
|
mapDarkStyleUrl: '',
|
|
mapLightStyleUrl: '',
|
|
),
|
|
serverDiskInfo: ServerDiskInfo(
|
|
diskAvailable: '100GB',
|
|
diskSize: '500GB',
|
|
diskUse: '400GB',
|
|
diskUsagePercentage: 80.0,
|
|
),
|
|
versionStatus: VersionStatus.upToDate,
|
|
);
|
|
|
|
void main() {
|
|
late UploadService sut;
|
|
late MockUploadRepository mockUploadRepository;
|
|
late MockDriftBackupRepository mockBackupRepository;
|
|
late MockStorageRepository mockStorageRepository;
|
|
late MockDriftLocalAssetRepository mockLocalAssetRepository;
|
|
late MockAppSettingsService mockAppSettingsService;
|
|
late MockAssetMediaRepository mockAssetMediaRepository;
|
|
late Drift db;
|
|
|
|
setUpAll(() async {
|
|
registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos);
|
|
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
const MethodChannel('plugins.flutter.io/path_provider'),
|
|
(MethodCall methodCall) async => 'test',
|
|
);
|
|
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
|
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
|
|
|
await Store.put(StoreKey.serverEndpoint, 'http://test-server.com');
|
|
await Store.put(StoreKey.deviceId, 'test-device-id');
|
|
});
|
|
|
|
setUp(() {
|
|
mockUploadRepository = MockUploadRepository();
|
|
mockBackupRepository = MockDriftBackupRepository();
|
|
mockStorageRepository = MockStorageRepository();
|
|
mockLocalAssetRepository = MockDriftLocalAssetRepository();
|
|
mockAppSettingsService = MockAppSettingsService();
|
|
mockAssetMediaRepository = MockAssetMediaRepository();
|
|
|
|
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false);
|
|
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false);
|
|
|
|
sut = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo,
|
|
);
|
|
|
|
mockUploadRepository.onUploadStatus = (_) {};
|
|
mockUploadRepository.onTaskProgress = (_) {};
|
|
});
|
|
|
|
tearDown(() {
|
|
sut.dispose();
|
|
});
|
|
|
|
group('getUploadTask', () {
|
|
test('should call getOriginalFilename from AssetMediaRepository for regular photo', () async {
|
|
final asset = LocalAssetStub.image1;
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/file.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg');
|
|
|
|
final task = await sut.getUploadTask(asset);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields['filename'], equals('OriginalPhoto.jpg'));
|
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
|
});
|
|
|
|
test('should call getOriginalFilename when original filename is null', () async {
|
|
final asset = LocalAssetStub.image2;
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/file.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
|
|
|
final task = await sut.getUploadTask(asset);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields['filename'], equals(asset.name));
|
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
|
});
|
|
|
|
test('should call getOriginalFilename for live photo', () async {
|
|
final asset = LocalAssetStub.image1;
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/file.mov');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => mockFile);
|
|
when(
|
|
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
|
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
|
|
|
final task = await sut.getUploadTask(asset);
|
|
expect(task, isNotNull);
|
|
// For live photos, extension should be changed to match the video file
|
|
expect(task!.fields['filename'], equals('OriginalLivePhoto.mov'));
|
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
|
});
|
|
});
|
|
|
|
group('getLivePhotoUploadTask', () {
|
|
test('should call getOriginalFilename for live photo upload task', () async {
|
|
final asset = LocalAssetStub.image1;
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/livephoto.heic');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
|
when(
|
|
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
|
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
|
|
|
final task = await sut.getLivePhotoUploadTask(asset, 'video-id-123');
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
|
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
|
});
|
|
|
|
test('should call getOriginalFilename when original filename is null', () async {
|
|
final asset = LocalAssetStub.image2;
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/fallback.heic');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
|
|
|
final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456');
|
|
expect(task, isNotNull);
|
|
// Should fall back to asset.name when original filename is null
|
|
expect(task!.fields['filename'], equals(asset.name));
|
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
|
});
|
|
});
|
|
|
|
group('Server Info - cloudId and eTag metadata', () {
|
|
test('should include cloudId and eTag metadata on iOS when server version is 2.4+', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
final sutWithV24 = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo,
|
|
);
|
|
addTearDown(() => sutWithV24.dispose());
|
|
|
|
final assetWithCloudId = LocalAsset(
|
|
id: 'test-asset-id',
|
|
name: 'test.jpg',
|
|
type: AssetType.image,
|
|
createdAt: DateTime(2025, 1, 1),
|
|
updatedAt: DateTime(2025, 1, 2),
|
|
cloudId: 'cloud-id-123',
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
);
|
|
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/test.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
|
|
|
final task = await sutWithV24.getUploadTask(assetWithCloudId);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields.containsKey('metadata'), isTrue);
|
|
|
|
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
|
expect(metadata, hasLength(1));
|
|
expect(metadata[0]['key'], equals('mobile-app'));
|
|
expect(metadata[0]['value']['iCloudId'], equals('cloud-id-123'));
|
|
expect(metadata[0]['value']['eTag'], isNotNull);
|
|
});
|
|
|
|
test('should NOT include metadata on iOS when server version is below 2.4', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
final sutWithV23 = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo.copyWith(
|
|
serverVersion: const ServerVersion(major: 2, minor: 3, patch: 0),
|
|
latestVersion: const ServerVersion(major: 2, minor: 3, patch: 0),
|
|
),
|
|
);
|
|
addTearDown(() => sutWithV23.dispose());
|
|
|
|
final assetWithCloudId = LocalAsset(
|
|
id: 'test-asset-id',
|
|
name: 'test.jpg',
|
|
type: AssetType.image,
|
|
createdAt: DateTime(2025, 1, 1),
|
|
updatedAt: DateTime(2025, 1, 2),
|
|
cloudId: 'cloud-id-123',
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
);
|
|
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/test.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
|
|
|
final task = await sutWithV23.getUploadTask(assetWithCloudId);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields.containsKey('metadata'), isFalse);
|
|
});
|
|
|
|
test('should NOT include metadata on Android regardless of server version', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
final sutAndroid = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo,
|
|
);
|
|
addTearDown(() => sutAndroid.dispose());
|
|
|
|
final assetWithCloudId = LocalAsset(
|
|
id: 'test-asset-id',
|
|
name: 'test.jpg',
|
|
type: AssetType.image,
|
|
createdAt: DateTime(2025, 1, 1),
|
|
updatedAt: DateTime(2025, 1, 2),
|
|
cloudId: 'cloud-id-123',
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
);
|
|
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/test.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
|
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
|
|
|
final task = await sutAndroid.getUploadTask(assetWithCloudId);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields.containsKey('metadata'), isFalse);
|
|
});
|
|
|
|
test('should NOT include metadata when cloudId is null even on iOS with server 2.4+', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
final sutWithV24 = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo,
|
|
);
|
|
addTearDown(() => sutWithV24.dispose());
|
|
|
|
final assetWithoutCloudId = LocalAsset(
|
|
id: 'test-asset-id',
|
|
name: 'test.jpg',
|
|
type: AssetType.image,
|
|
createdAt: DateTime(2025, 1, 1),
|
|
updatedAt: DateTime(2025, 1, 2),
|
|
cloudId: null, // No cloudId
|
|
);
|
|
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/test.jpg');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithoutCloudId)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile);
|
|
when(
|
|
() => mockAssetMediaRepository.getOriginalFilename(assetWithoutCloudId.id),
|
|
).thenAnswer((_) async => 'test.jpg');
|
|
|
|
final task = await sutWithV24.getUploadTask(assetWithoutCloudId);
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields.containsKey('metadata'), isFalse);
|
|
});
|
|
|
|
test('should include metadata for live photos with cloudId on iOS 2.4+', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
|
|
final sutWithV24 = UploadService(
|
|
mockUploadRepository,
|
|
mockBackupRepository,
|
|
mockStorageRepository,
|
|
mockLocalAssetRepository,
|
|
mockAppSettingsService,
|
|
mockAssetMediaRepository,
|
|
_serverInfo,
|
|
);
|
|
addTearDown(() => sutWithV24.dispose());
|
|
|
|
final assetWithCloudId = LocalAsset(
|
|
id: 'test-livephoto-id',
|
|
name: 'livephoto.heic',
|
|
type: AssetType.image,
|
|
createdAt: DateTime(2025, 1, 1),
|
|
updatedAt: DateTime(2025, 1, 2),
|
|
cloudId: 'cloud-id-livephoto',
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
);
|
|
|
|
final mockEntity = MockAssetEntity();
|
|
final mockFile = File('/path/to/livephoto.heic');
|
|
|
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
|
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
|
when(
|
|
() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id),
|
|
).thenAnswer((_) async => 'livephoto.heic');
|
|
|
|
final task = await sutWithV24.getLivePhotoUploadTask(assetWithCloudId, 'video-123');
|
|
|
|
expect(task, isNotNull);
|
|
expect(task!.fields.containsKey('metadata'), isTrue);
|
|
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
|
|
|
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
|
expect(metadata, hasLength(1));
|
|
expect(metadata[0]['key'], equals('mobile-app'));
|
|
expect(metadata[0]['value']['iCloudId'], equals('cloud-id-livephoto'));
|
|
});
|
|
});
|
|
}
|