Compare commits

...

2 Commits

Author SHA1 Message Date
shenlong-tanwen
7447d407eb remove button from sync status page 2025-12-05 23:24:45 +05:30
shenlong-tanwen
3a796f6a3d fix test 2025-12-05 23:23:27 +05:30
2 changed files with 250 additions and 15 deletions

View File

@@ -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)),

View File

@@ -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'));
});
});
}