import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:drift/native.dart'; import 'package:drift/drift.dart' as drift; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/auth/auth_state.dart'; import 'package:motorcycle_dash/services/auth/auth_service.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.dart'; /// In-memory Mock AuthService for unit testing without live network / Firebase credentials class FakeAuthService implements AuthService { @override bool get isEmailVerified => false; @override Future sendEmailVerification() async {} @override Future reloadCurrentUser() async {} @override Future reauthenticate({String? password}) async {} @override Future deleteCurrentUser() async {} final _stateController = StreamController.broadcast(); AuthState _currentState = const AuthState.unauthenticated(isFirebaseConfigured: true); bool _isConfigured = true; @override Stream get authStateChanges => _stateController.stream; @override AuthState get currentState => _currentState; @override AuthUser? get currentUser => _currentState.user; @override bool get isConfigured => _isConfigured; void setConfigured(bool val) { _isConfigured = val; if (!val) { _emit(const AuthState.unauthenticated(isFirebaseConfigured: false)); } } void _emit(AuthState state) { _currentState = state; _stateController.add(state); } @override Future signInWithEmailAndPassword(String email, String password) async { if (!_isConfigured) { _emit(const AuthState.error( 'Cloud authentication unavailable in offline mode.', isFirebaseConfigured: false, )); return; } _emit(const AuthState.loading(isFirebaseConfigured: true)); if (email == 'rider@motorsignal.app' && password == 'superSecret123') { final user = AuthUser( uid: 'user_12345', email: email, displayName: 'Track Rider', providerId: 'password', createdAt: DateTime(2026, 1, 1), ); _emit(AuthState.authenticated(user, isFirebaseConfigured: true)); } else { _emit(const AuthState.error( 'Invalid credentials. Please verify your email and password.', isFirebaseConfigured: true, )); } } @override Future createUserWithEmailAndPassword( String email, String password, { String? displayName, }) async { if (!_isConfigured) { _emit(const AuthState.error( 'Registration unavailable offline.', isFirebaseConfigured: false, )); return; } _emit(const AuthState.loading(isFirebaseConfigured: true)); if (email.contains('@')) { final user = AuthUser( uid: 'new_user_${DateTime.now().millisecondsSinceEpoch}', email: email, displayName: displayName ?? 'New Rider', providerId: 'password', createdAt: DateTime.now(), ); _emit(AuthState.authenticated(user, isFirebaseConfigured: true)); } else { _emit(const AuthState.error('The email address format is invalid.', isFirebaseConfigured: true)); } } @override Future signInWithGoogle() async { if (!_isConfigured) { _emit(const AuthState.error( 'Google Sign-In unavailable without configuration.', isFirebaseConfigured: false, )); return; } _emit(const AuthState.loading(isFirebaseConfigured: true)); final user = AuthUser( uid: 'google_rider_99', email: 'pilot@gmail.com', displayName: 'Apex Pilot', photoUrl: 'https://lh3.googleusercontent.com/a/avatar.png', providerId: 'google.com', createdAt: DateTime(2026, 3, 15), ); _emit(AuthState.authenticated(user, isFirebaseConfigured: true)); } @override Future signInAnonymously() async { _emit(const AuthState.loading(isFirebaseConfigured: true)); final guest = AuthUser( uid: 'anon_guest_777', isAnonymous: true, providerId: 'anonymous', ); _emit(AuthState.anonymous(guest, isFirebaseConfigured: _isConfigured)); } @override Future sendPasswordResetEmail(String email) async { if (!email.contains('@')) { _emit(const AuthState.error('The email address format is invalid.')); } } @override Future getIdToken([bool forceRefresh = false]) async { if (_currentState.user == null) return null; return 'fake_id_token_${_currentState.user!.uid}'; } @override Future signOut() async { _emit(AuthState.unauthenticated(isFirebaseConfigured: _isConfigured)); } @override void dispose() { _stateController.close(); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('MotorSignal Phase 1 — Authentication & Identity Abstraction Tests', () { test('1. AuthUser Model Serialization & Attributes', () { final now = DateTime(2026, 8, 31, 10, 0, 0); final user = AuthUser( uid: 'usr_8821', email: 'ducati_rider@motorsignal.app', displayName: 'Desmo Pilot', photoUrl: 'https://motorsignal.app/avatar.png', isAnonymous: false, providerId: 'password', createdAt: now, lastSignInTime: now, ); expect(user.uid, equals('usr_8821')); expect(user.hasIdentifiedAccount, isTrue); expect(user.displayLabel, equals('Desmo Pilot')); final json = user.toJson(); final reconstructed = AuthUser.fromJson(json); expect(reconstructed.uid, equals(user.uid)); expect(reconstructed.email, equals(user.email)); expect(reconstructed.displayName, equals(user.displayName)); expect(reconstructed.isAnonymous, isFalse); expect(reconstructed.providerId, equals('password')); }); test('2. AuthUser Anonymous & Fallback Display Labels', () { const anonUser = AuthUser( uid: 'anon_3312', isAnonymous: true, ); expect(anonUser.hasIdentifiedAccount, isFalse); expect(anonUser.displayLabel, equals('Anonymous Rider')); const emailOnlyUser = AuthUser( uid: 'uid_554', email: 'r6@track.org', ); expect(emailOnlyUser.displayLabel, equals('r6@track.org')); const rawUser = AuthUser(uid: 'uid_999'); expect(rawUser.displayLabel, equals('Rider (uid_999)')); }); test('3. AuthState Status Flags & Immutability', () { const initialState = AuthState.initial(); expect(initialState.isUnauthenticated, isTrue); expect(initialState.isAuthenticated, isFalse); expect(initialState.isLoading, isFalse); final user = AuthUser(uid: 'u1', email: 'test@motor.com'); final authedState = AuthState.authenticated(user); expect(authedState.isAuthenticated, isTrue); expect(authedState.isAnonymous, isFalse); expect(authedState.user?.email, equals('test@motor.com')); final errorState = AuthState.error('Network failure'); expect(errorState.hasError, isTrue); expect(errorState.errorMessage, equals('Network failure')); final clearedError = errorState.copyWith(clearError: true); expect(clearedError.hasError, isFalse); expect(clearedError.errorMessage, isNull); }); test('4. Riverpod AuthNotifier & State Transitions', () async { final fakeAuth = FakeAuthService(); final container = ProviderContainer( overrides: [ authServiceProvider.overrideWithValue(fakeAuth), ], ); addTearDown(container.dispose); // Initial state expect(container.read(isAuthenticatedProvider), isFalse); expect(container.read(currentUserProvider), isNull); // Successful sign in final notifier = container.read(authNotifierProvider.notifier); await notifier.signInWithEmailAndPassword('rider@motorsignal.app', 'superSecret123'); expect(container.read(isAuthenticatedProvider), isTrue); expect(container.read(currentUserProvider)?.email, equals('rider@motorsignal.app')); expect(container.read(currentUserProvider)?.displayName, equals('Track Rider')); // Sign out await notifier.signOut(); expect(container.read(isAuthenticatedProvider), isFalse); expect(container.read(currentUserProvider), isNull); // Google sign-in await notifier.signInWithGoogle(); expect(container.read(isAuthenticatedProvider), isTrue); expect(container.read(currentUserProvider)?.providerId, equals('google.com')); // Guest / Anonymous mode await notifier.signInAnonymously(); expect(container.read(isAnonymousUserProvider), isTrue); expect(container.read(isAuthenticatedProvider), isFalse); // Failed sign in await notifier.signInWithEmailAndPassword('wrong@email.com', 'badpass'); expect(container.read(authNotifierProvider).hasError, isTrue); expect( container.read(authNotifierProvider).errorMessage, contains('Invalid credentials'), ); }); test('5. Unconfigured Offline Fallback Mode Handling', () async { final fakeAuth = FakeAuthService(); fakeAuth.setConfigured(false); final container = ProviderContainer( overrides: [ authServiceProvider.overrideWithValue(fakeAuth), ], ); addTearDown(container.dispose); final state = container.read(authNotifierProvider); expect(state.isFirebaseConfigured, isFalse); expect(state.isUnauthenticated, isTrue); final notifier = container.read(authNotifierProvider.notifier); await notifier.signInWithEmailAndPassword('pilot@motorsignal.app', 'pass123'); expect(container.read(authNotifierProvider).hasError, isTrue); expect( container.read(authNotifierProvider).errorMessage, contains('offline mode'), ); }); test('6. Authoritative Telemetry & Drift SQLite Isolation Guarantee', () async { // Confirm that local Drift database operates seamlessly with zero auth final db = AppDatabase.forTesting(NativeDatabase.memory()); addTearDown(db.close); final startTime = DateTime(2026, 8, 31, 8, 0, 0); final rideId = await db.into(db.rides).insert(RidesCompanion.insert( startTime: startTime, distanceMiles: const drift.Value(12.4), topSpeedMph: const drift.Value(68.5), maxLeanLeft: const drift.Value(24.2), maxLeanRight: const drift.Value(28.7), maxAccelG: const drift.Value(0.55), maxBrakingG: const drift.Value(-0.85), )); expect(rideId, isPositive); // Verify unauthenticated ride was persisted locally to Drift Schema 5 final rides = await db.select(db.rides).get(); expect(rides.length, equals(1)); expect(rides.first.id, equals(rideId)); expect(rides.first.topSpeedMph, equals(68.5)); expect(rides.first.maxLeanRight, equals(28.7)); // Verify 20 Hz sub-second samples persist locally without any auth checks final sampleTime = startTime.add(const Duration(milliseconds: 50)); await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert( rideId: rideId, timestamp: sampleTime, latitude: const drift.Value(37.7749), longitude: const drift.Value(-122.4194), speed: const drift.Value(68.5), leanAngle: const drift.Value(28.7), longitudinalG: const drift.Value(0.2), lateralG: const drift.Value(0.4), )); final samples = await db.select(db.telemetrySamples).get(); expect(samples.length, equals(1)); expect(samples.first.speed, equals(68.5)); }); }); }