MotorSignal-App/firebase/emulator-tests/firestore.rules.test.js

189 lines
6.3 KiB
JavaScript

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