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))); } }); });