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'; 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 get authStateChanges => Stream.value(_authState); @override bool get isConfigured => true; @override bool get isEmailVerified => true; @override Future getIdToken([bool forceRefresh = false]) async => idTokenToReturn; @override Future createUserWithEmailAndPassword( String email, String password, { String? displayName, }) async {} @override Future deleteCurrentUser() async {} @override void dispose() {} @override Future ensureBackendIdentity() async {} @override Future reauthenticate({String? password}) async {} @override Future reloadCurrentUser() async {} @override Future sendEmailVerification() async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signInAnonymously() async => setGuest(); @override Future signInWithEmailAndPassword( String email, String password, ) async => setAuthenticated('user_123', email); @override Future signInWithGoogle() async => setAuthenticated('user_123', 'rider@google.com'); @override Future 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 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.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( '404 Not Found

404 Not Found

cloudflare', 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'); }, ); }); }