├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── package-lock.json ├── package.json └── spec ├── basic.spec.js ├── comments.spec.js ├── helpers.js ├── posts.spec.js └── projects.spec.js /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "angularfirebase-267db" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Episode 147 - Testing Firestore Rules with the Emulator 2 | 3 | Learn how to test your Firebase security rules using the new Cloud Firestore emulator. 4 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [] 3 | } -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | 4 | // Secure by default 5 | match /{document=**} { 6 | allow read: if false; 7 | allow write: if false; 8 | } 9 | 10 | // Unsecured Rules 11 | match /posts/{docId} { 12 | allow read, write; 13 | } 14 | 15 | // Secured to authenticated users 16 | match /comments/{docId} { 17 | allow read: if request.auth.uid != null; 18 | allow write: if request.auth.uid == request.resource.data.userId; 19 | } 20 | 21 | // Role-based authorization 22 | function getUserData() { 23 | return get(/databases/$(database)/documents/users/$(request.auth.uid)).data 24 | } 25 | 26 | match /projects/{docId} { 27 | allow read, write: if getUserData().roles['admin'] == true || resource.data.members.hasAny([request.auth.uid]) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rules", 3 | "version": "1.0.0", 4 | "description": "Learn test your Firebase security rules using the new Firestore emulator.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "jest": "^23.6.0" 14 | }, 15 | "dependencies": { 16 | "@firebase/testing": "^0.3.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/AngularFirebase/147-firestore-emulator-rules-testing.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/AngularFirebase/147-firestore-emulator-rules-testing/issues" 24 | }, 25 | "homepage": "https://github.com/AngularFirebase/147-firestore-emulator-rules-testing#readme" 26 | } 27 | -------------------------------------------------------------------------------- /spec/basic.spec.js: -------------------------------------------------------------------------------- 1 | const { setup, teardown } = require('./helpers'); 2 | const { assertFails, assertSucceeds } = require('@firebase/testing'); 3 | 4 | describe('Database rules', () => { 5 | let db; 6 | let ref; 7 | 8 | // Applies only to tests in this describe block 9 | beforeAll(async () => { 10 | db = await setup(); 11 | 12 | // All paths are secure by default 13 | ref = db.collection('some-nonexistent-collection'); 14 | }); 15 | 16 | afterAll(async () => { 17 | await teardown(); 18 | }); 19 | 20 | test('fail when reading/writing an unauthorized collection', async () => { 21 | const failedRead = await assertFails(ref.get()); 22 | expect(failedRead); 23 | 24 | // One-line await 25 | expect(await assertFails(ref.add({}))); 26 | 27 | // Custom Matchers 28 | await expect(ref.get()).toDeny(); 29 | await expect(ref.get()).toAllow(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/comments.spec.js: -------------------------------------------------------------------------------- 1 | const { setup, teardown } = require('./helpers'); 2 | 3 | describe('Comments rules', () => { 4 | afterEach(async () => { 5 | await teardown(); 6 | }); 7 | 8 | test('fail when not authenticated', async () => { 9 | const db = await setup(); 10 | 11 | const commentsRef = db.collection('comments'); 12 | 13 | await expect(commentsRef.get()).toDeny(); 14 | await expect(commentsRef.add({})).toDeny(); 15 | }); 16 | 17 | test('pass when authenticated', async () => { 18 | const db = await setup({ 19 | uid: 'jeffd23', 20 | email: 'hello@angularfirebase.com' 21 | }); 22 | 23 | const commentsRef = db.collection('comments'); 24 | 25 | await expect(commentsRef.get()).toAllow(); 26 | await expect(commentsRef.add({ userId: 'jeffd23' })).toAllow(); 27 | await expect(commentsRef.add({ userId: 'someOtherUser' })).toDeny(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /spec/helpers.js: -------------------------------------------------------------------------------- 1 | const firebase = require('@firebase/testing'); 2 | const fs = require('fs'); 3 | 4 | module.exports.setup = async (auth, data) => { 5 | const projectId = `rules-spec-${Date.now()}`; 6 | const app = await firebase.initializeTestApp({ 7 | projectId, 8 | auth 9 | }); 10 | 11 | const db = app.firestore(); 12 | 13 | // Write mock documents before rules 14 | if (data) { 15 | for (const key in data) { 16 | const ref = db.doc(key); 17 | await ref.set(data[key]); 18 | } 19 | } 20 | 21 | // Apply rules 22 | await firebase.loadFirestoreRules({ 23 | projectId, 24 | rules: fs.readFileSync('firestore.rules', 'utf8') 25 | }); 26 | 27 | return db; 28 | }; 29 | 30 | module.exports.teardown = async () => { 31 | Promise.all(firebase.apps().map(app => app.delete())); 32 | }; 33 | 34 | expect.extend({ 35 | async toAllow(x) { 36 | let pass = false; 37 | try { 38 | await firebase.assertSucceeds(x); 39 | pass = true; 40 | } catch (err) {} 41 | 42 | return { 43 | pass, 44 | message: () => 'Expected Firebase operation to be allowed, but it failed' 45 | }; 46 | } 47 | }); 48 | 49 | expect.extend({ 50 | async toDeny(x) { 51 | let pass = false; 52 | try { 53 | await firebase.assertFails(x); 54 | pass = true; 55 | } catch (err) {} 56 | return { 57 | pass, 58 | message: () => 59 | 'Expected Firebase operation to be denied, but it was allowed' 60 | }; 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /spec/posts.spec.js: -------------------------------------------------------------------------------- 1 | const { setup, teardown } = require('./helpers'); 2 | 3 | describe('Post rules', () => { 4 | let db; 5 | let postsRef; 6 | // Applies only to tests in this describe block 7 | beforeAll(async () => { 8 | db = await setup(); 9 | // Allow rules in place for this collection 10 | postsRef = db.collection('posts'); 11 | }); 12 | 13 | afterAll(async () => { 14 | await teardown(); 15 | }); 16 | 17 | test('succeeds when reading/writing an authorized collection', async () => { 18 | await expect(postsRef.add({})).toAllow(); 19 | await expect(postsRef.get()).toAllow(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /spec/projects.spec.js: -------------------------------------------------------------------------------- 1 | const { setup, teardown } = require('./helpers'); 2 | 3 | const mockData = { 4 | 'users/jeffd23': { 5 | roles: { 6 | admin: true 7 | } 8 | }, 9 | 'projects/testId': { 10 | members: ['bob'] 11 | } 12 | }; 13 | 14 | describe('Project rules', () => { 15 | let db; 16 | let projectsRef; 17 | // Applies only to tests in this describe block 18 | beforeAll(async () => {}); 19 | 20 | afterAll(async () => { 21 | await teardown(); 22 | }); 23 | 24 | test('deny a user without the admin role', async () => { 25 | const db = await setup({ uid: null }, mockData); 26 | 27 | // Allow rules in place for this collection 28 | projRef = db.doc('projects/testId'); 29 | await expect(projRef.get()).toDeny(); 30 | }); 31 | 32 | test('allow a user with the admin role', async () => { 33 | const db = await setup({ uid: 'jeffd23' }, mockData); 34 | 35 | // Allow rules in place for this collection 36 | projRef = db.doc('projects/testId'); 37 | await expect(projRef.get()).toAllow(); 38 | }); 39 | 40 | test('deny a user if they are not on the Access Control List or an admin', async () => { 41 | const db = await setup({ uid: 'frank' }, mockData); 42 | 43 | // Allow rules in place for this collection 44 | projRef = db.doc('projects/testId'); 45 | await expect(projRef.get()).toDeny(); 46 | }); 47 | 48 | test('allow a user if they are on the Access Control List', async () => { 49 | const db = await setup({ uid: 'bob' }, mockData); 50 | 51 | // Allow rules in place for this collection 52 | projRef = db.doc('projects/testId'); 53 | await expect(projRef.get()).toAllow(); 54 | }); 55 | }); 56 | --------------------------------------------------------------------------------