test: add Firestore/Storage security rules tests
Adds emulator-backed rules-unit tests verifying the Firestore rules committed in Task 3, including the global constraint that bookings/inquiries are never publicly readable. Excludes firebase-tests/ from the default vitest run since it needs the emulator (via `npm run test:rules`) rather than jsdom.
This commit is contained in:
106
firebase-tests/firestore.rules.test.ts
Normal file
106
firebase-tests/firestore.rules.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import {
|
||||
initializeTestEnvironment,
|
||||
assertSucceeds,
|
||||
assertFails,
|
||||
type RulesTestEnvironment,
|
||||
} from '@firebase/rules-unit-testing'
|
||||
import { doc, getDoc, setDoc, addDoc, collection } from 'firebase/firestore'
|
||||
|
||||
const ADMIN_UID = 'dev-admin-placeholder-uid'
|
||||
|
||||
let testEnv: RulesTestEnvironment
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await initializeTestEnvironment({
|
||||
projectId: 'demo-braids-by-mira',
|
||||
firestore: {
|
||||
rules: readFileSync('firestore.rules', 'utf8'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await testEnv.cleanup()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await testEnv.clearFirestore()
|
||||
})
|
||||
|
||||
describe('Firestore security rules', () => {
|
||||
it('allows anyone to read products', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'products/edge-control'), { name: 'Edge Control' })
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(getDoc(doc(unauthed.firestore(), 'products/edge-control')))
|
||||
})
|
||||
|
||||
it('blocks a non-admin from writing to products', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(setDoc(doc(unauthed.firestore(), 'products/hacked'), { name: 'Hack' }))
|
||||
})
|
||||
|
||||
it('allows the admin to write to products', async () => {
|
||||
const admin = testEnv.authenticatedContext(ADMIN_UID)
|
||||
await assertSucceeds(setDoc(doc(admin.firestore(), 'products/new-item'), { name: 'New Item' }))
|
||||
})
|
||||
|
||||
it('allows anyone to create a booking', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(
|
||||
addDoc(collection(unauthed.firestore(), 'bookings'), {
|
||||
serviceId: 'knotless',
|
||||
date: '2026-09-01',
|
||||
time: '10:00 AM',
|
||||
contact: { name: 'Jane', phone: '555-0100', email: 'jane@example.com', notes: '' },
|
||||
status: 'confirmed',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks a non-admin from reading bookings (PII protection)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookings/b1'), {
|
||||
serviceId: 'knotless',
|
||||
contact: { name: 'Jane', phone: '555-0100', email: 'jane@example.com', notes: '' },
|
||||
})
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(getDoc(doc(unauthed.firestore(), 'bookings/b1')))
|
||||
})
|
||||
|
||||
it('allows the admin to read bookings', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookings/b1'), { serviceId: 'knotless' })
|
||||
})
|
||||
const admin = testEnv.authenticatedContext(ADMIN_UID)
|
||||
await assertSucceeds(getDoc(doc(admin.firestore(), 'bookings/b1')))
|
||||
})
|
||||
|
||||
it('blocks a non-admin from reading inquiries (PII protection)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'inquiries/i1'), {
|
||||
productId: 'edge-control',
|
||||
contact: { name: 'Jane', email: 'jane@example.com', notes: '' },
|
||||
})
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(getDoc(doc(unauthed.firestore(), 'inquiries/i1')))
|
||||
})
|
||||
|
||||
it('allows anyone to read bookedSlots (no PII there)', async () => {
|
||||
await testEnv.withSecurityRulesDisabled(async (ctx) => {
|
||||
await setDoc(doc(ctx.firestore(), 'bookedSlots/2026-09-01'), { times: ['10:00 AM'] })
|
||||
})
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertSucceeds(getDoc(doc(unauthed.firestore(), 'bookedSlots/2026-09-01')))
|
||||
})
|
||||
|
||||
it('blocks a non-admin from writing bookedSlots directly', async () => {
|
||||
const unauthed = testEnv.unauthenticatedContext()
|
||||
await assertFails(setDoc(doc(unauthed.firestore(), 'bookedSlots/2026-09-01'), { times: ['1:00 PM'] }))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user