Closed b0ot closed 3 years ago
@b0ot I was not able to reproduce this. Here's what I have:
firestore.rules
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
function postHasAllRequiredFields() {
let requiredFields = ["authorId", "visibility", "content", "headline"];
return (request.resource.data.keys().hasAll(requiredFields));
}
match /posts/{post} {
allow read, write: if postHasAllRequiredFields();
}
}
}
index.test.js
const firebase = require("@firebase/rules-unit-testing");
const app =firebase.initializeTestApp({
projectId: "fir-dumpster"
});
const db = app.firestore();
async function main() {
await firebase.assertSucceeds(db.collection('posts').add({
"authorId": "foo",
"visibility": "bar",
"content": "baz",
"headline": "qux",
}));
console.log("Request succeeded.");
await firebase.assertFails(db.collection('posts').add({
"authorId": "foo",
"visibility": "bar",
"content": "baz",
}));
console.log("Request failed.");
}
main();
Then here's what I get when I run it:
$ firebase emulators:exec "node index.test.js"
i emulators: Starting emulators: firestore
i firestore: Firestore Emulator logging to firestore-debug.log
i Running script: node index.test.js
Request succeeded.
Request failed.
✔ Script exited successfully (code 0)
i emulators: Shutting down emulators.
i firestore: Stopping Firestore Emulator
i hub: Stopping emulator hub
I think you must have a syntax error in your rules somewhere?
Sam, If there is a syntax error, I haven't been able to figure it out. I will confirm that I am able to use let within functions at the match /databases/{database}/documents { level.
I tried to simplify the testing further and created the following.
This works for 100% of test cases
function postHasOnlyAllowedFields() {
return request.resource.data.keys().hasOnly(["authorId", "visibility", "content", "headline", "photo", "tags", "location"])
}
// function postHasOnlyAllowedFields() {
// let requiredAndOptionalFields = ["authorId", "visibility", "content", "headline", "photo", "tags", "location"]
// return request.resource.data.keys().hasOnly(requiredAndOptionalFields);
// }
This does NOT work
// function postHasOnlyAllowedFields() {
// return request.resource.data.keys().hasOnly(["authorId", "visibility", "content", "headline", "photo", "tags", "location"])
// }
function postHasOnlyAllowedFields() {
let requiredAndOptionalFields = ["authorId", "visibility", "content", "headline", "photo", "tags", "location"]
return request.resource.data.keys().hasOnly(requiredAndOptionalFields);
}
The error is:
Error: 3 INVALID_ARGUMENT: Error compiling rules:
L48:9 missing ';' at 'return'
L48 refers to:
return request.resource.data.keys().hasOnly(requiredAndOptionalFields);
Full firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// match /{document=**} {
// allow read, write: if request.time < timestamp.date(2020, 6, 4);
// }
// match /{document=**} {
// allow read, write: if request.auth != null
// }
function userIsModerator() {
return request.auth.token.isModerator == true;
}
function documentFieldsCheckOut(requiredFields, optionalFields) {
let allFields = requiredFields.concat(optionalFields);
// let unused = debug("**hack to allow debugs**")
return request.resource.data.keys().hasAll(requiredFields) && request.resource.data.keys().hasOnly(allFields)
}
function editOnlyChangesFields(allowedFields){
let affectedKeys = request.resource.data.diff(resource.data).affectedKeys();
return affectedKeys.hasOnly(allowedFields);
}
match /offers/{offerID} {
allow read: if request.auth != null
}
match /readonly/{docId} {
allow read: if true;
allow write: if false;
}
match /users/{userId} {
allow write: if (request.auth.uid == userId);
}
match /posts/{postId} {
function postHasAllRequiredFields() {
return (request.resource.data.keys().hasAll(["authorId", "visibility", "content", "headline"]));
}
// function postHasOnlyAllowedFields() {
// return request.resource.data.keys().hasOnly(["authorId", "visibility", "content", "headline", "photo", "tags", "location"])
// }
function postHasOnlyAllowedFields() {
let requiredAndOptionalFields = ["authorId", "visibility", "content", "headline", "photo", "tags", "location"]
return request.resource.data.keys().hasOnly(requiredAndOptionalFields);
}
allow read: if (resource.data.visibility == "public") || (resource.data.authorId == request.auth.uid);
allow update: if ((resource.data.authorId == request.auth.uid) || userIsModerator()) && editOnlyChangesFields(["visibility", "content"])
allow create: if (request.resource.data.authorId == request.auth.uid) && postHasAllRequiredFields() && postHasOnlyAllowedFields()
// allow create: if (request.resource.data.authorId == request.auth.uid) && documentFieldsCheckOut(["authorId", "visibility", "content", "headline"], ["photo", "tags", "location"]);
}
match /rooms/{roomId} {
// Security rules for the rooms go here
function userIsRoomMod() {
return (request.auth.uid in (get(/databases/$(database)/documents/rooms/$(roomId)).data.roomMods));
}
match /posts/{postId} {
allow update: if (resource.data.authorId == request.auth.uid) ||
userIsRoomMod()
// get retrieves a document from the database. Should start /databases/$(database)/documents
}
}
}
}
Full test.js
const assert = require('assert');
const firebase = require('@firebase/rules-unit-testing');
const { request } = require('http');
// const { get } = require('https');
const MY_PROJECT_ID = "fir-securityrules";
const myId = "user_abc"
const myAuth = {uid: "user_abc", email: "abc@gmail.com"};
const modId = "user_mod"
const modAuth = {uid: modId, email: "mod@gmail.com", isModerator: true}
const theirId = "user_xyz"
const EMULATOR_PORT = 8092
function getFirestore(auth) {
const db = firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth }).firestore();
db.useEmulator("localhost", EMULATOR_PORT)
return db
}
function getAdminFirestore() {
const db = firebase.initializeAdminApp({projectId: MY_PROJECT_ID}).firestore();
// db.useEmulator("localhost", EMULATOR_PORT)
return db
}
beforeEach(async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
describe("Majafy App", () => {
it("understands math", () => {
assert.strictEqual(2+2, 4);
});
it("Can read items in the read-only collection", async() => {
const db = getFirestore(null)
const testDoc = db.collection("readonly").doc("testDoc");
await firebase.assertSucceeds(testDoc.get());
})
it("Can't write items in the read-only collection", async() => {
const db = getFirestore(null)
const testDoc2 = db.collection("readonly").doc("testDoc2");
await firebase.assertFails(testDoc2.set({foo: "bar"}));
})
it("Can write to a user document with the same ID as our user", async() => {
const db = getFirestore(myAuth)
const testDoc = db.collection("users").doc(myId);
await firebase.assertSucceeds(testDoc.set({foo: "bar"}));
})
it("Can't write to a user document with the same ID as our user", async() => {
const db = getFirestore(myAuth)
const testDoc = db.collection("users").doc(theirId);
await firebase.assertFails(testDoc.set({foo: "bar"}));
})
it("Can read posts marked public", async() => {
const db = getFirestore(null)
const testQuery = db.collection("posts").where("visibility", "==", "public")
await firebase.assertSucceeds(testQuery.get());
})
it("Can read my own posts", async() => {
const db = getFirestore(myAuth)
const testQuery = db.collection("posts").where("authorId", "==", myId);
await firebase.assertSucceeds(testQuery.get());
})
it("Can't read all posts", async() => {
const db = getFirestore(myAuth)
const testQuery = db.collection("posts");
await firebase.assertFails(testQuery.get());
})
it("Can read a single public post", async() => {
const admin = getAdminFirestore();
const postId = "public_post";
const setupDoc = admin.collection("posts").doc(postId)
await setupDoc.set({authorId: theirId, visibility: "public"})
const db = getFirestore(null)
const testRead = db.collection("posts").doc(postId);
await firebase.assertSucceeds(testRead.get());
})
it("Can't read a private post belonging to another user", async() => {
const admin = getAdminFirestore();
const postId = "private_post";
const setupDoc = admin.collection("posts").doc(postId)
await setupDoc.set({authorId: theirId, visibility: "private"})
const db = getFirestore(myAuth)
const testRead = db.collection("posts").doc(postId);
await firebase.assertFails(testRead.get());
})
it("Allows a user to edit their own post", async() => {
const postId = "post_123"
const admin = getAdminFirestore()
await admin.collection('posts').doc(postId).set({content: "before", authorId: myId})
const db = getFirestore(myAuth);
const testDoc = db.collection("posts").doc(postId)
await firebase.assertSucceeds(testDoc.update({content: "after"}))
})
it("Doesn't allow a user to edit somebody elses post", async() => {
const postId = "post_123"
const admin = getAdminFirestore()
await admin.collection('posts').doc(postId).set({content: "before", authorId: theirId})
const db = getFirestore(myAuth);
const testDoc = db.collection("posts").doc(postId)
await firebase.assertFails(testDoc.update({content: "after"}))
})
it("Allows a moderator to edit somebody elses post", async() => {
const postId = "post_123"
const admin = getAdminFirestore()
await admin.collection('posts').doc(postId).set({content: "before", authorId: theirId})
const db = getFirestore(modAuth);
const testDoc = db.collection("posts").doc(postId)
await firebase.assertSucceeds(testDoc.update({content: "after"}))
})
it("Allows a user to edit their own room post", async() => {
const postPath = "/rooms/room_abc/posts/post_123"
const admin = getAdminFirestore()
await admin.doc(postPath).set({content: "before", authorId: myId})
const db = getFirestore(myAuth);
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.update({content: "after"}))
})
it("Won't allow a user to edit someone else's room post", async() => {
const postPath = "/rooms/room_abc/posts/post_123"
const admin = getAdminFirestore()
await admin.doc(postPath).set({content: "before", authorId: theirId})
const db = getFirestore(myAuth);
const testDoc = db.doc(postPath)
await firebase.assertFails(testDoc.update({content: "after"}))
})
it("Allows room mod to edit another person's room post", async() => {
const roomPath = "/rooms/room_abc";
const postPath = `${roomPath}/posts/post_123`;
//const postPath = "/rooms/room_abc/posts/post_123"
const admin = getAdminFirestore()
await admin.doc(roomPath).set({topic: "Unit testers", roomMods: [myId, "fooUser"]})
await admin.doc(postPath).set({content: "before", authorId: theirId, headline: "headline", visibility: "public"})
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.update({content: "after"}))
})
it("allows a user to create a post when they list themselves as teh author", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.set({content: "after", authorId: myId, headline: "headline", visibility: "public"}))
})
it("doesn't allow a post when they list a different author", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertFails(testDoc.set({content: "after", authorId: theirId}))
})
it("allows a user to create a post when they have all required fields", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.set({content: "after", authorId: myId, headline: "headline", visibility: "public"}))
})
it("doesn't allow a user to create a post when they don't have all required fields", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertFails(testDoc.set({content: "after", authorId: myId}))
})
it("Can create a post with all required and optional fields", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.set({content: "content", authorId: myId, headline: "headline", visibility: "public", location: "San Fran", tags: ["screencast", "tutorial"], photo: "url_goes_here"}))
})
it("doesn't allow a user to create a post when they don't have all required fields", async() => {
const postPath = "/posts/post_123"
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertFails(testDoc.set({content: "after", authorId: myId, headline: "headline", visibility: "public", not_allowed: true}))
})
it("Can edit a post with allowed fields", async() => {
const postPath = "/posts/post_123"
const admin = getAdminFirestore();
await admin.doc(postPath).set({content: "before_content", authorId: myId, headline: "before_headline", visibility: "public"})
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertSucceeds(testDoc.update({content:"after_content"}));
})
it("Can't edit a post's forbidden fields", async() => {
const postPath = "/posts/post_123"
const admin = getAdminFirestore();
await admin.doc(postPath).set({content: "before_content", authorId: myId, headline: "before_headline", visibility: "public"})
const db = getFirestore(myAuth)
const testDoc = db.doc(postPath)
await firebase.assertFails(testDoc.update({headline:"sneaky change?"}));
})
})
after(async() => {
await firebase.clearFirestoreData({projectId: MY_PROJECT_ID});
});
Ok, I'm an idiot... the error and Line Number had me so focused on the return statement, however it was actually the line above that had the issue.
Error was: Error: 3 INVALID_ARGUMENT: Error compiling rules: L48:9 missing ';' at 'return'
L47: let requiredAndOptionalFields = ["authorId", "visibility", "content", "headline", "photo", "tags", "location"] L48: return request.resource.data.keys().hasOnly(requiredAndOptionalFields);
I simply needed to add a ';' to the end of L47 let requiredAndOptionalFields = ["authorId", "visibility", "content", "headline", "photo", "tags", "location"];
I think my earlier frustrations have caused me to be a bit too quick to jump to "bug" as opposed to doing a proper amount of normal debugging. I'll try to vet my problems more before future bug reports. I also could have sworn I had tried this but obviously not.
Apologies to @samtstern and team.
@b0ot thank you for following up and glad you fixed it! I think your earlier frustrations make it perfectly reasonable that you suspect a bug, you have definitely encountered some real ones before.
Importing clearFirestoreData from '@firebase/rules-unit-testing'; no longer works with version 2.0.0. How do we import it now?
@samtstern
Version info
firebase-functions-test:
firebase-functions:
firebase-admin:
firebase -V: 8.17.0
Test case
Based on Firebase YouTube Tutorial: Intermediate topics in Firebase Security Rules - Firecasts
Steps to reproduce
Todd has just refactored his code from:
Expected behavior
Refactored code should still pass test cases
Actual behavior
results in multiple errors:
The same result happens in the next section as well when Todd does the posthasonlyAllowedFields.
It appears that the "let" syntax is not valid.