mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 01:10:00 +03:00
Compare commits
2 Commits
d47e9a5db4
...
7447d407eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7447d407eb | ||
|
|
3a796f6a3d |
@@ -13,7 +13,6 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
|
||||
@@ -26,8 +25,6 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverInfo = ref.read(serverInfoProvider);
|
||||
|
||||
Future<void> exportDatabase() async {
|
||||
try {
|
||||
// WAL Checkpoint to ensure all changes are written to the database
|
||||
@@ -154,17 +151,6 @@ class SyncStatusAndActions extends HookConsumerWidget {
|
||||
ref.read(backgroundSyncProvider).hashAssets();
|
||||
},
|
||||
),
|
||||
if (CurrentPlatform.isIOS && serverInfo.serverVersion.isAtLeast(major: 2, minor: 4))
|
||||
ListTile(
|
||||
title: Text(
|
||||
"sync_cloud_ids".t(context: context),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
leading: const Icon(Icons.keyboard_command_key_rounded),
|
||||
subtitle: Text("tap_to_run_job".t(context: context)),
|
||||
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus),
|
||||
onTap: ref.read(backgroundSyncProvider).syncCloudIds,
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
const SizedBox(height: 24),
|
||||
_SectionHeaderText(text: "actions".t(context: context)),
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
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';
|
||||
@@ -16,8 +24,29 @@ 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';
|
||||
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;
|
||||
@@ -62,6 +91,7 @@ void main() {
|
||||
mockLocalAssetRepository,
|
||||
mockAppSettingsService,
|
||||
mockAssetMediaRepository,
|
||||
_serverInfo,
|
||||
);
|
||||
|
||||
mockUploadRepository.onUploadStatus = (_) {};
|
||||
@@ -165,4 +195,223 @@ void main() {
|
||||
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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user