parent
13ec9e5a9d
commit
5d3f8c68bd
@ -0,0 +1,353 @@
|
||||
import 'dart:io';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import '../local/local_storage_service.dart';
|
||||
import '../local/models/item.dart';
|
||||
import '../session/models/user.dart';
|
||||
import 'models/firebase_config.dart';
|
||||
|
||||
/// Exception thrown when Firebase operations fail.
|
||||
class FirebaseException implements Exception {
|
||||
/// Error message.
|
||||
final String message;
|
||||
|
||||
/// Creates a [FirebaseException] with the provided message.
|
||||
FirebaseException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'FirebaseException: $message';
|
||||
}
|
||||
|
||||
/// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics).
|
||||
///
|
||||
/// This service provides:
|
||||
/// - Cloud Firestore for optional metadata sync and backup
|
||||
/// - Firebase Storage for optional media storage
|
||||
/// - Firebase Authentication for user login/logout
|
||||
/// - Firebase Cloud Messaging for push notifications
|
||||
/// - Firebase Analytics for optional analytics
|
||||
///
|
||||
/// The service is modular and optional - can be enabled/disabled without affecting other modules.
|
||||
/// When disabled, all methods return safely without throwing errors.
|
||||
///
|
||||
/// The service maintains offline-first behavior by syncing with local storage
|
||||
/// and only using Firebase as an optional cloud backup/sync layer.
|
||||
class FirebaseService {
|
||||
/// Firebase configuration (determines which services are enabled).
|
||||
final FirebaseConfig config;
|
||||
|
||||
/// Local storage service for offline-first behavior.
|
||||
final LocalStorageService localStorage;
|
||||
|
||||
/// Firestore instance (null if not enabled).
|
||||
FirebaseFirestore? _firestore;
|
||||
|
||||
/// Firebase Storage instance (null if not enabled).
|
||||
FirebaseStorage? _storage;
|
||||
|
||||
/// Firebase Auth instance (null if not enabled).
|
||||
firebase_auth.FirebaseAuth? _auth;
|
||||
|
||||
/// Firebase Messaging instance (null if not enabled).
|
||||
FirebaseMessaging? _messaging;
|
||||
|
||||
/// Firebase Analytics instance (null if not enabled).
|
||||
FirebaseAnalytics? _analytics;
|
||||
|
||||
/// Whether Firebase has been initialized.
|
||||
bool _initialized = false;
|
||||
|
||||
/// Current user from Firebase Auth (if enabled).
|
||||
firebase_auth.User? _firebaseUser;
|
||||
|
||||
/// Creates a [FirebaseService] instance.
|
||||
///
|
||||
/// [config] - Firebase configuration (determines which services are enabled).
|
||||
/// [localStorage] - Local storage service for offline-first behavior.
|
||||
FirebaseService({
|
||||
required this.config,
|
||||
required this.localStorage,
|
||||
});
|
||||
|
||||
/// Gets the current Firebase Auth user (null if not logged in or auth disabled).
|
||||
firebase_auth.User? get currentFirebaseUser => _firebaseUser;
|
||||
|
||||
/// Checks if Firebase is enabled and initialized.
|
||||
bool get isEnabled => config.enabled && _initialized;
|
||||
|
||||
/// Checks if a user is logged in via Firebase Auth.
|
||||
bool get isLoggedIn => _auth != null && _firebaseUser != null;
|
||||
|
||||
/// Initializes Firebase services based on configuration.
|
||||
///
|
||||
/// Must be called before using any Firebase services.
|
||||
/// If Firebase is disabled, this method does nothing.
|
||||
///
|
||||
/// Throws [FirebaseException] if initialization fails.
|
||||
Future<void> initialize() async {
|
||||
if (!config.enabled) {
|
||||
return; // Firebase disabled, nothing to initialize
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize Firebase Core (required for all services)
|
||||
await Firebase.initializeApp();
|
||||
|
||||
// Initialize enabled services
|
||||
if (config.firestoreEnabled) {
|
||||
_firestore = FirebaseFirestore.instance;
|
||||
// Enable offline persistence for Firestore
|
||||
_firestore!.settings = const Settings(
|
||||
persistenceEnabled: true,
|
||||
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.storageEnabled) {
|
||||
_storage = FirebaseStorage.instance;
|
||||
}
|
||||
|
||||
if (config.authEnabled) {
|
||||
_auth = firebase_auth.FirebaseAuth.instance;
|
||||
// Listen for auth state changes
|
||||
_auth!.authStateChanges().listen((firebase_auth.User? user) {
|
||||
_firebaseUser = user;
|
||||
});
|
||||
_firebaseUser = _auth!.currentUser;
|
||||
}
|
||||
|
||||
if (config.messagingEnabled) {
|
||||
_messaging = FirebaseMessaging.instance;
|
||||
// Request notification permissions
|
||||
await _messaging!.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.analyticsEnabled) {
|
||||
_analytics = FirebaseAnalytics.instance;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to initialize Firebase: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs in a user with email and password.
|
||||
///
|
||||
/// [email] - User email address.
|
||||
/// [password] - User password.
|
||||
///
|
||||
/// Returns the Firebase Auth user.
|
||||
///
|
||||
/// Throws [FirebaseException] if auth is disabled or login fails.
|
||||
Future<firebase_auth.User> loginWithEmailPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
if (!config.enabled || !config.authEnabled) {
|
||||
throw FirebaseException('Firebase Auth is not enabled');
|
||||
}
|
||||
|
||||
if (!_initialized || _auth == null) {
|
||||
throw FirebaseException('Firebase not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
final credential = await _auth!.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
_firebaseUser = credential.user;
|
||||
return _firebaseUser!;
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to login: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs out the current user.
|
||||
///
|
||||
/// Throws [FirebaseException] if auth is disabled or logout fails.
|
||||
Future<void> logout() async {
|
||||
if (!config.enabled || !config.authEnabled) {
|
||||
throw FirebaseException('Firebase Auth is not enabled');
|
||||
}
|
||||
|
||||
if (!_initialized || _auth == null) {
|
||||
throw FirebaseException('Firebase not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
await _auth!.signOut();
|
||||
_firebaseUser = null;
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to logout: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncs local items to Firestore (cloud backup).
|
||||
///
|
||||
/// [userId] - User ID to associate items with (for multi-user support).
|
||||
///
|
||||
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
||||
Future<void> syncItemsToFirestore(String userId) async {
|
||||
if (!config.enabled || !config.firestoreEnabled) {
|
||||
throw FirebaseException('Firestore is not enabled');
|
||||
}
|
||||
|
||||
if (!_initialized || _firestore == null) {
|
||||
throw FirebaseException('Firestore not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all local items
|
||||
final items = await localStorage.getAllItems();
|
||||
|
||||
// Batch write to Firestore
|
||||
final batch = _firestore!.batch();
|
||||
final collection = _firestore!.collection('users').doc(userId).collection('items');
|
||||
|
||||
for (final item in items) {
|
||||
final docRef = collection.doc(item.id);
|
||||
batch.set(docRef, {
|
||||
'id': item.id,
|
||||
'data': item.data,
|
||||
'created_at': item.createdAt,
|
||||
'updated_at': item.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
await batch.commit();
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to sync items to Firestore: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Syncs items from Firestore to local storage.
|
||||
///
|
||||
/// [userId] - User ID to fetch items for.
|
||||
///
|
||||
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
||||
Future<void> syncItemsFromFirestore(String userId) async {
|
||||
if (!config.enabled || !config.firestoreEnabled) {
|
||||
throw FirebaseException('Firestore is not enabled');
|
||||
}
|
||||
|
||||
if (!_initialized || _firestore == null) {
|
||||
throw FirebaseException('Firestore not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
final snapshot = await _firestore!
|
||||
.collection('users')
|
||||
.doc(userId)
|
||||
.collection('items')
|
||||
.get();
|
||||
|
||||
for (final doc in snapshot.docs) {
|
||||
final data = doc.data();
|
||||
final item = Item(
|
||||
id: data['id'] as String,
|
||||
data: data['data'] as Map<String, dynamic>,
|
||||
createdAt: data['created_at'] as int,
|
||||
updatedAt: data['updated_at'] as int,
|
||||
);
|
||||
|
||||
// Only insert if not already in local storage (avoid duplicates)
|
||||
final existing = await localStorage.getItem(item.id);
|
||||
if (existing == null) {
|
||||
await localStorage.insertItem(item);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to sync items from Firestore: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads a file to Firebase Storage.
|
||||
///
|
||||
/// [file] - File to upload.
|
||||
/// [path] - Storage path (e.g., 'users/userId/media/image.jpg').
|
||||
///
|
||||
/// Returns the download URL.
|
||||
///
|
||||
/// Throws [FirebaseException] if Storage is disabled or upload fails.
|
||||
Future<String> uploadFile(File file, String path) async {
|
||||
if (!config.enabled || !config.storageEnabled) {
|
||||
throw FirebaseException('Firebase Storage is not enabled');
|
||||
}
|
||||
|
||||
if (!_initialized || _storage == null) {
|
||||
throw FirebaseException('Firebase Storage not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
try {
|
||||
final ref = _storage!.ref().child(path);
|
||||
await ref.putFile(file);
|
||||
return await ref.getDownloadURL();
|
||||
} catch (e) {
|
||||
throw FirebaseException('Failed to upload file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the FCM token for push notifications.
|
||||
///
|
||||
/// Returns the FCM token, or null if messaging is disabled.
|
||||
Future<String?> getFcmToken() async {
|
||||
if (!config.enabled || !config.messagingEnabled || _messaging == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await _messaging!.getToken();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs an event to Firebase Analytics.
|
||||
///
|
||||
/// [eventName] - Name of the event.
|
||||
/// [parameters] - Optional event parameters.
|
||||
///
|
||||
/// Does nothing if Analytics is disabled.
|
||||
Future<void> logEvent(String eventName, {Map<String, dynamic>? parameters}) async {
|
||||
if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
|
||||
Map<String, Object>? analyticsParams;
|
||||
if (parameters != null) {
|
||||
analyticsParams = parameters.map((key, value) => MapEntry(key, value as Object));
|
||||
}
|
||||
|
||||
await _analytics!.logEvent(
|
||||
name: eventName,
|
||||
parameters: analyticsParams,
|
||||
);
|
||||
} catch (e) {
|
||||
// Silently fail - analytics failures shouldn't break the app
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes of Firebase resources.
|
||||
///
|
||||
/// Should be called when the service is no longer needed.
|
||||
Future<void> dispose() async {
|
||||
if (_auth != null) {
|
||||
await _auth!.signOut();
|
||||
}
|
||||
_firebaseUser = null;
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
/// Configuration for Firebase services.
|
||||
///
|
||||
/// This model holds Firebase configuration options and feature flags
|
||||
/// to enable/disable specific Firebase services.
|
||||
class FirebaseConfig {
|
||||
/// Whether Firebase is enabled (all services disabled if false).
|
||||
final bool enabled;
|
||||
|
||||
/// Whether Firestore cloud sync is enabled.
|
||||
final bool firestoreEnabled;
|
||||
|
||||
/// Whether Firebase Storage is enabled.
|
||||
final bool storageEnabled;
|
||||
|
||||
/// Whether Firebase Authentication is enabled.
|
||||
final bool authEnabled;
|
||||
|
||||
/// Whether Firebase Cloud Messaging (push notifications) is enabled.
|
||||
final bool messagingEnabled;
|
||||
|
||||
/// Whether Firebase Analytics is enabled.
|
||||
final bool analyticsEnabled;
|
||||
|
||||
/// Creates a [FirebaseConfig] instance.
|
||||
///
|
||||
/// [enabled] - Whether Firebase is enabled (default: false).
|
||||
/// [firestoreEnabled] - Whether Firestore is enabled (default: true if enabled).
|
||||
/// [storageEnabled] - Whether Storage is enabled (default: true if enabled).
|
||||
/// [authEnabled] - Whether Auth is enabled (default: true if enabled).
|
||||
/// [messagingEnabled] - Whether Messaging is enabled (default: true if enabled).
|
||||
/// [analyticsEnabled] - Whether Analytics is enabled (default: true if enabled).
|
||||
const FirebaseConfig({
|
||||
this.enabled = false,
|
||||
this.firestoreEnabled = true,
|
||||
this.storageEnabled = true,
|
||||
this.authEnabled = true,
|
||||
this.messagingEnabled = true,
|
||||
this.analyticsEnabled = true,
|
||||
});
|
||||
|
||||
/// Creates a [FirebaseConfig] with all services disabled.
|
||||
const FirebaseConfig.disabled()
|
||||
: enabled = false,
|
||||
firestoreEnabled = false,
|
||||
storageEnabled = false,
|
||||
authEnabled = false,
|
||||
messagingEnabled = false,
|
||||
analyticsEnabled = false;
|
||||
|
||||
/// Creates a [FirebaseConfig] with all services enabled.
|
||||
const FirebaseConfig.enabled()
|
||||
: enabled = true,
|
||||
firestoreEnabled = true,
|
||||
storageEnabled = true,
|
||||
authEnabled = true,
|
||||
messagingEnabled = true,
|
||||
analyticsEnabled = true;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FirebaseConfig(enabled: $enabled, firestore: $firestoreEnabled, '
|
||||
'storage: $storageEnabled, auth: $authEnabled, messaging: $messagingEnabled, '
|
||||
'analytics: $analyticsEnabled)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: 'test@example.com',
|
||||
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: 'test@example.com',
|
||||
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>);
|
||||
}
|
||||
Loading…
Reference in new issue