import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/services/auth/auth_service.dart'; import 'package:motorcycle_dash/services/auth/auth_state.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/sync/sync_manager.dart'; import 'package:motorcycle_dash/services/sync/sync_outbox.dart'; import 'package:motorcycle_dash/services/sync/sync_provider.dart'; class _FakeAuthService implements AuthService { final AuthUser _user; _FakeAuthService([String uid = 'pilot_pixel_8a']) : _user = AuthUser( uid: uid, email: '$uid@example.com', displayName: 'Pixel Pilot', isAnonymous: false, ); @override AuthUser? get currentUser => _user; @override AuthState get currentState => AuthState.authenticated(_user); @override Stream get authStateChanges => Stream.value(currentState); @override bool get isConfigured => true; @override bool get isEmailVerified => true; @override Future createUserWithEmailAndPassword( String email, String password, { String? displayName, }) async {} @override Future deleteCurrentUser() async {} @override void dispose() {} @override Future ensureBackendIdentity() async {} @override Future getIdToken([bool forceRefresh = false]) async => 'fake_token'; @override Future reauthenticate({String? password}) async {} @override Future reloadCurrentUser() async {} @override Future sendEmailVerification() async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signInAnonymously() async {} @override Future signInWithEmailAndPassword( String email, String password, ) async {} @override Future signInWithGoogle() async {} @override Future signOut() async {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('Phase 6 — Disposable Backup Verification & Outbox Recovery Gate', () async { final originalFile = File( r'C:\Users\Bill\Desktop\flutter-app\output\master-consolidation-20261012\backup-071303\motorsignal.sqlite', ); expect( originalFile.existsSync(), isTrue, reason: 'Original backup file must exist', ); // 1. Verify original SHA-256 before copy final originalBytes = await originalFile.readAsBytes(); final originalHash = sha256.convert(originalBytes).toString(); const expectedHash = '7125915195cdcf1de2d351426d8a991789904e7c3befd12af6f0d0f0b2416122'; expect( originalHash, equals(expectedHash), reason: 'Original backup must match authoritative SHA-256', ); // 2. Create disposable copy in scratch directory final scratchDir = Directory( '${Directory.systemTemp.path}/motorsignal_backup_test_${DateTime.now().millisecondsSinceEpoch}', ); if (!scratchDir.existsSync()) { scratchDir.createSync(recursive: true); } final disposableFile = File( '${scratchDir.path}/disposable_motorsignal.sqlite', ); if (disposableFile.existsSync()) { disposableFile.deleteSync(); } await originalFile.copy(disposableFile.path); expect(disposableFile.existsSync(), isTrue); // 3. Open disposable copy via Drift / SQLite final db = AppDatabase.forTesting(NativeDatabase(disposableFile)); try { // Integrity check final integrityRows = await db .customSelect('PRAGMA integrity_check') .get(); final integrityResult = integrityRows.first.data['integrity_check']; expect( integrityResult, equals('ok'), reason: 'SQLite integrity must be OK', ); // Count rides and samples final rideCountRows = await db .customSelect('SELECT COUNT(*) as count FROM rides') .get(); final totalRides = rideCountRows.first.data['count'] as int; expect(totalRides, equals(22), reason: 'Must contain exactly 22 rides'); final completedRidesRows = await db .customSelect( 'SELECT COUNT(*) as count FROM rides WHERE end_time IS NOT NULL', ) .get(); final completedRides = completedRidesRows.first.data['count'] as int; final samplesRows = await db .customSelect('SELECT COUNT(*) as count FROM telemetry_samples') .get(); final totalSamples = samplesRows.first.data['count'] as int; expect( totalSamples, equals(67798), reason: 'Must contain exactly 67,798 telemetry samples', ); // 4. Test Foreign Account Isolation against real Pixel backup // In the real backup, rides are owned by Pixel rider '4btn4W3iOLWgfloMBYYK5hWa1hY2' SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final outbox = SyncOutbox(prefs); final fakeProvider = FakeSyncProvider(providerId: 'firebase'); final foreignAuth = _FakeAuthService('foreign_pilot_123'); final foreignSyncManager = SyncManager( outbox, initialProviders: [fakeProvider], defaultProviderId: 'firebase', db: db, authService: foreignAuth, autoStart: false, ); final foreignReport = await foreignSyncManager.reconcileCompletedRides(); expect(foreignReport.errorCount, equals(0)); expect(foreignReport.discoveredCompletedRides, equals(completedRides)); expect( foreignReport.foreignOwnerSkippedCount, equals(completedRides), reason: 'Foreign account must NEVER claim or touch real Pixel rides', ); expect(foreignReport.totalRecovered, equals(0)); expect(outbox.count, equals(0)); // 5. Test Owner Recovery against disposable backup final ownerAuth = _FakeAuthService('4btn4W3iOLWgfloMBYYK5hWa1hY2'); final ownerSyncManager = SyncManager( outbox, initialProviders: [fakeProvider], defaultProviderId: 'firebase', db: db, authService: ownerAuth, autoStart: false, ); final ownerReport = await ownerSyncManager.reconcileCompletedRides(); expect(ownerReport.errorCount, equals(0)); expect(ownerReport.discoveredCompletedRides, equals(completedRides)); expect(ownerReport.recoveredQueuedCount, equals(completedRides)); expect(ownerReport.totalRecovered, equals(completedRides)); expect(outbox.count, equals(completedRides)); // 6. Test repeated reconciliation idempotence as owner final report2 = await ownerSyncManager.reconcileCompletedRides(); expect(report2.totalRecovered, equals(0)); expect(report2.alreadyTrackedCount, equals(completedRides)); expect(outbox.count, equals(completedRides)); } finally { await db.close(); } // 6. Verify original SHA-256 after operations to prove zero mutation final postBytes = await originalFile.readAsBytes(); final postHash = sha256.convert(postBytes).toString(); expect( postHash, equals(expectedHash), reason: 'Original backup MUST remain 100% UNTOUCHED', ); }); }