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 {} } File? _findBackupFile() { final candidates = [ r'C:\Users\Bill\Desktop\flutter-app\output\master-consolidation-20261012\backup-071303\motorsignal.sqlite', 'output/master-consolidation-20261012/backup-071303/motorsignal.sqlite', '../backup-071303/motorsignal.sqlite', '../../output/master-consolidation-20261012/backup-071303/motorsignal.sqlite', ]; for (final path in candidates) { final file = File(path); if (file.existsSync()) { return file; } } return null; } void main() { TestWidgetsFlutterBinding.ensureInitialized(); final originalFile = _findBackupFile(); test( 'Phase 6 — Disposable Backup Verification & Outbox Recovery Gate', () async { expect(originalFile, isNotNull, reason: 'Original backup file must exist when test is executed'); final file = originalFile!; // 1. Verify original SHA-256 before copy final originalBytes = await file.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 file.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 file.readAsBytes(); final postHash = sha256.convert(postBytes).toString(); expect(postHash, equals(expectedHash), reason: 'Original backup MUST remain 100% UNTOUCHED'); }, skip: originalFile == null ? 'Real Pixel backup not present in this environment (e.g. CI runner)' : null, ); }