Phase 7 - Added Firebase layer
This commit is contained in:
@@ -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>);
|
||||
}
|
||||
Reference in New Issue
Block a user