diff --git a/firebase/emulator-tests/firestore.rules.test.js b/firebase/emulator-tests/firestore.rules.test.js new file mode 100644 index 0000000..b46e98f --- /dev/null +++ b/firebase/emulator-tests/firestore.rules.test.js @@ -0,0 +1,85 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { + assertFails, + assertSucceeds, + initializeTestEnvironment, +} from '@firebase/rules-unit-testing'; +import { doc, getDoc, 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(); +}); + +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)); + }); +});