MotorSignal-App/test/disposable_backup_recovery_...

217 lines
7.3 KiB
Dart

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<AuthState> get authStateChanges => Stream.value(currentState);
@override
bool get isConfigured => true;
@override
bool get isEmailVerified => true;
@override
Future<void> createUserWithEmailAndPassword(
String email,
String password, {
String? displayName,
}) async {}
@override
Future<void> deleteCurrentUser() async {}
@override
void dispose() {}
@override
Future<void> ensureBackendIdentity() async {}
@override
Future<String?> getIdToken([bool forceRefresh = false]) async => 'fake_token';
@override
Future<void> reauthenticate({String? password}) async {}
@override
Future<void> reloadCurrentUser() async {}
@override
Future<void> sendEmailVerification() async {}
@override
Future<void> sendPasswordResetEmail(String email) async {}
@override
Future<void> signInAnonymously() async {}
@override
Future<void> signInWithEmailAndPassword(
String email,
String password,
) async {}
@override
Future<void> signInWithGoogle() async {}
@override
Future<void> 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',
);
});
}