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, writeBatch, query, where } 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, accessUids: ['alice'], 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', })); // Alice can create ride AND organizer participant in an atomic WriteBatch (as done by client app) const batch = writeBatch(alice); batch.set(doc(alice, 'group_rides/ride_batch_101'), { ...rideData, id: 'ride_batch_101', }); batch.set(doc(alice, 'group_rides/ride_batch_101/participants/alice'), { userId: 'alice', role: 'organizer', status: 'accepted', riderHandle: 'alice_apex', riderDisplayName: 'Alice Apex', }); await assertSucceeds(batch.commit()); // 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, accessUids: ['alice'], 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, accessUids: ['alice'], 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, accessUids: ['alice', 'bob'], 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('upcoming rides and my rides collection queries enforce accessUids and organizerUid', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); const unauth = testEnv.unauthenticatedContext().firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); // Alice's ride with Bob invited await setDoc(doc(db, 'group_rides/ride_query_test'), { id: 'ride_query_test', organizerUid: 'alice', title: 'Query Verification Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, accessUids: ['alice', 'bob'], visibility: 'friendsOnly', joinPolicy: 'inviteOnly', meetingPoint: { latitude: 34.0, longitude: -84.0, displayName: 'Point A' }, }); }); // 1. Alice queries My Rides (organizerUid == 'alice') -> SUCCEEDS const myRidesQuery = query( collection(alice, 'group_rides'), where('organizerUid', '==', 'alice'), where('status', 'in', ['scheduled', 'assembling', 'underway']), ); const aliceSnap = await assertSucceeds(getDocs(myRidesQuery)); expect(aliceSnap.docs.length).toBe(1); // 2. Bob queries Upcoming Rides (accessUids array-contains 'bob') -> SUCCEEDS const bobUpcomingQuery = query( collection(bob, 'group_rides'), where('accessUids', 'array-contains', 'bob'), where('status', 'in', ['scheduled', 'assembling', 'underway']), ); const bobSnap = await assertSucceeds(getDocs(bobUpcomingQuery)); expect(bobSnap.docs.length).toBe(1); // 3. Charlie queries Upcoming Rides (accessUids array-contains 'charlie') -> SUCCEEDS (0 results) const charlieUpcomingQuery = query( collection(charlie, 'group_rides'), where('accessUids', 'array-contains', 'charlie'), where('status', 'in', ['scheduled', 'assembling', 'underway']), ); const charlieSnap = await assertSucceeds(getDocs(charlieUpcomingQuery)); expect(charlieSnap.docs.length).toBe(0); // 4. Broad query WITHOUT accessUids or organizerUid -> DENIED const broadQuery = query( collection(bob, 'group_rides'), where('status', '==', 'scheduled'), ); await assertFails(getDocs(broadQuery)); // 5. Unauthenticated user CANNOT query group_rides const unauthQuery = query( collection(unauth, 'group_rides'), where('organizerUid', '==', 'alice'), where('status', 'in', ['scheduled', 'assembling', 'underway']), ); await assertFails(getDocs(unauthQuery)); }); test('status-aware participant authorization and privilege escalation prevention', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); const dave = testEnv.authenticatedContext('dave').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); // Ride with requestToJoin policy await setDoc(doc(db, 'group_rides/ride_request_only'), { id: 'ride_request_only', organizerUid: 'alice', title: 'Request Only Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, accessUids: ['alice'], visibility: 'public', joinPolicy: 'requestToJoin', meetingPoint: { latitude: 34.0, longitude: -84.0, displayName: 'Point R' }, }); // Ride with open join policy await setDoc(doc(db, 'group_rides/ride_open'), { id: 'ride_open', organizerUid: 'alice', title: 'Open Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, accessUids: ['alice'], visibility: 'public', joinPolicy: 'open', meetingPoint: { latitude: 34.0, longitude: -84.0, displayName: 'Point O' }, }); // Ride with inviteOnly policy await setDoc(doc(db, 'group_rides/ride_invite_strict'), { id: 'ride_invite_strict', organizerUid: 'alice', title: 'Invite Only Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 1, accessUids: ['alice'], visibility: 'public', joinPolicy: 'inviteOnly', meetingPoint: { latitude: 34.0, longitude: -84.0, displayName: 'Point I' }, }); }); // 1. On requestToJoin ride, Charlie CANNOT self-join with 'accepted' await assertFails(setDoc(doc(charlie, 'group_rides/ride_request_only/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'accepted', })); // 2. Charlie CAN create 'requested' participant record await assertSucceeds(setDoc(doc(charlie, 'group_rides/ride_request_only/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'requested', })); // 3. Charlie CANNOT self-promote from 'requested' to 'accepted' await assertFails(updateDoc(doc(charlie, 'group_rides/ride_request_only/participants/charlie'), { status: 'accepted', })); // 4. Organizer Alice CAN approve Charlie's request await assertSucceeds(updateDoc(doc(alice, 'group_rides/ride_request_only/participants/charlie'), { status: 'accepted', })); // 5. On inviteOnly ride, stranger Dave CANNOT request or join await assertFails(setDoc(doc(dave, 'group_rides/ride_invite_strict/participants/dave'), { userId: 'dave', role: 'participant', status: 'requested', })); await assertFails(setDoc(doc(dave, 'group_rides/ride_invite_strict/participants/dave'), { userId: 'dave', role: 'participant', status: 'accepted', })); // 6. On open ride, Dave CAN directly join with 'accepted' await assertSucceeds(setDoc(doc(dave, 'group_rides/ride_open/participants/dave'), { userId: 'dave', role: 'participant', status: 'accepted', })); // 7. Dave can update accessUids and increment participantCount on open ride await assertSucceeds(updateDoc(doc(dave, 'group_rides/ride_open'), { accessUids: ['alice', 'dave'], participantCount: 2, updatedAt: '2026-09-10T09:05:00Z', })); }); 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, accessUids: ['alice', 'bob', 'charlie'], 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 via point-get await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_live/live_presence/bob'))); // 8b. Collection listing is STRICTLY DENIED for everyone (including organizer and accepted participants) await assertFails(getDocs(collection(alice, 'group_rides/ride_live/live_presence'))); await assertFails(getDocs(collection(bob, 'group_rides/ride_live/live_presence'))); await assertFails(getDocs(collection(dave, 'group_rides/ride_live/live_presence'))); await assertFails(getDocs(collection(testEnv.unauthenticatedContext().firestore(), 'group_rides/ride_live/live_presence'))); // 8c. Signed-out point-get is DENIED await assertFails(getDoc(doc(testEnv.unauthenticatedContext().firestore(), 'group_rides/ride_live/live_presence/bob'))); // 8d. Invited Charlie CANNOT point-get Bob's presence await assertFails(getDoc(doc(charlie, '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'))); // 11b. Participant roster block privacy: Peer Charlie blocks Peer Bob await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_live/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'accepted', riderHandle: 'charlie_rider', riderDisplayName: 'Charlie Rider', }); await setDoc(doc(db, 'users/charlie/blocks/bob'), { blockedUid: 'bob' }); }); // Charlie CANNOT read Bob's participant document due to mutual block await assertFails(getDoc(doc(charlie, 'group_rides/ride_live/participants/bob'))); // Bob CANNOT read Charlie's participant document due to mutual block await assertFails(getDoc(doc(bob, 'group_rides/ride_live/participants/charlie'))); // But Organizer Alice CAN still read Bob's participant document await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_live/participants/bob'))); // And Bob CAN read his own participant document await assertSucceeds(getDoc(doc(bob, 'group_rides/ride_live/participants/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, accessUids: ['alice', 'bob'], 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, accessUids: ['alice', 'bob'], 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 deletes his participant record) 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 B2: Voluntary Leave via status transition (status: 'accepted' -> 'left', presence doc temporarily remains) await resetLiveState(); await assertSucceeds(updateDoc(doc(bob, 'group_rides/ride_lifecycle/participants/bob'), { status: 'left', })); // Stale presence document still exists in database, but Alice direct get is DENIED await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Bob self get for cleanup verification remains ALLOWED await assertSucceeds(getDoc(doc(bob, 'group_rides/ride_lifecycle/live_presence/bob'))); // Scenario B3: Organizer removal via status transition (status: 'accepted' -> 'removed', presence doc temporarily remains) await resetLiveState(); await assertSucceeds(updateDoc(doc(alice, 'group_rides/ride_lifecycle/participants/bob'), { status: 'removed', })); // Stale presence document still exists in database, but Alice direct get is DENIED await assertFails(getDoc(doc(alice, 'group_rides/ride_lifecycle/live_presence/bob'))); // Scenario B4: Participant status transition to 'declined' (presence doc temporarily remains) await resetLiveState(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await updateDoc(doc(db, 'group_rides/ride_lifecycle/participants/bob'), { status: 'declined' }); }); // Stale presence document still exists in database, but Alice direct get is DENIED 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 C2: Canceled Ride (Ride marked canceled, presence doc temporarily remains) await resetLiveState(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await updateDoc(doc(db, 'group_rides/ride_lifecycle'), { status: 'canceled' }); }); // 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'))); // Scenario E: Participants Collection Listing is STRICTLY DENIED for everyone await resetLiveState(); await assertFails(getDocs(collection(alice, 'group_rides/ride_lifecycle/participants'))); await assertFails(getDocs(collection(bob, 'group_rides/ride_lifecycle/participants'))); await assertFails(getDocs(collection(testEnv.authenticatedContext('stranger').firestore(), 'group_rides/ride_lifecycle/participants'))); await assertFails(getDocs(collection(testEnv.unauthenticatedContext().firestore(), 'group_rides/ride_lifecycle/participants'))); // Scenario F: Participant single-doc get authorization const charlie = testEnv.authenticatedContext('charlie').firestore(); await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_lifecycle/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'invited', }); }); // Charlie (invited) CAN read his own participant document await assertSucceeds(getDoc(doc(charlie, 'group_rides/ride_lifecycle/participants/charlie'))); // Charlie (invited) CANNOT read peer Bob's participant document await assertFails(getDoc(doc(charlie, 'group_rides/ride_lifecycle/participants/bob'))); // Organizer Alice CAN read invited Charlie's participant document await assertSucceeds(getDoc(doc(alice, 'group_rides/ride_lifecycle/participants/charlie'))); }); 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', })); }); }); describe('MotorSignal route planning and sharing rules', () => { test('owner can create, read, update, and delete their own route', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const routeRef = doc(alice, 'routes/route_alice_1'); // 1. Create valid route: SUCCEEDS await assertSucceeds(setDoc(routeRef, { id: 'route_alice_1', ownerUid: 'alice', title: 'Skyline Boulevard Loop', description: 'Twisties through the redwoods', visibility: 'private', stops: [ { id: 'stop-1', latitude: 37.4, longitude: -122.2, displayName: 'Alice\'s Restaurant', stopType: 'origin', orderIndex: 0, }, { id: 'stop-2', latitude: 37.3, longitude: -122.1, displayName: 'Page Mill Vista', stopType: 'destination', orderIndex: 1, }, ], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', })); // 2. Read own route: SUCCEEDS await assertSucceeds(getDoc(routeRef)); // 3. Update own route: SUCCEEDS await assertSucceeds(updateDoc(routeRef, { title: 'Skyline Boulevard Loop Extended', updatedAt: '2026-09-04T12:30:00Z', })); // 4. Delete own route: SUCCEEDS await assertSucceeds(deleteDoc(routeRef)); }); test('validates route constraints: title length and max stops', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); // Title too short (<3 chars): FAILS await assertFails(setDoc(doc(alice, 'routes/route_short_title'), { id: 'route_short_title', ownerUid: 'alice', title: 'AB', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', })); // Stops exceed 25 cap: FAILS const excessiveStops = []; for (let i = 0; i < 26; i++) { excessiveStops.push({ id: `stop-${i}`, latitude: 37.0 + i * 0.01, longitude: -122.0, displayName: `Stop ${i}`, stopType: 'waypoint', orderIndex: i, }); } await assertFails(setDoc(doc(alice, 'routes/route_too_many_stops'), { id: 'route_too_many_stops', ownerUid: 'alice', title: 'Way Too Many Stops Route', stops: excessiveStops, createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', })); }); test('private route cannot be read or modified by non-owner', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'routes/route_private_alice'), { id: 'route_private_alice', ownerUid: 'alice', title: 'Secret Canyon Route', visibility: 'private', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const bob = testEnv.authenticatedContext('bob').firestore(); const targetRoute = doc(bob, 'routes/route_private_alice'); // Bob cannot read private route await assertFails(getDoc(targetRoute)); // Bob cannot update private route await assertFails(updateDoc(targetRoute, { title: 'Hacked Title' })); // Bob cannot delete private route await assertFails(deleteDoc(targetRoute)); }); test('public route can be read by any signed-in user, but only modified by owner', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'routes/route_public_alice'), { id: 'route_public_alice', ownerUid: 'alice', title: 'Public Coastal Highway 1', visibility: 'public', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const bob = testEnv.authenticatedContext('bob').firestore(); const targetRoute = doc(bob, 'routes/route_public_alice'); // Bob can read public route await assertSucceeds(getDoc(targetRoute)); // Bob cannot update public route await assertFails(updateDoc(targetRoute, { title: 'Bob Was Here' })); }); test('groupOnly route can be read by accepted group members, but not by outsiders', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_for_route'), { id: 'ride_for_route', organizerUid: 'alice', title: 'Sunday Group 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_for_route/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); await setDoc(doc(db, 'routes/route_group_only'), { id: 'route_group_only', ownerUid: 'alice', title: 'Group Official Route', visibility: 'groupOnly', attachedGroupRideId: 'ride_for_route', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); // Accepted member Bob CAN read groupOnly route await assertSucceeds(getDoc(doc(bob, 'routes/route_group_only'))); // Non-member Charlie CANNOT read groupOnly route await assertFails(getDoc(doc(charlie, 'routes/route_group_only'))); }); test('group ride route attachment authority: only organizer can attach or detach route', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_attach_authority'), { id: 'ride_attach_authority', organizerUid: 'alice', title: 'Route Authority Group 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_attach_authority/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); await setDoc(doc(db, 'routes/route_official_alice'), { id: 'route_official_alice', ownerUid: 'alice', title: 'Alice Official Route', visibility: 'private', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const rideDocRef = (db) => doc(db, 'group_rides/ride_attach_authority'); // 1. Participant Bob attempts to attach a route: MUST FAIL await assertFails(updateDoc(rideDocRef(bob), { routeId: 'route_bob_custom', routeTitle: 'Bob\'s Route', routeDistanceMeters: 45000, routeStopCount: 4, })); // 2. Organizer Alice attaches a route: SUCCEEDS await assertSucceeds(updateDoc(rideDocRef(alice), { routeId: 'route_official_alice', routeTitle: 'Alice\'s Official Route', routeDistanceMeters: 52000, routeStopCount: 5, })); // 3. Participant Bob attempts to detach or change route: MUST FAIL await assertFails(updateDoc(rideDocRef(bob), { routeId: null, })); // 4. Organizer Alice detaches route: SUCCEEDS await assertSucceeds(updateDoc(rideDocRef(alice), { routeId: null, routeTitle: null, routeDistanceMeters: null, routeStopCount: null, })); }); test('group-only route read authorization enforces accepted status and mutual block privacy', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_go_matrix'), { id: 'ride_go_matrix', organizerUid: 'alice', title: 'Matrix Group Ride', status: 'scheduled', scheduledStartAt: '2026-09-10T09:00:00Z', departureAt: '2026-09-10T09:30:00Z', participantCount: 6, visibility: 'public', meetingPoint: { latitude: 37.77, longitude: -122.41, displayName: 'SF' }, }); // 1. accepted participant await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); // 2. invited participant await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/charlie'), { userId: 'charlie', role: 'participant', status: 'invited', riderHandle: 'charlie', riderDisplayName: 'Charlie', }); // 3. requested participant await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/dave'), { userId: 'dave', role: 'participant', status: 'requested', riderHandle: 'dave', riderDisplayName: 'Dave', }); // 4. left participant await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/eve'), { userId: 'eve', role: 'participant', status: 'left', riderHandle: 'eve', riderDisplayName: 'Eve', }); // 5. removed participant await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/frank'), { userId: 'frank', role: 'participant', status: 'removed', riderHandle: 'frank', riderDisplayName: 'Frank', }); // 6. accepted rider but mutually blocked with owner Alice await setDoc(doc(db, 'group_rides/ride_go_matrix/participants/mallory'), { userId: 'mallory', role: 'participant', status: 'accepted', riderHandle: 'mallory', riderDisplayName: 'Mallory', }); await setDoc(doc(db, 'users/alice/blocks/mallory'), { blocked: true }); // Route attached to this group ride await setDoc(doc(db, 'routes/route_go_matrix_valid'), { id: 'route_go_matrix_valid', ownerUid: 'alice', title: 'Matrix Group Route', visibility: 'groupOnly', attachedGroupRideId: 'ride_go_matrix', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); // Route marked groupOnly but with no attached group ride await setDoc(doc(db, 'routes/route_go_matrix_no_group'), { id: 'route_go_matrix_no_group', ownerUid: 'alice', title: 'Orphan Group Route', visibility: 'groupOnly', attachedGroupRideId: null, stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const bob = testEnv.authenticatedContext('bob').firestore(); const charlie = testEnv.authenticatedContext('charlie').firestore(); const dave = testEnv.authenticatedContext('dave').firestore(); const eve = testEnv.authenticatedContext('eve').firestore(); const frank = testEnv.authenticatedContext('frank').firestore(); const mallory = testEnv.authenticatedContext('mallory').firestore(); const outsider = testEnv.authenticatedContext('outsider').firestore(); const validRouteRef = (db) => doc(db, 'routes/route_go_matrix_valid'); const orphanRouteRef = (db) => doc(db, 'routes/route_go_matrix_no_group'); // accepted participant -> ALLOW await assertSucceeds(getDoc(validRouteRef(bob))); // invited participant -> DENY await assertFails(getDoc(validRouteRef(charlie))); // requested participant -> DENY await assertFails(getDoc(validRouteRef(dave))); // left participant -> DENY await assertFails(getDoc(validRouteRef(eve))); // removed participant -> DENY await assertFails(getDoc(validRouteRef(frank))); // blocked accepted rider -> DENY await assertFails(getDoc(validRouteRef(mallory))); // outsider -> DENY await assertFails(getDoc(validRouteRef(outsider))); // route with no attached group ride -> DENY (even for accepted rider Bob) await assertFails(getDoc(orphanRouteRef(bob))); }); test('group ride route attachment requires organizer to own the route', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'group_rides/ride_attach_ownership'), { id: 'ride_attach_ownership', organizerUid: 'alice', title: 'Ownership Group 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_attach_ownership/participants/bob'), { userId: 'bob', role: 'participant', status: 'accepted', riderHandle: 'bob', riderDisplayName: 'Bob', }); await setDoc(doc(db, 'routes/route_alice_owned'), { id: 'route_alice_owned', ownerUid: 'alice', title: 'Alice Route', visibility: 'private', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); await setDoc(doc(db, 'routes/route_bob_owned'), { id: 'route_bob_owned', ownerUid: 'bob', title: 'Bob Route', visibility: 'private', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const rideRef = (db) => doc(db, 'group_rides/ride_attach_ownership'); // 1. Organizer Alice attaching route she owns -> ALLOW await assertSucceeds(updateDoc(rideRef(alice), { routeId: 'route_alice_owned', routeTitle: 'Alice Route', routeDistanceMeters: 10000, routeStopCount: 2, })); // 2. Organizer Alice attaching another user's route (Bob's route) -> DENY await assertFails(updateDoc(rideRef(alice), { routeId: 'route_bob_owned', routeTitle: 'Bob Route', routeDistanceMeters: 10000, routeStopCount: 2, })); // 3. Non-organizer participant Bob attaching his own route -> DENY await assertFails(updateDoc(rideRef(bob), { routeId: 'route_bob_owned', routeTitle: 'Bob Route', routeDistanceMeters: 10000, routeStopCount: 2, })); // 4. Organizer Alice attaching non-existent route -> DENY await assertFails(updateDoc(rideRef(alice), { routeId: 'route_non_existent', })); }); test('route schema immutability and visibility whitelisting', async () => { await testEnv.withSecurityRulesDisabled(async (context) => { const db = context.firestore(); await setDoc(doc(db, 'routes/route_immutable_test'), { id: 'route_immutable_test', ownerUid: 'alice', title: 'Original Title', visibility: 'private', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); }); const alice = testEnv.authenticatedContext('alice').firestore(); const bob = testEnv.authenticatedContext('bob').firestore(); const routeRef = doc(alice, 'routes/route_immutable_test'); // 1. Updating title: ALLOW await assertSucceeds(updateDoc(routeRef, { title: 'Updated Safe Title', updatedAt: '2026-09-04T13:00:00Z', })); // 2. Modifying immutable ownerUid: DENY await assertFails(updateDoc(routeRef, { ownerUid: 'bob', })); // 3. Modifying immutable id: DENY await assertFails(updateDoc(routeRef, { id: 'different_id', })); // 4. Modifying immutable createdAt: DENY await assertFails(updateDoc(routeRef, { createdAt: '2026-09-05T00:00:00Z', })); // 5. Forging ownerUid on create: DENY await assertFails(setDoc(doc(bob, 'routes/route_forged_bob'), { id: 'route_forged_bob', ownerUid: 'alice', title: 'Forged Owner Route', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', })); // 6. Unsupported visibility: DENY await assertFails(setDoc(doc(alice, 'routes/route_invalid_visibility'), { id: 'route_invalid_visibility', ownerUid: 'alice', title: 'Invalid Visibility Route', visibility: 'secret_unsupported', stops: [], createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', })); }); test('route stops validation enforces bounds, types, and required fields', async () => { const alice = testEnv.authenticatedContext('alice').firestore(); const baseRoute = (routeId, stops) => ({ id: routeId, ownerUid: 'alice', title: 'Stop Validation Route', visibility: 'private', stops, createdAt: '2026-09-04T12:00:00Z', updatedAt: '2026-09-04T12:00:00Z', }); const makeStop = (overrides = {}) => ({ id: 'stop-valid-1', order: 0, stopType: 'waypoint', displayName: 'Valid Stop', latitude: 37.77, longitude: -122.41, ...overrides, }); // 1. Valid stop passes await assertSucceeds(setDoc(doc(alice, 'routes/route_stop_valid'), baseRoute('route_stop_valid', [makeStop()]))); // 2. Latitude > 90 -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_lat_high'), baseRoute('route_stop_lat_high', [makeStop({ latitude: 90.001 })]))); // 3. Latitude < -90 -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_lat_low'), baseRoute('route_stop_lat_low', [makeStop({ latitude: -90.001 })]))); // 4. Longitude > 180 -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_lng_high'), baseRoute('route_stop_lng_high', [makeStop({ longitude: 180.001 })]))); // 5. Longitude < -180 -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_lng_low'), baseRoute('route_stop_lng_low', [makeStop({ longitude: -180.001 })]))); // 6. Unsupported stopType -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_bad_type'), baseRoute('route_stop_bad_type', [makeStop({ stopType: 'hyperloop' })]))); // 7. Empty stop ID -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_empty_id'), baseRoute('route_stop_empty_id', [makeStop({ id: '' })]))); // 8. Empty displayName -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_empty_name'), baseRoute('route_stop_empty_name', [makeStop({ displayName: '' })]))); // 9. Oversized displayName (> 80 chars) -> DENY await assertFails(setDoc(doc(alice, 'routes/route_stop_long_name'), baseRoute('route_stop_long_name', [makeStop({ displayName: 'A'.repeat(81) })]))); // 10. Malformed stop object (missing latitude) -> DENY const malformedStop = { id: 'stop-bad', order: 0, stopType: 'waypoint', displayName: 'Bad Stop', longitude: -122.41, }; await assertFails(setDoc(doc(alice, 'routes/route_stop_malformed'), baseRoute('route_stop_malformed', [malformedStop]))); // 11. 25 stops (technical cap) -> ALLOW const maxStops = []; for (let i = 0; i < 25; i++) { maxStops.push(makeStop({ id: `stop-${i}`, order: i, displayName: `Stop ${i}` })); } await assertSucceeds(setDoc(doc(alice, 'routes/route_25_stops'), baseRoute('route_25_stops', maxStops))); }); }); });