Phase 7 - Added Firebase layer

This commit is contained in:
gitea
2025-11-05 21:53:38 +01:00
parent 13ec9e5a9d
commit 5d3f8c68bd
14 changed files with 1255 additions and 5 deletions
+2
View File
@@ -14,6 +14,7 @@ void main() {
expect(config.immichBaseUrl, isNotEmpty);
expect(config.immichApiKey, isNotEmpty);
expect(config.nostrRelays, isNotEmpty);
expect(config.firebaseConfig, isNotNull);
});
/// Tests that loading 'prod' environment returns the correct configuration.
@@ -27,6 +28,7 @@ void main() {
expect(config.immichBaseUrl, isNotEmpty);
expect(config.immichApiKey, isNotEmpty);
expect(config.nostrRelays, isNotEmpty);
expect(config.firebaseConfig, isNotNull);
});
/// Tests that loading configuration is case-insensitive.
@@ -0,0 +1,310 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:app_boilerplate/data/firebase/firebase_service.dart';
import 'package:app_boilerplate/data/firebase/models/firebase_config.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:app_boilerplate/data/local/models/item.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart' as path;
import 'firebase_service_test.mocks.dart';
@GenerateMocks([LocalStorageService])
void main() {
// Initialize Flutter bindings and sqflite for testing
TestWidgetsFlutterBinding.ensureInitialized();
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
late MockLocalStorageService mockLocalStorage;
late FirebaseService firebaseService;
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('firebase_test_');
mockLocalStorage = MockLocalStorageService();
});
tearDown(() async {
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
});
group('FirebaseService - Configuration', () {
test('service is disabled when config.enabled is false', () {
final config = FirebaseConfig.disabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(service.isEnabled, isFalse);
expect(service.isLoggedIn, isFalse);
});
test('service can be enabled with config', () {
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Service is not initialized yet, so isEnabled is false
expect(service.isEnabled, isFalse);
});
});
group('FirebaseService - Initialization', () {
test('initialize does nothing when Firebase is disabled', () async {
final config = FirebaseConfig.disabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Should not throw even though Firebase is not set up
// (In real app, Firebase.initializeApp() would fail, but we're testing disabled case)
expect(() => service.initialize(), returnsNormally);
});
test('initialize fails gracefully when Firebase not configured', () async {
// This test verifies that when Firebase is enabled but not configured,
// the service handles the error gracefully
// Note: In real scenarios, Firebase.initializeApp() requires actual config files
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// In a test environment without Firebase config, this will fail
// but that's expected - Firebase requires actual project setup
expect(
() => service.initialize(),
throwsA(isA<FirebaseException>()),
);
});
});
group('FirebaseService - Authentication', () {
test('loginWithEmailPassword throws when auth disabled', () async {
final config = FirebaseConfig(
enabled: true,
authEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.loginWithEmailPassword(
email: '[email protected]',
password: 'password123',
),
throwsA(isA<FirebaseException>()),
);
});
test('loginWithEmailPassword throws when not initialized', () async {
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.loginWithEmailPassword(
email: '[email protected]',
password: 'password123',
),
throwsA(isA<FirebaseException>()),
);
});
test('logout throws when auth disabled', () async {
final config = FirebaseConfig(
enabled: true,
authEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.logout(),
throwsA(isA<FirebaseException>()),
);
});
});
group('FirebaseService - Firestore Sync', () {
test('syncItemsToFirestore throws when Firestore disabled', () async {
final config = FirebaseConfig(
enabled: true,
firestoreEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.syncItemsToFirestore('user1'),
throwsA(isA<FirebaseException>()),
);
});
test('syncItemsToFirestore throws when not initialized', () async {
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.syncItemsToFirestore('user1'),
throwsA(isA<FirebaseException>()),
);
});
test('syncItemsFromFirestore throws when Firestore disabled', () async {
final config = FirebaseConfig(
enabled: true,
firestoreEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.syncItemsFromFirestore('user1'),
throwsA(isA<FirebaseException>()),
);
});
test('syncItemsFromFirestore throws when not initialized', () async {
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
expect(
() => service.syncItemsFromFirestore('user1'),
throwsA(isA<FirebaseException>()),
);
});
});
group('FirebaseService - Storage', () {
test('uploadFile throws when Storage disabled', () async {
final config = FirebaseConfig(
enabled: true,
storageEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
final testFile = File(path.join(tempDir.path, 'test.txt'));
await testFile.writeAsString('test content');
expect(
() => service.uploadFile(testFile, 'test/path.txt'),
throwsA(isA<FirebaseException>()),
);
});
test('uploadFile throws when not initialized', () async {
final config = FirebaseConfig.enabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
final testFile = File(path.join(tempDir.path, 'test.txt'));
await testFile.writeAsString('test content');
expect(
() => service.uploadFile(testFile, 'test/path.txt'),
throwsA(isA<FirebaseException>()),
);
});
});
group('FirebaseService - Messaging', () {
test('getFcmToken returns null when messaging disabled', () async {
final config = FirebaseConfig(
enabled: true,
messagingEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
final token = await service.getFcmToken();
expect(token, isNull);
});
});
group('FirebaseService - Analytics', () {
test('logEvent does nothing when Analytics disabled', () async {
final config = FirebaseConfig(
enabled: true,
analyticsEnabled: false,
);
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Should not throw even though Analytics is disabled
await service.logEvent('test_event');
});
test('logEvent does nothing when Firebase disabled', () async {
final config = FirebaseConfig.disabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Should not throw even though Firebase is disabled
await service.logEvent('test_event');
});
});
group('FirebaseService - Offline Scenarios', () {
test('service maintains offline-first behavior when disabled', () async {
final config = FirebaseConfig.disabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Service should not interfere with local storage operations
expect(service.isEnabled, isFalse);
expect(service.isLoggedIn, isFalse);
});
test('service can be disposed safely', () async {
final config = FirebaseConfig.disabled();
final service = FirebaseService(
config: config,
localStorage: mockLocalStorage,
);
// Should not throw
await service.dispose();
});
});
}
@@ -0,0 +1,174 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in app_boilerplate/test/data/firebase/firebase_service_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i4;
import 'dart:io' as _i2;
import 'package:app_boilerplate/data/local/local_storage_service.dart' as _i3;
import 'package:app_boilerplate/data/local/models/item.dart' as _i5;
import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
class _FakeFile_0 extends _i1.SmartFake implements _i2.File {
_FakeFile_0(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
/// A class which mocks [LocalStorageService].
///
/// See the documentation for Mockito's code generation for more information.
class MockLocalStorageService extends _i1.Mock
implements _i3.LocalStorageService {
MockLocalStorageService() {
_i1.throwOnMissingStub(this);
}
@override
_i4.Future<void> initialize({
String? sessionDbPath,
_i2.Directory? sessionCacheDir,
}) =>
(super.noSuchMethod(
Invocation.method(
#initialize,
[],
{
#sessionDbPath: sessionDbPath,
#sessionCacheDir: sessionCacheDir,
},
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> reinitializeForSession({
required String? newDbPath,
required _i2.Directory? newCacheDir,
}) =>
(super.noSuchMethod(
Invocation.method(
#reinitializeForSession,
[],
{
#newDbPath: newDbPath,
#newCacheDir: newCacheDir,
},
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> clearAllData() => (super.noSuchMethod(
Invocation.method(
#clearAllData,
[],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> insertItem(_i5.Item? item) => (super.noSuchMethod(
Invocation.method(
#insertItem,
[item],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<_i5.Item?> getItem(String? id) => (super.noSuchMethod(
Invocation.method(
#getItem,
[id],
),
returnValue: _i4.Future<_i5.Item?>.value(),
) as _i4.Future<_i5.Item?>);
@override
_i4.Future<List<_i5.Item>> getAllItems() => (super.noSuchMethod(
Invocation.method(
#getAllItems,
[],
),
returnValue: _i4.Future<List<_i5.Item>>.value(<_i5.Item>[]),
) as _i4.Future<List<_i5.Item>>);
@override
_i4.Future<void> deleteItem(String? id) => (super.noSuchMethod(
Invocation.method(
#deleteItem,
[id],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> updateItem(_i5.Item? item) => (super.noSuchMethod(
Invocation.method(
#updateItem,
[item],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<_i2.File> getCachedImage(String? url) => (super.noSuchMethod(
Invocation.method(
#getCachedImage,
[url],
),
returnValue: _i4.Future<_i2.File>.value(_FakeFile_0(
this,
Invocation.method(
#getCachedImage,
[url],
),
)),
) as _i4.Future<_i2.File>);
@override
_i4.Future<void> clearImageCache() => (super.noSuchMethod(
Invocation.method(
#clearImageCache,
[],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> close() => (super.noSuchMethod(
Invocation.method(
#close,
[],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
}