import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart' as http_testing; 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/r2_upload_service.dart'; import 'package:motorcycle_dash/services/sync/sync_models.dart'; import 'package:motorcycle_dash/services/sync/sync_outbox.dart'; import 'package:motorcycle_dash/services/sync/sync_manager.dart'; import 'package:motorcycle_dash/services/sync/sync_provider.dart'; class MockSecurityAuthService implements AuthService { AuthUser? _user; String? _token; void setUser(AuthUser? user, {String? token}) { _user = user; _token = token ?? (user != null ? 'jwt_token_${user.uid}' : null); } @override bool get isConfigured => true; @override AuthUser? get currentUser => _user; @override AuthState get currentState => _user == null ? const AuthState.unauthenticated() : (_user!.isAnonymous ? AuthState.anonymous(_user!) : AuthState.authenticated(_user!)); @override Stream get authStateChanges => Stream.value(currentState); @override Future getIdToken([bool forceRefresh = false]) async => _token; @override Future createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signInAnonymously() async { setUser(const AuthUser(uid: 'anon_rider_777', isAnonymous: true)); } @override Future signInWithEmailAndPassword(String email, String password) async { setUser(AuthUser(uid: 'authenticated_pilot_888', email: email)); } @override Future signInWithGoogle() async { setUser(const AuthUser(uid: 'google_rider_999', email: 'google@test.com')); } @override Future signOut() async { _user = null; _token = null; } @override void dispose() {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('MotorSignal Phase 2E — Cloud Sync Security & R2 Gateway Audit Tests', () { late SharedPreferences prefs; late MockSecurityAuthService authService; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); authService = MockSecurityAuthService(); }); // 1. Valid Firebase ID Token Flow test('Security 1: Valid Firebase JWT ID token is transmitted in Bearer header', () async { authService.setUser( const AuthUser(uid: 'pilot_alpha', email: 'pilot@apex.com'), token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbW90b2FwcC1kMjJlMCIsImF1ZCI6Im1vdG9hcHAtZDIyZTAiLCJzdWIiOiJwaWxvdF9hbHBoYSIsImV4cCI6OTk5OTk5OTk5OX0.dummy_sig', ); final token = await authService.getIdToken(); expect(token, isNotNull); expect(token!.startsWith('eyJ'), isTrue); final mockClient = http_testing.MockClient((request) async { expect(request.headers['Authorization'], equals('Bearer $token')); expect(request.headers['X-Remote-Ride-Id'], equals('ride_42_20260831_120000')); expect(request.headers['X-Bundle-Sha256'], equals('valid_sha256')); expect(request.headers['X-App-Version'], equals('1.0.2')); return http.Response( jsonEncode({ 'success': true, 'objectKey': 'users/pilot_alpha/rides/ride_42_20260831_120000/telemetry.zip', 'size': 2048, }), 200, ); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: token, remoteRideId: 'ride_42_20260831_120000', zipBytes: Uint8List.fromList([1, 2, 3, 4, 5]), sha256Hash: 'valid_sha256', appVersion: '1.0.2', ); expect(result.success, isTrue); expect(result.objectKey, equals('users/pilot_alpha/rides/ride_42_20260831_120000/telemetry.zip')); }); // 2. Missing / Malformed Authorization Header Handling test('Security 2: Missing or malformed token returns 401 and transitions to authRequired', () async { final mockClient = http_testing.MockClient((request) async { return http.Response( jsonEncode({'error': 'Missing or malformed Authorization header'}), 401, ); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: '', remoteRideId: 'ride_1_20260831_120000', zipBytes: Uint8List.fromList([1, 2]), sha256Hash: 'dummy', appVersion: '1.0.2', ); expect(result.success, isFalse); expect(result.requiresAuth, isTrue); expect(result.statusCode, equals(401)); }); // 3. Expired or Invalid JWT Token test('Security 3: Expired or invalid token returns 401 authRequired', () async { final mockClient = http_testing.MockClient((request) async { return http.Response( jsonEncode({'error': 'Invalid or expired Firebase authentication token'}), 401, ); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: 'expired.jwt.token', remoteRideId: 'ride_1_20260831_120000', zipBytes: Uint8List.fromList([1, 2]), sha256Hash: 'dummy', appVersion: '1.0.2', ); expect(result.success, isFalse); expect(result.requiresAuth, isTrue); }); // 4. Malformed Remote Ride ID / Path Traversal Attack Rejection test('Security 4: Malicious remoteRideId is rejected with HTTP 400 permanent failure', () async { final mockClient = http_testing.MockClient((request) async { final rideId = request.headers['X-Remote-Ride-Id']; if (rideId == null || rideId.contains('..') || rideId.contains('/')) { return http.Response(jsonEncode({'error': 'Invalid or malicious ride ID'}), 400); } return http.Response('OK', 200); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: 'valid_token', remoteRideId: '../../users/victim/rides/ride_1', zipBytes: Uint8List.fromList([1, 2]), sha256Hash: 'dummy', appVersion: '1.0.2', ); expect(result.success, isFalse); expect(result.isRetryable, isFalse); expect(result.statusCode, equals(400)); }); // 5. Cloudflare R2 / Worker 5xx Temporary Server Failure test('Security 5: Server 500 error classifies as retryable temporary failure', () async { final mockClient = http_testing.MockClient((request) async { return http.Response('Cloudflare Worker Exception', 500); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: 'valid_token', remoteRideId: 'ride_5_20260831_120000', zipBytes: Uint8List.fromList([1, 2]), sha256Hash: 'dummy', appVersion: '1.0.2', ); expect(result.success, isFalse); expect(result.isRetryable, isTrue); expect(result.requiresAuth, isFalse); }); // 6. Network Timeout Handling test('Security 6: Gateway timeout classifies as retryable temporary failure', () async { final mockClient = http_testing.MockClient((request) async { return http.Response('Gateway Timeout', 504); }); final service = R2UploadService(client: mockClient); final result = await service.uploadTelemetryBundle( idToken: 'valid_token', remoteRideId: 'ride_6_20260831_120000', zipBytes: Uint8List.fromList([1, 2]), sha256Hash: 'dummy', appVersion: '1.0.2', ); expect(result.success, isFalse); expect(result.isRetryable, isTrue); }); // 7. SHA-256 Exact Byte Integrity Verification test('Security 7: SHA-256 is computed accurately on exact ZIP bytes', () { final testBytes = utf8.encode('MOTORSIGNAL_20HZ_TELEMETRY_FORENSIC_BUNDLE'); final expectedSha = sha256.convert(testBytes).toString(); final computed = sha256.convert(Uint8List.fromList(testBytes)).toString(); expect(computed, equals(expectedSha)); expect(computed.length, equals(64)); }); // 8. Outbox Backward Compatibility & Composite Key Isolation test('Security 8: Outbox preserves composite key \${localRideId}_\${providerId}', () async { final outbox = SyncOutbox(prefs); await outbox.enqueue(501, 'firebase'); await outbox.enqueue(501, 'github'); expect(outbox.count, equals(2)); expect(outbox.getItem(501, 'firebase'), isNotNull); expect(outbox.getItem(501, 'github'), isNotNull); expect(outbox.getItem(501, 'firebase')!.key, equals('501_firebase')); expect(outbox.getItem(501, 'github')!.key, equals('501_github')); }); // 9. Multi-Provider SyncManager Resilience test('Security 9: Provider failure does not contaminate other providers', () async { final outbox = SyncOutbox(prefs); final p1 = FakeSyncProvider(providerId: 'firebase', behavior: FakeSyncBehavior.temporaryFailure); final p2 = FakeSyncProvider(providerId: 'github', behavior: FakeSyncBehavior.success); final manager = SyncManager(outbox, initialProviders: [p1, p2]); await manager.enqueueRide(502, targetProviderId: 'firebase', autoProcess: false); await manager.enqueueRide(502, targetProviderId: 'github', autoProcess: false); await manager.processQueue(); expect(outbox.getItem(502, 'firebase')!.status, equals(SyncStatus.retryWait)); expect(outbox.getItem(502, 'github')!.status, equals(SyncStatus.synced)); }); }); }