MotorSignal-App/test/cloud_confirmation_contract...

1109 lines
35 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:drift/native.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart' as http_testing;
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/firebase_sync_provider.dart';
import 'package:motorcycle_dash/services/sync/r2_upload_service.dart';
import 'package:motorcycle_dash/services/sync/sync_provider.dart';
import 'package:motorcycle_dash/services/sync/sync_models.dart';
class _MockAuthService implements AuthService {
AuthUser? _currentUser;
AuthState _authState = const AuthState.unauthenticated();
String? idTokenToReturn = 'valid_mock_jwt_token';
String? refreshedIdToken;
int getIdTokenCalls = 0;
void setAuthenticated(String uid, String email) {
_currentUser = AuthUser(uid: uid, email: email, isAnonymous: false);
_authState = AuthState.authenticated(_currentUser!);
}
void setGuest() {
_currentUser = const AuthUser(uid: 'guest_uid', isAnonymous: true);
_authState = AuthState.anonymous(_currentUser!);
}
void setSignedOut() {
_currentUser = null;
_authState = const AuthState.unauthenticated();
}
@override
AuthUser? get currentUser => _currentUser;
@override
AuthState get currentState => _authState;
@override
Stream<AuthState> get authStateChanges => Stream.value(_authState);
@override
bool get isConfigured => true;
@override
bool get isEmailVerified => true;
@override
Future<String?> getIdToken([bool forceRefresh = false]) async {
getIdTokenCalls++;
if (forceRefresh && refreshedIdToken != null) {
return refreshedIdToken;
}
return idTokenToReturn;
}
@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<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 => setGuest();
@override
Future<void> signInWithEmailAndPassword(
String email,
String password,
) async => setAuthenticated('user_123', email);
@override
Future<void> signInWithGoogle() async =>
setAuthenticated('user_123', 'rider@google.com');
@override
Future<void> signOut() async => setSignedOut();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Trustworthy Cloud Confirmation & R2 Gateway Verification Contract', () {
late AppDatabase db;
late _MockAuthService authService;
late FakeFirebaseFirestore fakeFirestore;
const testUid = 'pilot_apex_42';
const remoteRideId = 'ride_12345_20261012';
const validSha256 =
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const validSize = 4096;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
authService = _MockAuthService()
..setAuthenticated(testUid, 'pilot@apex.com');
fakeFirestore = FakeFirebaseFirestore();
});
tearDown(() async {
await db.close();
});
Map<String, dynamic> createFirestoreRideData({
String ownerId = testUid,
String rideId = remoteRideId,
String bundleStatus = 'uploaded',
String? objectKey,
String sha256 = validSha256,
int size = validSize,
}) {
final key = objectKey ?? 'users/$ownerId/rides/$rideId/telemetry.zip';
return {
'id': rideId,
'ownerId': ownerId,
'telemetryBundlePath': key,
'telemetryBundle': {
'provider': 'cloudflare-r2',
'objectKey': key,
'status': bundleStatus,
'bundleSizeBytes': size,
'sha256': sha256,
},
};
}
test(
'1. Confirmed: Firestore doc + matching R2 bundle returns confirmed status',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
expect(request.url.path, equals('/r2/verify'));
expect(request.url.queryParameters['rideId'], equals(remoteRideId));
expect(
request.headers['authorization'],
contains('valid_mock_jwt_token'),
);
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': validSize,
'sha256': validSha256,
'etag': '"mock_etag_123"',
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.confirmed));
expect(result.isConfirmed, isTrue);
expect(result.sha256, equals(validSha256));
expect(result.sizeBytes, equals(validSize));
expect(await provider.isRideConfirmedInCloud(remoteRideId), isTrue);
},
);
test(
'2. Missing: Firestore doc exists but R2 bundle is missing (404) returns missing',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'error': 'Telemetry bundle not found in R2',
'code': 'OBJECT_NOT_FOUND',
}),
404,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.missing));
expect(result.isMissing, isTrue);
expect(await provider.isRideConfirmedInCloud(remoteRideId), isFalse);
},
);
test(
'3. Missing: Firestore doc does not exist returns missing without calling R2',
() async {
var r2Called = false;
final mockClient = http_testing.MockClient((request) async {
r2Called = true;
return http.Response('{}', 200);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.missing));
expect(
r2Called,
isFalse,
reason: 'R2 must not be queried when Firestore metadata is absent',
);
},
);
test(
'4. Inconsistent: Checksum mismatch between Firestore and R2 returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData(sha256: validSha256));
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': validSize,
'sha256': 'different_corrupted_hash_9999',
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
expect(result.isInconsistent, isTrue);
expect(await provider.isRideConfirmedInCloud(remoteRideId), isFalse);
},
);
test(
'5. Inconsistent: Object size mismatch returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData(size: 4096));
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': 99999, // Mismatched size
'sha256': validSha256,
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
},
);
test(
'6. Inconsistent: Wrong remote ride identity inside doc metadata returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData(rideId: 'different_ride_999'));
final r2Service = R2UploadService(
client: http_testing.MockClient(
(_) async => http.Response('{}', 200),
),
);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
},
);
test(
'7. Unauthorized: Wrong owner inside doc returns unauthorized',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData(ownerId: 'different_owner_uid'));
final r2Service = R2UploadService(
client: http_testing.MockClient(
(_) async => http.Response('{}', 200),
),
);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.unauthorized));
expect(result.isUnauthorized, isTrue);
},
);
test(
'8. Unauthorized: R2 Worker HTTP 401/403 returns unauthorized',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({'error': 'Unauthorized token'}),
401,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.unauthorized));
},
);
test('9. Rate Limited: HTTP 429 returns temporarilyUnverifiable', () async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response('Rate limit exceeded', 429);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(
result.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
expect(result.isTemporarilyUnverifiable, isTrue);
});
test(
'10. Server Error: HTTP 500/503 returns temporarilyUnverifiable',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response('Cloudflare backend error', 503);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(
result.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
},
);
test(
'11. Network Timeout: Timeout returns temporarilyUnverifiable',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
await Future<void>.delayed(const Duration(milliseconds: 100));
return http.Response('{}', 200);
});
final r2Service = R2UploadService(client: mockClient);
// We test R2 timeout directly
final r2Direct = await r2Service.verifyTelemetryBundle(
idToken: 'token',
remoteRideId: remoteRideId,
timeout: const Duration(milliseconds: 10),
);
expect(
r2Direct.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
},
);
test(
'12. Worker Endpoint Undeployed (404 Endpoint not found) returns temporarilyUnverifiable',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
// Current live worker response when /r2/verify is not yet deployed
return http.Response(
jsonEncode({'error': 'Endpoint not found'}),
404,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(
result.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
expect(result.isConfirmed, isFalse);
},
);
test(
'13. Legacy bundles without optional fields confirm cleanly when R2 verifies',
() async {
// Legacy document without sha256 or bundleSizeBytes in metadata
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set({
'id': remoteRideId,
'ownerId': testUid,
'telemetryBundle': {
'provider': 'cloudflare-r2',
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'status': 'uploaded',
},
});
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': 1024,
'sha256': 'legacy_hash',
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.legacyUnverified));
expect(result.isLegacyUnverified, isTrue);
},
);
test(
'14. Edge Routing: Non-JSON HTML 404 from proxy returns temporarilyUnverifiable instead of missing',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
'<html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1>cloudflare</body></html>',
404,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(
result.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
},
);
test(
'15. uploadRide Protection: Aborts with permanentFailure when cloud confirmation is inconsistent (never overwrites object)',
() async {
final rideId = await db.startRide(
startTime: DateTime.utc(2026, 10, 12, 10, 0, 0),
);
await db.endRide(rideId, const RidesCompanion());
await db.claimRideForCloudOwner(rideId, testUid);
final actualRemoteRideId = await db.ensureCloudRideId(rideId);
// Firestore document exists
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(actualRemoteRideId)
.set(createFirestoreRideData(rideId: actualRemoteRideId));
var uploadAttempted = false;
final mockClient = http_testing.MockClient((request) async {
if (request.url.path == '/r2/verify') {
// Return inconsistent hash
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': actualRemoteRideId,
'objectKey':
'users/$testUid/rides/$actualRemoteRideId/telemetry.zip',
'size': validSize,
'sha256': 'corrupted_hash_mismatch_123',
}),
200,
);
}
if (request.url.path == '/r2/upload') {
uploadAttempted = true;
return http.Response('{}', 200);
}
return http.Response('{}', 404);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.uploadRide(
SyncItem(
localRideId: rideId,
providerId: 'firebase',
createdAt: DateTime.now(),
),
);
expect(result.success, isFalse);
expect(result.isRetryable, isFalse);
expect(result.errorMessage, contains('inconsistent'));
expect(
uploadAttempted,
isFalse,
reason:
'Must NEVER attempt R2 upload when cloud data is inconsistent',
);
},
);
test(
'16. uploadRide Protection: Defers with temporaryFailure when cloud verification is temporarilyUnverifiable (no heavy upload)',
() async {
final rideId = await db.startRide(
startTime: DateTime.utc(2026, 10, 12, 10, 0, 0),
);
await db.endRide(rideId, const RidesCompanion());
await db.claimRideForCloudOwner(rideId, testUid);
final actualRemoteRideId = await db.ensureCloudRideId(rideId);
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(actualRemoteRideId)
.set(createFirestoreRideData(rideId: actualRemoteRideId));
var uploadAttempted = false;
final mockClient = http_testing.MockClient((request) async {
if (request.url.path == '/r2/verify') {
return http.Response('Rate limit exceeded', 429);
}
if (request.url.path == '/r2/upload') {
uploadAttempted = true;
return http.Response('{}', 200);
}
return http.Response('{}', 404);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.uploadRide(
SyncItem(
localRideId: rideId,
providerId: 'firebase',
createdAt: DateTime.now(),
),
);
expect(result.success, isFalse);
expect(result.isRetryable, isTrue);
expect(result.errorMessage, contains('temporarily unavailable'));
expect(
uploadAttempted,
isFalse,
reason: 'Must NOT upload bundle when verification is rate limited',
);
},
);
test(
'17. uploadRide Protection: Returns authRequired when cloud verification is unauthorized',
() async {
final rideId = await db.startRide(
startTime: DateTime.utc(2026, 10, 12, 10, 0, 0),
);
await db.endRide(rideId, const RidesCompanion());
await db.claimRideForCloudOwner(rideId, testUid);
final actualRemoteRideId = await db.ensureCloudRideId(rideId);
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(actualRemoteRideId)
.set(
createFirestoreRideData(
ownerId: 'different_owner_uid',
rideId: actualRemoteRideId,
),
);
var uploadAttempted = false;
final mockClient = http_testing.MockClient((request) async {
if (request.url.path == '/r2/upload') {
uploadAttempted = true;
}
return http.Response('{}', 200);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.uploadRide(
SyncItem(
localRideId: rideId,
providerId: 'firebase',
createdAt: DateTime.now(),
),
);
expect(result.success, isFalse);
expect(result.requiresAuth, isTrue);
expect(uploadAttempted, isFalse);
},
);
test(
'18. Modern bundle (syncVersion >= 3) missing required sha256 returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set({
'id': remoteRideId,
'ownerId': testUid,
'syncVersion': 3,
'bundleSizeBytes': 2048,
'telemetryBundle': {
'provider': 'cloudflare-r2',
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'status': 'uploaded',
'bundleSizeBytes': 2048,
},
});
final mockClient = http_testing.MockClient(
(request) async => http.Response('{}', 200),
);
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
expect(result.isInconsistent, isTrue);
expect(result.message, contains('missing required sha256'));
},
);
test(
'19. Modern bundle (syncVersion >= 3) with non-positive bundleSizeBytes returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set({
'id': remoteRideId,
'ownerId': testUid,
'syncVersion': 3,
'bundleSizeBytes': 0,
'sha256': validSha256,
'telemetryBundle': {
'provider': 'cloudflare-r2',
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'status': 'uploaded',
'bundleSizeBytes': 0,
'sha256': validSha256,
},
});
final mockClient = http_testing.MockClient(
(request) async => http.Response('{}', 200),
);
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
expect(result.isInconsistent, isTrue);
expect(result.message, contains('invalid bundleSizeBytes'));
},
);
test(
'20. R2 Worker response with invalid objectKey pattern returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'wrong_path/telemetry.zip',
'size': validSize,
'sha256': validSha256,
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
expect(result.message, contains('objectKey'));
},
);
test(
'21. R2 Worker response with non-positive size returns inconsistent',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': 0,
'sha256': validSha256,
}),
200,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.inconsistent));
expect(result.message, contains('size'));
},
);
test(
'22. uploadRide Protection: Legacy unverified ride confirms without re-uploading bundle',
() async {
final rideId = await db.startRide(
startTime: DateTime.utc(2026, 10, 12, 10, 0, 0),
);
await db.endRide(rideId, const RidesCompanion());
await db.claimRideForCloudOwner(rideId, testUid);
final actualRemoteRideId = await db.ensureCloudRideId(rideId);
// Legacy Firestore record without sha256
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(actualRemoteRideId)
.set({
'id': actualRemoteRideId,
'ownerId': testUid,
'telemetryBundle': {
'provider': 'cloudflare-r2',
'objectKey':
'users/$testUid/rides/$actualRemoteRideId/telemetry.zip',
'status': 'uploaded',
},
});
var uploadAttempted = false;
final mockClient = http_testing.MockClient((request) async {
if (request.url.path == '/r2/verify') {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': actualRemoteRideId,
'objectKey':
'users/$testUid/rides/$actualRemoteRideId/telemetry.zip',
'size': 1024,
'sha256': 'legacy_hash',
}),
200,
);
}
if (request.url.path == '/r2/upload') {
uploadAttempted = true;
}
return http.Response('{}', 200);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.uploadRide(
SyncItem(
localRideId: rideId,
providerId: 'firebase',
createdAt: DateTime.now(),
),
);
expect(result.success, isTrue);
expect(result.remoteId, equals(actualRemoteRideId));
expect(
uploadAttempted,
isFalse,
reason: 'Must not re-upload existing historical bundle',
);
},
);
test(
'23. Expired Firebase token triggers forceRefresh and confirms on retry',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
authService.idTokenToReturn = 'expired_old_token';
authService.refreshedIdToken = 'fresh_valid_jwt_token';
var callCount = 0;
final mockClient = http_testing.MockClient((request) async {
callCount++;
final authHeader = request.headers['Authorization'];
if (authHeader == 'Bearer expired_old_token') {
return http.Response(
jsonEncode({'error': 'Token expired', 'code': 'UNAUTHORIZED'}),
401,
);
}
if (authHeader == 'Bearer fresh_valid_jwt_token') {
return http.Response(
jsonEncode({
'verified': true,
'remoteRideId': remoteRideId,
'objectKey': 'users/$testUid/rides/$remoteRideId/telemetry.zip',
'size': validSize,
'sha256': validSha256,
}),
200,
);
}
return http.Response('{}', 403);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(result.status, equals(CloudConfirmationStatus.confirmed));
expect(result.isConfirmed, isTrue);
expect(
callCount,
equals(2),
reason: 'Should have retried once with refreshed token',
);
},
);
test(
'24. Worker returns 503 TEMPORARY_ERROR when Firestore temporarily fails (maps to temporarilyUnverifiable)',
() async {
await fakeFirestore
.collection('users')
.doc(testUid)
.collection('rides')
.doc(remoteRideId)
.set(createFirestoreRideData());
final mockClient = http_testing.MockClient((request) async {
return http.Response(
jsonEncode({
'error': 'Firestore temporary lookup error',
'code': 'TEMPORARY_ERROR',
}),
503,
);
});
final r2Service = R2UploadService(client: mockClient);
final provider = FirebaseSyncProvider(
db: db,
authService: authService,
firestore: fakeFirestore,
r2UploadService: r2Service,
);
final result = await provider.verifyCloudConfirmation(remoteRideId);
expect(
result.status,
equals(CloudConfirmationStatus.temporarilyUnverifiable),
);
expect(result.isConfirmed, isFalse);
},
);
});
}