You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
14 KiB

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:dio/dio.dart';
import 'package:app_boilerplate/core/exceptions/immich_exception.dart';
import 'package:app_boilerplate/data/immich/immich_service.dart';
import 'package:app_boilerplate/data/immich/models/immich_asset.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:app_boilerplate/data/local/models/item.dart';
import 'package:path/path.dart' as path;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() {
// Initialize Flutter bindings and sqflite for testing
TestWidgetsFlutterBinding.ensureInitialized();
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
late LocalStorageService localStorage;
late Directory testDir;
late String testDbPath;
late Directory testCacheDir;
setUp(() async {
// Create temporary directory for testing
testDir = await Directory.systemTemp.createTemp('immich_test_');
testDbPath = path.join(testDir.path, 'test_local_storage.db');
testCacheDir = Directory(path.join(testDir.path, 'image_cache'));
// Initialize local storage
localStorage = LocalStorageService(
testDbPath: testDbPath,
testCacheDir: testCacheDir,
);
await localStorage.initialize();
});
tearDown(() async {
await localStorage.close();
try {
if (await testDir.exists()) {
await testDir.delete(recursive: true);
}
} catch (_) {
// Ignore cleanup errors
}
});
group('ImmichService - Upload', () {
/// Tests successful image upload.
test('uploadImage - success', () async {
// Arrange
final testFile = File(path.join(testDir.path, 'test_image.jpg'));
await testFile.writeAsBytes([1, 2, 3, 4, 5]);
final mockDio = _createMockDio(
onPost: (path, data) => Response(
statusCode: 200,
data: {
'id': 'asset-123',
'duplicate': false,
},
requestOptions: RequestOptions(path: path),
),
onGet: (path) => Response(
statusCode: 200,
data: {
'id': 'asset-123',
'originalFileName': 'test_image.jpg',
'createdAt': DateTime.now().toIso8601String(),
'fileSizeByte': 5,
'mimeType': 'image/jpeg',
},
requestOptions: RequestOptions(path: path),
),
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final response = await immichService.uploadImage(testFile);
// Assert
expect(response.id, equals('asset-123'));
expect(response.duplicate, isFalse);
// Verify metadata was stored locally
final cachedAsset = await immichService.getCachedAsset('asset-123');
expect(cachedAsset, isNotNull);
expect(cachedAsset!.id, equals('asset-123'));
});
/// Tests upload failure when file doesn't exist.
test('uploadImage - file not found', () async {
// Arrange
final nonExistentFile = File(path.join(testDir.path, 'non_existent.jpg'));
final mockDio = _createMockDio();
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act & Assert
expect(
() => immichService.uploadImage(nonExistentFile),
throwsA(isA<ImmichException>()),
);
});
/// Tests upload failure with network error.
test('uploadImage - network error', () async {
// Arrange
final testFile = File(path.join(testDir.path, 'test_image.jpg'));
await testFile.writeAsBytes([1, 2, 3]);
final mockDio = _createMockDio(
onPost: (path, data) => throw DioException(
requestOptions: RequestOptions(path: path),
error: 'Network error',
),
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act & Assert
expect(
() => immichService.uploadImage(testFile),
throwsA(isA<ImmichException>()),
);
});
/// Tests upload with duplicate detection.
test('uploadImage - duplicate asset', () async {
// Arrange
final testFile = File(path.join(testDir.path, 'test_image.jpg'));
await testFile.writeAsBytes([1, 2, 3]);
final mockDio = _createMockDio(
onPost: (path, data) => Response(
statusCode: 200,
data: {
'id': 'asset-456',
'duplicate': true,
},
requestOptions: RequestOptions(path: path),
),
onGet: (path) => Response(
statusCode: 200,
data: {
'id': 'asset-456',
'originalFileName': 'test_image.jpg',
'createdAt': DateTime.now().toIso8601String(),
'fileSizeByte': 3,
'mimeType': 'image/jpeg',
},
requestOptions: RequestOptions(path: path),
),
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final response = await immichService.uploadImage(testFile);
// Assert
expect(response.duplicate, isTrue);
});
});
group('ImmichService - Fetch Assets', () {
/// Tests successful asset fetch.
test('fetchAssets - success', () async {
// Arrange
final mockAssets = [
{
'id': 'asset-1',
'originalFileName': 'image1.jpg',
'createdAt': DateTime.now().toIso8601String(),
'fileSizeByte': 1000,
'mimeType': 'image/jpeg',
},
{
'id': 'asset-2',
'originalFileName': 'image2.jpg',
'createdAt': DateTime.now().toIso8601String(),
'fileSizeByte': 2000,
'mimeType': 'image/png',
},
];
final mockDio = _createMockDio(
onPost: (path, data) {
if (path == '/api/search/metadata') {
// Return the correct nested response structure
return Response(
statusCode: 200,
data: {
'assets': {
'items': mockAssets,
'total': mockAssets.length,
'count': mockAssets.length,
'nextPage': null,
}
},
requestOptions: RequestOptions(path: path),
);
}
return Response(
statusCode: 200,
data: {},
requestOptions: RequestOptions(path: path),
);
},
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final assets = await immichService.fetchAssets(limit: 100);
// Assert
expect(assets.length, equals(2));
expect(assets[0].id, equals('asset-1'));
expect(assets[1].id, equals('asset-2'));
// Verify assets were cached locally
final cachedAssets = await immichService.getCachedAssets();
expect(cachedAssets.length, equals(2));
});
/// Tests fetch with pagination.
test('fetchAssets - pagination', () async {
// Arrange
final mockDio = _createMockDio(
onPost: (path, data) {
if (path == '/api/search/metadata') {
// Extract limit and skip from request body (data), not query params
final requestData = data as Map<String, dynamic>;
final limit = requestData['limit'] as int? ?? 100;
final skip = requestData['skip'] as int? ?? 0;
return Response(
statusCode: 200,
data: {
'assets': {
'items': List.generate(
limit,
(i) => {
'id': 'asset-${skip + i}',
'originalFileName': 'image${skip + i}.jpg',
'createdAt': DateTime.now().toIso8601String(),
'fileSizeByte': 1000,
'mimeType': 'image/jpeg',
},
),
'total': 100, // Mock total
'count': limit,
'nextPage': null,
}
},
requestOptions: RequestOptions(path: path),
);
}
return Response(
statusCode: 200,
data: {},
requestOptions: RequestOptions(path: path),
);
},
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final assets1 = await immichService.fetchAssets(limit: 10, skip: 0);
final assets2 = await immichService.fetchAssets(limit: 10, skip: 10);
// Assert
expect(assets1.length, equals(10));
expect(assets2.length, equals(10));
expect(assets1[0].id, equals('asset-0'));
expect(assets2[0].id, equals('asset-10'));
});
/// Tests fetch failure with server error.
test('fetchAssets - server error', () async {
// Arrange
final mockDio = _createMockDio(
onPost: (path, data) {
if (path == '/api/search/metadata') {
return Response(
statusCode: 500,
data: {
'error': 'Internal server error',
'message': 'Internal server error'
},
requestOptions: RequestOptions(path: path),
);
}
return Response(
statusCode: 200,
data: {},
requestOptions: RequestOptions(path: path),
);
},
);
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act & Assert
expect(
() => immichService.fetchAssets(),
throwsA(isA<ImmichException>()),
);
});
});
group('ImmichService - Cached Assets', () {
/// Tests getting cached asset by ID.
test('getCachedAsset - success', () async {
// Arrange - store asset in local storage
final asset = ImmichAsset(
id: 'asset-789',
fileName: 'cached_image.jpg',
createdAt: DateTime.now(),
fileSize: 5000,
mimeType: 'image/jpeg',
);
final item = Item(
id: 'immich_asset-789',
data: {
'type': 'immich_asset',
'asset': asset.toJson(),
},
);
await localStorage.insertItem(item);
final mockDio = _createMockDio();
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final cached = await immichService.getCachedAsset('asset-789');
// Assert
expect(cached, isNotNull);
expect(cached!.id, equals('asset-789'));
expect(cached.fileName, equals('cached_image.jpg'));
});
/// Tests getting cached asset when not found.
test('getCachedAsset - not found', () async {
// Arrange
final mockDio = _createMockDio();
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final cached = await immichService.getCachedAsset('non-existent');
// Assert
expect(cached, isNull);
});
/// Tests getting all cached assets.
test('getCachedAssets - returns all cached', () async {
// Arrange - store multiple assets
final assets = [
ImmichAsset(
id: 'asset-1',
fileName: 'image1.jpg',
createdAt: DateTime.now(),
fileSize: 1000,
mimeType: 'image/jpeg',
),
ImmichAsset(
id: 'asset-2',
fileName: 'image2.jpg',
createdAt: DateTime.now(),
fileSize: 2000,
mimeType: 'image/png',
),
];
for (final asset in assets) {
final item = Item(
id: 'immich_${asset.id}',
data: {
'type': 'immich_asset',
'asset': asset.toJson(),
},
);
await localStorage.insertItem(item);
}
final mockDio = _createMockDio();
final immichService = ImmichService(
baseUrl: 'https://immich.example.com',
apiKey: 'test-api-key',
localStorage: localStorage,
dio: mockDio,
);
// Act
final cached = await immichService.getCachedAssets();
// Assert
expect(cached.length, equals(2));
expect(cached.map((a) => a.id).toList(),
containsAll(['asset-1', 'asset-2']));
});
});
}
/// Helper to create a mock Dio instance for testing.
Dio _createMockDio({
Response Function(String path, dynamic data)? onPost,
Response Function(String path)? onGet,
}) {
final dio = Dio();
dio.options.baseUrl = 'https://immich.example.com';
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
if (options.method == 'POST' && onPost != null) {
final response = onPost(options.path, options.data);
handler.resolve(response);
} else if (options.method == 'GET' && onGet != null) {
// Build full URL with query parameters for GET requests
final queryString = options.queryParameters.entries
.map((e) => '${e.key}=${e.value}')
.join('&');
final fullPath = queryString.isNotEmpty
? '${options.path}?$queryString'
: options.path;
final response = onGet(fullPath);
handler.resolve(response);
} else {
handler.next(options);
}
},
));
return dio;
}

Powered by TurnKey Linux.