Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint One] Creating Security Rules with TDD
Writing our first security rules tests
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Structure of this Module PUBLIC
- Lesson 3: [Sprint One] Setting up Firebase PUBLIC
- Lesson 4: [Sprint One] Creating Security Rules with TDD PRO
- Lesson 5: [Sprint One] Testing Authentication PRO
- Lesson 6: [Sprint One] Component Store PRO
- Lesson 7: [Sprint One] Circumventing Firebase Authentication for E2E Tests PRO
- Lesson 8: [Sprint Two] Displaying Client List from Firestore PRO
- Lesson 9: [Sprint Two] - Adding Clients PRO
- Lesson 10: [Sprint Two] - Editing Clients PRO
- Lesson 11: [Sprint Two] - Client Details PRO
- Lesson 12: Preparing for Delivery PRO
- Lesson 13: Configuring for Production PRO
- Lesson 14: [Sprint Three] - Refactoring PRO
- Lesson 15: [Sprint Three] Setting up PWA PRO
- Lesson 16: [Sprint Three] Logout PRO
- Lesson 17: [Sprint Three] Delete a Client PRO
- Lesson 18: [Sprint Three] - Feedback Mechanism PRO
- Lesson 19: [Sprint Three] View Feedback PRO
- Lesson 20: More Styling PRO
- Lesson 21: [Sprint Four] - Refactoring Feedback PRO
- Lesson 22: [Sprint Four] - Feedback Dates PRO
- Lesson 23: [Sprint Four] - Client Survey PRO
- Lesson 24: [Sprint Four] - View Survey PRO
- Lesson 25: Final Touches PRO
- Lesson 26: Conclusion PRO
Lesson Outline
Creating Security Rules with TDD
We've done some basic configuration now to install Firebase, the emulators for developing with Firebase locally, and we even have a unit testing solution for our security rules set up. So far, we have just got the basic structure set up, in this lesson we are going to start working towards satisfying our first user story:
Feature: Log in to the application
User story: As a massage therapist, I want to be able to log in to the application using my Google account from any device, so that only I can access private information in the application and I can do it from any device
Project Management
- Move the card for
Issue #21
to theTest
column in the Kanban board - Make sure you have the latest code in your main branch:
git checkout main
git pull
- Create a new branch for this task and switch to it:
git checkout -b jm-21
(always use your own initials instead of mine) a In the beginning, I will give you explicit instructions like this for the project management process. However, as we progress, I will just give simpler prompts so that you can try to implement the process yourself.
We are going to get started by working on our security rules to define how data in our database can be accessed in relation to this feature - e.g. only the authenticated admin user should be able to access data in the database. We will make sure to write tests for these security rules before creating the rules themselves.
Technically, we could start with our Cypress tests first which is where we would usually start: write a failing E2E test that captures the essence of the user story, and use that to develop further tests. If we took this approach we would eventually hit a point where the E2E test fails because the database is denying all read/write requests. At that point it would then prompt us to work on our security rules. If you're not sure where to start, then it is always a safe bet to start with the E2E tests and see where it takes you.
However, our security rules are something that we are just going to need to work on pretty much once in the beginning and then barely ever have to touch them again. So, I would prefer to just get them out of the way now.
Writing unit tests for Firestore Security Rules
The basic idea is that if the massage therapist (admin) logs in with their Google account they will be able to access/modify any of the information in the database, but nobody else can (technically later we will be adding a feature that allows clients to modify the client collection under specific circumstances, but we will address that when we get there).
Let's start by expanding out our tests to ensure that people can not access/modify the data by default, e.g. an unauthenticated user can not:
can not read from collections
can not write to collections
can not read from notes
can not write to notes
Modify
firestore.rules.spec.ts
to reflect the following:
import {
assertFails,
assertSucceeds,
initializeTestEnvironment,
RulesTestEnvironment,
TokenOptions,
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore';
let testEnv: RulesTestEnvironment;
const getFirestore = (authUser?: { uid: string; token: TokenOptions }) =>
authUser
? testEnv.authenticatedContext(authUser.uid, authUser.token).firestore()
: testEnv.unauthenticatedContext().firestore();
describe('Firestore security rules', () => {
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: 'refresh-module',
});
});
beforeEach(async () => {
await testEnv.clearFirestore();
await testEnv.withSecurityRulesDisabled(async (context) => {
const existingClientDocRef = doc(
context.firestore(),
'clients',
'existingDoc'
);
await setDoc(existingClientDocRef, { foo: 'bar' });
});
});
it('can not read/write from the clients collection', async () => {
const db = getFirestore();
const newDoc = doc(db, 'clients', 'testDoc');
const existingDoc = doc(db, 'clients', 'existingDoc');
await Promise.all([
assertFails(getDoc(existingDoc)), //read
assertFails(setDoc(newDoc, { foo: 'bar' })), // create
assertFails(setDoc(existingDoc, { foo: 'bar' })), // update
assertFails(deleteDoc(existingDoc)), // delete
]);
});
afterAll(async () => {
await testEnv.cleanup();
});
});
Aside from the new test which we will talk about in a moment, we have made a minor modification here in our beforeEach
hook. We are utilising the withSecurityRulesDisabled
method to set up some seed data in Firestore (and we do this right after clearing the data for each test, so that each test will have a nice clean slate to start from). This basically just allows us to ignore our rules to set up whatever data we want - in this case, I just want an existing document so that I can test accessing that document.
Since we will be using the more granular definitions of read/write, I have added an assertion for each of the 4
types. Now, this actually goes against a testing principle that I usually like to adhere to which is basically one assertion per test. Asserting multiple things can make your tests muddy and if something fails it isn't as clear exactly what failed. However, setting up a separate test for each of these 4 methods is a bit much (in my opinion), so I think it's a good tradeoff in this instance.
Let's do the same for the notes collection. We will modify the beforeEach
:
beforeEach(async () => {
await testEnv.clearFirestore();
await testEnv.withSecurityRulesDisabled(async (context) => {
const db = context.firestore();
const existingClientDocRef = doc(db, 'clients', 'existingDoc');
const existingNoteDocRef = doc(db, 'notes', 'existingDoc');
await Promise.all([
setDoc(existingClientDocRef, { foo: 'bar' }),
setDoc(existingNoteDocRef, { foo: 'bar' }),
]);
});
});
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).