import 'dart:io'; import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.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/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 '../repository.mocks.dart'; import '../mocks/asset_entity.mock.dart'; 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, ); 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); }); }); }