import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { assertFails, assertSucceeds, initializeTestEnvironment, } from '@firebase/rules-unit-testing'; import { collection, deleteDoc, doc, getDoc, getDocs, setDoc, updateDoc } from 'firebase/firestore'; import fs from 'node:fs'; import path from 'node:path'; const projectId = 'motorsignal-emulator'; let testEnv; beforeAll(async () => { testEnv = await initializeTestEnvironment({ projectId, firestore: { rules: fs.readFileSync(path.resolve(process.cwd(), '../../firestore.rules'), 'utf8'), }, }); }); afterAll(async () => { await testEnv?.cleanup(); }); afterEach(async () => { await testEnv?.clearFirestore(); }); const cleanupCollections = [ { name: 'rides/{rideId}/events', collectionPath: 'users/alice/rides/ride_cleanup/events', documentPath: 'users/alice/rides/ride_cleanup/events/event_cleanup', }, { name: 'rides/{rideId}/performance', collectionPath: 'users/alice/rides/ride_cleanup/performance', documentPath: 'users/alice/rides/ride_cleanup/performance/performance_cleanup', }, { name: 'rides', collectionPath: 'users/alice/rides', documentPath: 'users/alice/rides/ride_cleanup', }, { name: 'motorcycles', collectionPath: 'users/alice/motorcycles', documentPath: 'users/alice/motorcycles/motorcycle_cleanup', }, { name: 'trips', collectionPath: 'users/alice/trips', documentPath: 'users/alice/trips/trip_cleanup', }, { name: 'events', collectionPath: 'users/alice/events', documentPath: 'users/alice/events/event_cleanup', }, { name: 'friends', collectionPath: 'users/alice/friends', documentPath: 'users/alice/friends/friend_cleanup', }, ]; async function seedCleanupTree() { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'users/alice'), { id: 'alice' }); await setDoc(doc(db, 'users/alice/rides/ride_cleanup'), { id: 'ride_cleanup', ownerId: 'alice', }); await setDoc(doc(db, 'users/alice/rides/ride_cleanup/events/event_cleanup'), { type: 'ride-event', }); await setDoc(doc(db, 'users/alice/rides/ride_cleanup/performance/performance_cleanup'), { type: 'performance-run', }); await setDoc(doc(db, 'users/alice/motorcycles/motorcycle_cleanup'), { ownerId: 'alice', }); await setDoc(doc(db, 'users/alice/trips/trip_cleanup'), { type: 'trip' }); await setDoc(doc(db, 'users/alice/events/event_cleanup'), { type: 'event' }); await setDoc(doc(db, 'users/alice/friends/friend_cleanup'), { type: 'friend' }); }); } describe('MotorCloud ride ownership rules', () => { test('a signed-in owner can create and read their own ride', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const ride = doc(alice, 'users/alice/rides/ride_test_alice'); await assertSucceeds(setDoc(ride, { id: 'ride_test_alice', ownerId: 'alice', startTime: '2026-09-01T00:00:00.000Z', })); await assertSucceeds(getDoc(ride)); }); test('User A cannot read or update User B ride data', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const bobRideForSetup = doc(bob, 'users/bob/rides/ride_bob'); const bobRideForAlice = doc(alice, 'users/bob/rides/ride_bob'); await assertSucceeds(setDoc(bobRideForSetup, { id: 'ride_bob', ownerId: 'bob', startTime: '2026-09-01T00:00:00.000Z', })); await assertFails(getDoc(bobRideForAlice)); await assertFails(updateDoc(bobRideForAlice, { distanceMiles: 999 })); }); test('a caller cannot create a ride with another ownerId', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const forged = doc(alice, 'users/alice/rides/ride_forged'); await assertFails(setDoc(forged, { id: 'ride_forged', ownerId: 'bob', startTime: '2026-09-01T00:00:00.000Z', })); }); test('a caller cannot change an existing ride ownerId', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const ride = doc(alice, 'users/alice/rides/ride_owner_lock'); await assertSucceeds(setDoc(ride, { id: 'ride_owner_lock', ownerId: 'alice', startTime: '2026-09-01T00:00:00.000Z', })); await assertFails(updateDoc(ride, { ownerId: 'bob' })); }); test('signed-out callers cannot access user-owned rides', async () => { const unauthenticated = testEnv.unauthenticatedContext().firestore(); const ride = doc(unauthenticated, 'users/alice/rides/ride_test_alice'); await assertFails(getDoc(ride)); }); test('an owner can list and delete every account-cleanup path', async () => { await seedCleanupTree(); const alice = testEnv.authenticatedContext('alice').firestore(); await assertSucceeds(getDoc(doc(alice, 'users/alice'))); for (const entry of cleanupCollections) { await assertSucceeds(getDocs(collection(alice, entry.collectionPath))); } for (const entry of cleanupCollections) { await assertSucceeds(deleteDoc(doc(alice, entry.documentPath))); } await assertSucceeds(deleteDoc(doc(alice, 'users/alice'))); }); test('another user cannot list or delete any account-cleanup path', async () => { await seedCleanupTree(); const bob = testEnv.authenticatedContext('bob').firestore(); await assertFails(getDoc(doc(bob, 'users/alice'))); await assertFails(deleteDoc(doc(bob, 'users/alice'))); for (const entry of cleanupCollections) { await assertFails(getDocs(collection(bob, entry.collectionPath))); await assertFails(deleteDoc(doc(bob, entry.documentPath))); } }); test('signed-out callers cannot list or delete account-cleanup paths', async () => { await seedCleanupTree(); const signedOut = testEnv.unauthenticatedContext().firestore(); await assertFails(getDoc(doc(signedOut, 'users/alice'))); await assertFails(deleteDoc(doc(signedOut, 'users/alice'))); for (const entry of cleanupCollections) { await assertFails(getDocs(collection(signedOut, entry.collectionPath))); await assertFails(deleteDoc(doc(signedOut, entry.documentPath))); } }); }); describe('Social Architecture Security Rules', () => { test('public profile creation, reading, and privacy protection', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); // Alice creates public profile await assertSucceeds(setDoc(doc(alice, 'public_profiles/alice'), { id: 'alice', username: 'alice_apex', displayName: 'Alice Apex', avatarPreset: 'helmet_cyan', bio: 'Apex hunter', location: 'Atlanta', isSearchableByHandle: true, isProfilePublic: true, allowFriendRequests: true, })); // Bob can read Alice's public profile await assertSucceeds(getDoc(doc(bob, 'public_profiles/alice'))); // Bob CANNOT edit Alice's public profile await assertFails(updateDoc(doc(bob, 'public_profiles/alice'), { bio: 'Hacked' })); // Bob CANNOT list all public profiles await assertFails(getDocs(collection(bob, 'public_profiles'))); // Alice sets isSearchableByHandle = false await assertSucceeds(updateDoc(doc(alice, 'public_profiles/alice'), { isSearchableByHandle: false, })); // Bob can no longer read Alice's non-searchable profile since they are not friends await assertFails(getDoc(doc(bob, 'public_profiles/alice'))); }); test('handle reservation uniqueness and directory scraping prevention', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); // Alice creates public profile first so handle lookup can check searchability await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'public_profiles/alice'), { id: 'alice', username: 'alice_apex', isSearchableByHandle: true, }); }); // Alice claims handle await assertSucceeds(setDoc(doc(alice, 'handles/alice_apex'), { uid: 'alice', handle: 'alice_apex', })); // Bob CANNOT overwrite Alice's handle await assertFails(setDoc(doc(bob, 'handles/alice_apex'), { uid: 'bob', handle: 'alice_apex', })); // Bob CANNOT delete Alice's handle await assertFails(deleteDoc(doc(bob, 'handles/alice_apex'))); // Bob CANNOT list all handles await assertFails(getDocs(collection(bob, 'handles'))); }); test('friend requests: legitimate vs forged vs blocked vs disabled', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); // Seed Bob's public profile accepting requests await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'public_profiles/bob'), { id: 'bob', username: 'bob_rider', allowFriendRequests: true, }); }); // Alice sends legitimate friend request to Bob await assertSucceeds(setDoc(doc(alice, 'users/bob/friend_requests_incoming/alice'), { receiverUid: 'bob', senderUid: 'alice', senderHandle: 'alice_apex', })); // Alice CANNOT forge request pretending to be Charlie await assertFails(setDoc(doc(alice, 'users/bob/friend_requests_incoming/charlie'), { receiverUid: 'bob', senderUid: 'charlie', senderHandle: 'charlie', })); // Alice CANNOT send friend request to herself await assertFails(setDoc(doc(alice, 'users/alice/friend_requests_incoming/alice'), { receiverUid: 'alice', senderUid: 'alice', senderHandle: 'alice', })); }); test('friendship establishment requires incoming request; unilateral declaration fails', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); // Unilateral friendship: Alice tries to add Bob directly as a friend without incoming request -> FAILS await assertFails(setDoc(doc(alice, 'users/alice/friends/bob'), { uid: 'bob', handle: 'bob_rider', })); // Seed incoming request from Alice to Bob await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'users/bob/friend_requests_incoming/alice'), { receiverUid: 'bob', senderUid: 'alice', }); }); // Bob accepts request -> SUCCEEDS because Bob has incoming request from Alice await assertSucceeds(setDoc(doc(bob, 'users/bob/friends/alice'), { uid: 'alice', handle: 'alice_apex', })); // Bob can also create Alice's corresponding friend doc in the mutual acceptance await assertSucceeds(setDoc(doc(bob, 'users/alice/friends/bob'), { uid: 'bob', handle: 'bob_rider', })); // Outsider Charlie CANNOT delete Alice and Bob's friendship const charlie = testEnv.authenticatedContext('charlie').firestore(); await assertFails(deleteDoc(doc(charlie, 'users/alice/friends/bob'))); // Either participant (Alice) can remove friend await assertSucceeds(deleteDoc(doc(alice, 'users/alice/friends/bob'))); }); test('blocking prevents requests, hides profiles, and blocks cannot be forged', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); // Seed Alice's public profile await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'public_profiles/alice'), { id: 'alice', username: 'alice_apex', isSearchableByHandle: true, allowFriendRequests: true, }); await setDoc(doc(db, 'handles/alice_apex'), { uid: 'alice', handle: 'alice_apex', }); }); // Alice blocks Bob await assertSucceeds(setDoc(doc(alice, 'users/alice/blocks/bob'), { blockedUid: 'bob', })); // Bob CANNOT read or delete Alice's block doc await assertFails(getDoc(doc(bob, 'users/alice/blocks/bob'))); await assertFails(deleteDoc(doc(bob, 'users/alice/blocks/bob'))); // Bob CANNOT read Alice's public profile once blocked await assertFails(getDoc(doc(bob, 'public_profiles/alice'))); // Bob CANNOT get Alice's handle doc once blocked await assertFails(getDoc(doc(bob, 'handles/alice_apex'))); // Bob CANNOT send friend request to Alice once blocked await assertFails(setDoc(doc(bob, 'users/alice/friend_requests_incoming/bob'), { receiverUid: 'alice', senderUid: 'bob', })); }); describe('Group Rides Security Rules', () => { test('organizer can create group ride and initialize organizer participant', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const rideData = { id: 'ride_100', organizerUid: 'alice', organizerHandle: 'alice_apex', organizerDisplayName: 'Alice Apex', title: 'Blood Mountain Run', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, visibility: 'friendsOnly', joinPolicy: 'inviteOnly', meetingPoint: { latitude: 34.6, longitude: -83.9, displayName: 'Dahlonega Square', locationType: 'meetup', }, }; // Alice can create ride with herself as organizer await assertSucceeds(setDoc(doc(alice, 'group_rides/ride_100'), rideData)); // Alice creates herself as accepted organizer participant await assertSucceeds(setDoc(doc(alice, 'group_rides/ride_100/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', riderHandle: 'alice_apex', riderDisplayName: 'Alice Apex', })); // Bob CANNOT create a ride claiming Alice is the organizer await assertFails(setDoc(doc(bob, 'group_rides/ride_fake'), { ...rideData, id: 'ride_fake', organizerUid: 'alice', })); }); test('ride visibility and mutual block rules', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); // Seed ride as private, plus Bob as Alice's friend, and Alice blocks Charlie await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_private'), { id: 'ride_private', organizerUid: 'alice', title: 'Secret Run', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, visibility: 'private', meetingPoint: { latitude: 0, longitude: 0, displayName: 'Spot' }, }); await setDoc(doc(db, 'group_rides/ride_friends'), { id: 'ride_friends', organizerUid: 'alice', title: 'Friends Run', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, visibility: 'friendsOnly', meetingPoint: { latitude: 0, longitude: 0, displayName: 'Spot' }, }); // Bob is Alice's friend await setDoc(doc(db, 'users/alice/friends/bob'), { uid: 'bob' }); // Alice blocks Charlie await setDoc(doc(db, 'users/alice/blocks/charlie'), { blockedUid: 'charlie' }); }); // Private ride: non-member Bob CANNOT read await assertFails(getDoc(doc(bob, 'group_rides/ride_private'))); // Friends ride: friend Bob CAN read await assertSucceeds(getDoc(doc(bob, 'group_rides/ride_friends'))); // Blocked user Charlie CANNOT read friends ride await assertFails(getDoc(doc(charlie, 'group_rides/ride_friends'))); }); test('invitations, acceptance, and capacity modification', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_inv'), { id: 'ride_inv', organizerUid: 'alice', title: 'Invite Run', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, visibility: 'friendsOnly', meetingPoint: { latitude: 0, longitude: 0, displayName: 'Spot' }, }); }); // Alice invites Bob await assertSucceeds(setDoc(doc(alice, 'group_rides/ride_inv/participants/bob'), { userId: 'bob', role: 'participant', status: 'invited', riderHandle: 'bob_rider', riderDisplayName: 'Bob Rider', invitedBy: 'alice', })); // Outsider Charlie CANNOT forge an invitation to himself await assertFails(setDoc(doc(charlie, 'group_rides/ride_inv/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'invited', invitedBy: 'alice', })); // Charlie CANNOT accept Bob's invite await assertFails(updateDoc(doc(charlie, 'group_rides/ride_inv/participants/bob'), { status: 'accepted', })); // Bob accepts his own invite await assertSucceeds(updateDoc(doc(bob, 'group_rides/ride_inv/participants/bob'), { status: 'accepted', })); // Bob can adjust participantCount on the ride during acceptance await assertSucceeds(updateDoc(doc(bob, 'group_rides/ride_inv'), { participantCount: 2, updatedAt: '2026-09-10T09:05:00Z', })); // Bob CANNOT edit authority fields like title or organizerUid await assertFails(updateDoc(doc(bob, 'group_rides/ride_inv'), { title: 'Hijacked Title', })); }); test('live group presence security rules', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); const dave = testEnv.authenticatedContext('dave').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); // Ride setup: Alice is organizer, Bob is accepted participant, Charlie is invited only, Dave is stranger await setDoc(doc(db, 'group_rides/ride_live'), { id: 'ride_live', organizerUid: 'alice', title: 'Live Tracking Run', status: 'underway', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'San Francisco' }, }); await setDoc(doc(db, 'group_rides/ride_live/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', riderHandle: 'alice_rider', riderDisplayName: 'Alice Rider', }); await setDoc(doc(db, 'group_rides/ride_live/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob_rider', riderDisplayName: 'Bob Rider', }); await setDoc(doc(db, 'group_rides/ride_live/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'invited', riderHandle: 'charlie_rider', riderDisplayName: 'Charlie Rider', }); await setDoc(doc(db, 'users/alice'), { id: 'alice' }); await setDoc(doc(db, 'users/bob'), { id: 'bob' }); }); const validBobPresence = { userId: 'bob', groupRideId: 'ride_live', latitude: 37.7833, longitude: -122.4167, heading: 90.0, speedMph: 45.0, accuracyMeters: 5.0, updatedAt: '2026-09-10T09:40:00Z', expiresAt: '2026-09-10T09:42:00Z', sharingEnabled: true, status: 'riding', }; // 1. Accepted participant Bob publishes his own presence: SUCCEEDS await assertSucceeds(setDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'), validBobPresence)); // 2. Stranger Dave CANNOT publish presence await assertFails(setDoc(doc(dave, 'group_rides/ride_live/live_presence/dave'), { ...validBobPresence, userId: 'dave', })); // 3. Invited-only Charlie CANNOT publish presence until accepted await assertFails(setDoc(doc(charlie, 'group_rides/ride_live/live_presence/charlie'), { ...validBobPresence, userId: 'charlie', })); // 4. Bob CANNOT publish presence for Alice (spoofing) await assertFails(setDoc(doc(bob, 'group_rides/ride_live/live_presence/alice'), { ...validBobPresence, userId: 'alice', })); // 5. Organizer Alice CANNOT publish on Bob's presence document await assertFails(setDoc(doc(alice, 'group_rides/ride_live/live_presence/bob'), { ...validBobPresence, userId: 'bob', })); // 6. Non-whitelisted keys rejected await assertFails(setDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'), { ...validBobPresence, leanAngle: 42.0, })); // 7. Invalid coordinates rejected await assertFails(setDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'), { ...validBobPresence, latitude: 95.0, })); // 8. Accepted participant Alice CAN read Bob's presence await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_live/live_presence/bob'))); // 9. Stranger Dave CANNOT read Bob's presence even on public ride await assertFails(getDoc(doc(dave, 'group_rides/ride_live/live_presence/bob'))); // 10. If Bob has sharingEnabled: false, Alice CANNOT read Bob's presence await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_live/live_presence/bob'), { ...validBobPresence, sharingEnabled: false, }); }); await assertFails(getDoc(doc(alice, 'group_rides/ride_live/live_presence/bob'))); // But Bob CAN still read his own presence await assertSucceeds(getDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'))); // 11. If Alice blocks Bob, Alice CANNOT read Bob's presence even if sharingEnabled: true await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_live/live_presence/bob'), validBobPresence); await setDoc(doc(db, 'users/alice/blocks/bob'), { blockedUid: 'bob' }); }); await assertFails(getDoc(doc(alice, 'group_rides/ride_live/live_presence/bob'))); // 12. If ride is canceled, publishing is denied await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await deleteDoc(doc(db, 'users/alice/blocks/bob')); await setDoc(doc(db, 'group_rides/ride_live'), { id: 'ride_live', organizerUid: 'alice', title: 'Live Tracking Run', status: 'canceled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'San Francisco' }, }); }); await assertFails(setDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'), validBobPresence)); // 13. Bob can delete his own presence record await assertSucceeds(deleteDoc(doc(bob, 'group_rides/ride_live/live_presence/bob'))); }); test('live presence lifecycle: removal, voluntary leave, completion, and mid-session block', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const validBobPresence = { userId: 'bob', groupRideId: 'ride_lifecycle', latitude: 37.7833, longitude: -122.4167, heading: 90.0, speedMph: 45.0, accuracyMeters: 5.0, updatedAt: '2026-09-10T09:40:00Z', expiresAt: '2026-09-10T09:42:00Z', sharingEnabled: true, status: 'riding', }; const validAlicePresence = { userId: 'alice', groupRideId: 'ride_lifecycle', latitude: 37.7830, longitude: -122.4160, heading: 90.0, speedMph: 45.0, accuracyMeters: 5.0, updatedAt: '2026-09-10T09:40:00Z', expiresAt: '2026-09-10T09:42:00Z', sharingEnabled: true, status: 'riding', }; const resetLiveState = async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_lifecycle'), { id: 'ride_lifecycle', organizerUid: 'alice', title: 'Lifecycle Test Run', status: 'underway', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'SF' }, }); await setDoc(doc(db, 'group_rides/ride_lifecycle/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', }); await setDoc(doc(db, 'group_rides/ride_lifecycle/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', }); await setDoc(doc(db, 'group_rides/ride_lifecycle/live_presence/bob'), validBobPresence); await setDoc(doc(db, 'group_rides/ride_lifecycle/live_presence/alice'), validAlicePresence); await setDoc(doc(db, 'users/alice'), { id: 'alice' }); await setDoc(doc(db, 'users/bob'), { id: 'bob' }); await deleteDoc(doc(db, 'users/alice/blocks/bob')); await deleteDoc(doc(db, 'users/bob/blocks/alice')); }); }; // Scenario A: Participant Removal (Organizer removes Rider B) await resetLiveState(); // Alice reads Bob initially -> succeeds await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Organizer removes Bob by deleting participant record await assertSucceeds(deleteDoc(doc(alice, 'group_rides/ride_lifecycle/participants/bob'))); // Now Bob CANNOT write to live presence await assertFails(setDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/bob'), { ...validBobPresence, speedMph: 55.0, })); // Alice CANNOT read Bob's old presence await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Scenario B: Voluntary Leave (Bob leaves the ride) await resetLiveState(); // Bob deletes his participant record (voluntary leave) await assertSucceeds(deleteDoc(doc(bob, 'group_rides/ride_lifecycle/participants/bob'))); // Bob CANNOT write to live presence await assertFails(setDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/bob'), { ...validBobPresence, speedMph: 50.0, })); // Alice CANNOT read Bob's presence await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Scenario C: Completed Ride (Ride marked completed) await resetLiveState(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await updateDoc(doc(db, 'group_rides/ride_lifecycle'), { status: 'completed' }); }); // Bob CANNOT write to live presence await assertFails(setDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/bob'), { ...validBobPresence, speedMph: 0.0, })); // Alice CANNOT write to live presence await assertFails(setDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/alice'), { ...validAlicePresence, speedMph: 0.0, })); // Alice CANNOT read Bob's live presence await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Bob CANNOT read Alice's live presence await assertFails(getDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/alice'))); // Scenario D: Mid-Session Block (Alice blocks Bob during active ride) await resetLiveState(); // Both can read initially await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); await assertSucceeds(getDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/alice'))); // Alice blocks Bob await assertSucceeds(setDoc(doc(alice, 'users/alice/blocks/bob'), { blockedUid: 'bob' })); // Alice CANNOT read Bob's presence await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Bob CANNOT read Alice's presence await assertFails(getDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/alice'))); }); test('quick status security: organizer cannot impersonate or modify another rider status', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_status_sec'), { id: 'ride_status_sec', organizerUid: 'alice', title: 'Status Security Ride', status: 'underway', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'SF' }, }); await setDoc(doc(db, 'group_rides/ride_status_sec/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', riderHandle: 'alice', riderDisplayName: 'Alice', }); await setDoc(doc(db, 'group_rides/ride_status_sec/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); await setDoc(doc(db, 'group_rides/ride_status_sec/live_presence/bob'), { userId: 'bob', groupRideId: 'ride_status_sec', latitude: 37.78, longitude: -122.41, heading: 0.0, speedMph: 25.0, accuracyMeters: 5.0, updatedAt: '2026-09-10T09:40:00Z', expiresAt: '2026-09-10T09:42:00Z', sharingEnabled: true, status: 'riding', }); }); // 1. Bob can update his own quick status to 'needFuel' await assertSucceeds(updateDoc(doc(bob, 'group_rides/ride_status_sec/live_presence/bob'), { status: 'needFuel', updatedAt: '2026-09-10T09:41:00Z', })); // 2. Organizer Alice CANNOT update Bob's presence document or status await assertFails(updateDoc(doc(alice, 'group_rides/ride_status_sec/live_presence/bob'), { status: 'riding', })); // 3. Organizer Alice CANNOT create a presence document under Bob's UID await assertFails(setDoc(doc(alice, 'group_rides/ride_status_sec/live_presence/bob'), { userId: 'bob', groupRideId: 'ride_status_sec', latitude: 37.78, longitude: -122.41, heading: 0.0, speedMph: 25.0, accuracyMeters: 5.0, updatedAt: '2026-09-10T09:42:00Z', expiresAt: '2026-09-10T09:44:00Z', sharingEnabled: true, status: 'arrived', })); }); test('session authority: organizer transitions, participant denials, and terminal states', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_transition_test'), { id: 'ride_transition_test', organizerUid: 'alice', title: 'Session Authority Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'SF' }, }); await setDoc(doc(db, 'group_rides/ride_transition_test/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', riderHandle: 'alice', riderDisplayName: 'Alice', }); await setDoc(doc(db, 'group_rides/ride_transition_test/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); }); // 1. Participant Bob CANNOT start the ride (scheduled -> underway) await assertFails(updateDoc(doc(bob, 'group_rides/ride_transition_test'), { status: 'underway', })); // 2. Participant Bob CANNOT cancel the ride await assertFails(updateDoc(doc(bob, 'group_rides/ride_transition_test'), { status: 'canceled', })); // 3. Organizer Alice transitions scheduled -> assembling: SUCCEEDS await assertSucceeds(updateDoc(doc(alice, 'group_rides/ride_transition_test'), { status: 'assembling', })); // 4. Organizer Alice transitions assembling -> underway: SUCCEEDS await assertSucceeds(updateDoc(doc(alice, 'group_rides/ride_transition_test'), { status: 'underway', })); // 5. Participant Bob CANNOT complete the ride await assertFails(updateDoc(doc(bob, 'group_rides/ride_transition_test'), { status: 'completed', })); // 6. Organizer Alice transitions underway -> completed: SUCCEEDS await assertSucceeds(updateDoc(doc(alice, 'group_rides/ride_transition_test'), { status: 'completed', })); // 7. INVALID TRANSITION: completed -> underway MUST BE DENIED for organizer Alice await assertFails(updateDoc(doc(alice, 'group_rides/ride_transition_test'), { status: 'underway', })); // 8. Setup a canceled ride and verify canceled -> underway is denied await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_canceled_test'), { id: 'ride_canceled_test', organizerUid: 'alice', title: 'Canceled Authority Ride', status: 'canceled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 2, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'SF' }, }); }); // INVALID TRANSITION: canceled -> underway MUST BE DENIED await assertFails(updateDoc(doc(alice, 'group_rides/ride_canceled_test'), { status: 'underway', })); }); }); });