I am testing my Firestore security rules and have the following setup:
// firestore.spec.js
/**
* Creates a new client FirebaseApp with authentication and returns the Firestore instance.
* Also optionally seeds the project with mock data
*/
setupFirestoreDb = async (auth, data) => {
// initialize test app
const PROJECT_ID = "my-test-project-xyz";
const app = firebase.initializeTestApp({ projectId: PROJECT_ID, auth });
const db = app.firestore();
// Write mock documents
if (data) {
for (const key in data) {
const ref = db.doc(key);
ref.set(data[key]);
}
}
return db;
};
beforeEach(async () => {
// Clear the database between tests
await firebase.clearFirestoreData({ projectId: PROJECT_ID });
});
before(async () => {
// Load the rules file before the tests begin
const rules = fs.readFileSync("firestore.rules", "utf8");
await firebase.loadFirestoreRules({ projectId: PROJECT_ID, rules });
});
after(async () => {
// Delete all the FirebaseApp instances created during testing
// Note: this does not affect or clear any data
await Promise.all(firebase.apps().map((app) => app.delete()));
});
const mockData = {
'users/alice': {
foo: 'bar',
nestedData: {
baz: 'fae'
}
},
'users/bob': {
foo: 'bar',
nestedData: {
baz: 'fae'
}
},
// ... more data
}
// run the test suite
describe("Test Security Rules", () => {
it("should let any signed-in user to read any public profile", async () => {
let db = await setupFirestoreDb({uid: "bob"}, mockData);
aliceRef = db.collection("users").doc("alice");
await firebase.assertSucceeds(aliceRef.get()); // fails
});
// more test cases
...
});
And my security rules file:
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// define common functions used across collections/documents
function userExists() {
return exists(/databases/$(database)/documents/users/$(request.auth.uid));
}
// Fetch a user from Firestore by UID
function getUserData(uid) {
return get(/databases/$(database)/documents/users/$(uid)).data
}
function isValidUser() {
let userData = request.resource.data;
return userData.name != null && userData.phoneNumber != null;
}
// ... more functions
// lock the entire database by default
match /{document=**} {
allow read, write: if false;
}
// rules for users collection
match /users/{userId} {
allow read: if isSignedIn() && getUserData(userId) != null; // && userExists(userId); // make sure user being read exists
allow write: if isSignedIn() && isUser(userId) && isValidUser(); // only an authenticated user can create own account
// ... other rules for nested data (subcollections)
}
// ... more rules
}
}
FirebaseError: false for 'get' @ L*, Null value error. for 'get' @ L*
FirebaseError: false for 'get' @ L*, Null value error. for 'get' @ L*
, which occurs because I believe the function getUserData()
returns null (same for userExists()
).
getUserData()
to return true because the document for user 'alice' is created based on the mock data. Is it a problem with the Firestore emulator or is there something wrong with how I set up my mock data & test fixture? I used the Firebase simulator on the console to test the same rules against a real database and the rules work ( getUserData()
and userExists()
work as expected)
The code is largely based on this fireship tutorial and the official firebase unit testing tutorial
Turns out I had missed to see a crucial error in the logs letting me know that my user objects were actually not being written:
(node:25284) UnhandledPromiseRejectionWarning: FirebaseError: 7 PERMISSION_DENIED:
false for 'create' @ L*, Property phoneNumber is undefined on object. for 'create' @ L*
The fix was embarrassingly simple: add the name, phone number fields in the test fixture data to match the fields are enforced by the model/rules. That way, the get()
and exists()
functions would actually fetch against existing user data.
TL;DR - pay attention to the error and warning logs reported by the emulator when enforcing rules
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.