From aeef76cd9517c8ca1e5c9e47ce2a38c46fe715ac Mon Sep 17 00:00:00 2001 From: Jits Date: Sat, 10 Feb 2024 14:22:31 +0000 Subject: [PATCH] fixup! Initial test suites and CI --- .../test/firestore/firestore-rules.spec.ts | 24 +++++--- firebase/test/helpers/firestore.ts | 2 +- firebase/test/helpers/rtdb.ts | 2 +- firebase/test/helpers/storage.ts | 14 +++++ firebase/test/rtdb/rtdb-rules.spec.ts | 11 +++- firebase/test/storage/storage-rules.spec.ts | 58 +++++++++++++++++++ 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 firebase/test/helpers/storage.ts create mode 100644 firebase/test/storage/storage-rules.spec.ts diff --git a/firebase/test/firestore/firestore-rules.spec.ts b/firebase/test/firestore/firestore-rules.spec.ts index 9324d46..d0363f4 100644 --- a/firebase/test/firestore/firestore-rules.spec.ts +++ b/firebase/test/firestore/firestore-rules.spec.ts @@ -3,7 +3,7 @@ import { assertFails, initializeTestEnvironment, } from '@firebase/rules-unit-testing'; -import { doc, getDoc, serverTimestamp, setDoc, setLogLevel } from 'firebase/firestore'; +import { deleteDoc, doc, getDoc, setDoc, setLogLevel } from 'firebase/firestore'; import { createWriteStream, readFileSync } from 'node:fs'; import { get as httpGet } from 'node:http'; import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; @@ -29,6 +29,8 @@ beforeAll(async () => { }); afterAll(async () => { + await testEnv.cleanup(); + // Write the coverage report to a file const { coverageUrl } = getFirestoreMeta(PROJECT_ID); const coverageFile = './firestore-coverage.html'; @@ -48,21 +50,25 @@ beforeEach(async () => { }); describe('Firestore security rules', () => { - test('does not allow any reads or writes to an unused collection by an unauthenticated user', async () => { + test('does not allow any reads, writes or deletes to an unused collection by an unauthenticated user', async () => { const db = testEnv.unauthenticatedContext().firestore(); - const docref = doc(db, 'test/test'); + const docRef = doc(db, 'unused/1'); + + await assertFails(getDoc(docRef)); - await assertFails(getDoc(docref)); + await assertFails(setDoc(docRef, { name: 'someone' })); - await assertFails(setDoc(docref, { createdAt: serverTimestamp() })); + await assertFails(deleteDoc(docRef)); }); - test('does not allow any reads or writes to an unused collection by an authenticated user', async () => { + test('does not allow any reads, writes or deletes to an unused collection by an authenticated user', async () => { const db = testEnv.authenticatedContext('alice').firestore(); - const docref = doc(db, 'test/test'); + const docRef = doc(db, 'unused/1'); + + await assertFails(getDoc(docRef)); - await assertFails(getDoc(docref)); + await assertFails(setDoc(docRef, { name: 'someone' })); - await assertFails(setDoc(docref, { createdAt: serverTimestamp() })); + await assertFails(deleteDoc(docRef)); }); }); diff --git a/firebase/test/helpers/firestore.ts b/firebase/test/helpers/firestore.ts index e83f464..c54a126 100644 --- a/firebase/test/helpers/firestore.ts +++ b/firebase/test/helpers/firestore.ts @@ -4,7 +4,7 @@ export function getFirestoreMeta(projectId: string) { const { hostname: host, port } = parseHost(process.env.FIRESTORE_EMULATOR_HOST); if (!host || !port) { - throw new Error('Could not parse host and/orport from FIRESTORE_EMULATOR_HOST'); + throw new Error('Could not parse host and/or port from FIRESTORE_EMULATOR_HOST'); } const coverageUrl = `http://${host}:${port}/emulator/v1/projects/${projectId}:ruleCoverage.html`; diff --git a/firebase/test/helpers/rtdb.ts b/firebase/test/helpers/rtdb.ts index fc92f3b..015ed7d 100644 --- a/firebase/test/helpers/rtdb.ts +++ b/firebase/test/helpers/rtdb.ts @@ -4,7 +4,7 @@ export function getRtdbMeta(databaseName: string) { const { hostname: host, port } = parseHost(process.env.FIREBASE_DATABASE_EMULATOR_HOST); if (!host || !port) { - throw new Error('Could not parse host and/orport from FIREBASE_DATABASE_EMULATOR_HOST'); + throw new Error('Could not parse host and/or port from FIREBASE_DATABASE_EMULATOR_HOST'); } const coverageUrl = `http://${host}:${port}/.inspect/coverage?ns=${databaseName}`; diff --git a/firebase/test/helpers/storage.ts b/firebase/test/helpers/storage.ts new file mode 100644 index 0000000..68e1f13 --- /dev/null +++ b/firebase/test/helpers/storage.ts @@ -0,0 +1,14 @@ +import { parseHost } from 'ufo'; + +export function getStorageMeta() { + const { hostname: host, port } = parseHost(process.env.FIREBASE_STORAGE_EMULATOR_HOST); + + if (!host || !port) { + throw new Error('Could not parse host and/or port from FIREBASE_STORAGE_EMULATOR_HOST'); + } + + return { + host, + port: Number(port), + }; +} diff --git a/firebase/test/rtdb/rtdb-rules.spec.ts b/firebase/test/rtdb/rtdb-rules.spec.ts index 0189590..6da2a9c 100644 --- a/firebase/test/rtdb/rtdb-rules.spec.ts +++ b/firebase/test/rtdb/rtdb-rules.spec.ts @@ -3,7 +3,7 @@ import { assertFails, initializeTestEnvironment, } from '@firebase/rules-unit-testing'; -import { get, ref, set } from 'firebase/database'; +import { get, ref, remove, set } from 'firebase/database'; import { createWriteStream, readFileSync } from 'node:fs'; import { get as httpGet } from 'node:http'; import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; @@ -28,6 +28,7 @@ beforeAll(async () => { afterAll(async () => { await testEnv.cleanup(); + // Write the coverage report to a file const { coverageUrl } = getRtdbMeta(DATABASE_NAME); const coverageFile = './rtdb-coverage.html'; @@ -48,21 +49,25 @@ beforeEach(async () => { }); describe('RTDB security rules', () => { - test('does not allow any reads or writes to an unused key by an unauthenticated user', async () => { + test('does not allow any reads, writes or deletes to an unused key by an unauthenticated user', async () => { const db = testEnv.unauthenticatedContext().database(); const valueRef = ref(db, 'unusedKey'); await assertFails(get(valueRef)); await assertFails(set(valueRef, 'foo')); + + await assertFails(remove(valueRef)); }); - test('does not allow any reads or writes to an unused key by an authenticated user', async () => { + test('does not allow any reads, writes or deletes to an unused key by an authenticated user', async () => { const db = testEnv.authenticatedContext('alice').database(); const valueRef = ref(db, 'unusedKey'); await assertFails(get(valueRef)); await assertFails(set(valueRef, 'foo')); + + await assertFails(remove(valueRef)); }); }); diff --git a/firebase/test/storage/storage-rules.spec.ts b/firebase/test/storage/storage-rules.spec.ts new file mode 100644 index 0000000..c2bf1a2 --- /dev/null +++ b/firebase/test/storage/storage-rules.spec.ts @@ -0,0 +1,58 @@ +import { + RulesTestEnvironment, + assertFails, + initializeTestEnvironment, +} from '@firebase/rules-unit-testing'; +import { deleteObject, getDownloadURL, ref, updateMetadata } from 'firebase/storage'; +import { readFileSync } from 'node:fs'; +import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; +import { getStorageMeta } from '../helpers/storage'; + +const PROJECT_ID = 'demo-test'; // Must match the project name used to start the emulators + +let testEnv: RulesTestEnvironment; + +beforeAll(async () => { + const { host, port } = getStorageMeta(); + testEnv = await initializeTestEnvironment({ + projectId: PROJECT_ID, + storage: { + port, + host, + rules: readFileSync('storage.rules', 'utf8'), + }, + }); +}); + +afterAll(async () => { + await testEnv.cleanup(); +}); + +beforeEach(async () => { + await testEnv.clearStorage(); +}); + +describe('Storage security rules', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + test('does not allow any reads, writes or deletes to an unused object by an unauthenticated user', async () => { + const storage = testEnv.unauthenticatedContext().storage(); + const objectRef = ref(storage, 'unused.jpg'); + + await assertFails(getDownloadURL(objectRef)); + + await assertFails(updateMetadata(objectRef, { cacheControl: 'public, max-age=300' })); + + await assertFails(deleteObject(objectRef)); + }); + + test('does not allow any reads, writes or deletes to an unused object by an authenticated user', async () => { + const storage = testEnv.authenticatedContext('alice').storage(); + const objectRef = ref(storage, 'unused.jpg'); + + await assertFails(getDownloadURL(objectRef)); + + await assertFails(updateMetadata(objectRef, { cacheControl: 'public, max-age=300' })); + + await assertFails(deleteObject(objectRef)); + }); +});