I'm building out security rules along with some emulator-based unit tests so we can make sure they work as we expect. Generally, everything has been working very well, with tests succeeding and failing as I expect.
However, I'm running into an odd issue of a command which should succeed, yet always fails in a unit-test run on a local emulator. The Rules Playground accepts the call, though.
The Setup
The rules file (stripped down to not include the related rules/functions) is
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isSuper() {
return isSignedIn() && request.auth.token.isSuper == true;
}
function isUser(userId) {
return (isSignedIn() && request.auth.uid == userId);
}
function isMemberOf(facilityId) {
return isSignedIn() &&
(exists(/databases/$(database)/documents/Facilities/$(facilityId)/Members/$(request.auth.uid));
}
// ...
match /Facilities/{facilityId} {
allow read: if (isSuper() || isMemberOf(facilityId));
allow write: if isSuper();
match /Members/{userId} {
allow read: if (isSuper() || isUser(userId) || isMemberOf(facilityId));
allow write: // ...
}
}
// ...
}
}
The top section of my test/firestore.spec.js looks like this:
before(async () => {
// Silence expected rules rejections from Firestore SDK. Unexpected rejections
// will still bubble up and will be thrown as an error (failing the tests).
setLogLevel('error');
testEnv = await initializeTestEnvironment({
firestore: {rules: readFileSync('firestore.rules', 'utf8')},
});
});
after(async () => {
// Delete all the FirebaseApp instances created during testing.
// Note: this does not affect or clear any data.
await testEnv.cleanup();
// Write the coverage report to a file
const coverageFile = 'firestore-coverage.html';
const fstream = createWriteStream(coverageFile);
await new Promise((resolve, reject) => {
const { host, port } = testEnv.emulators.firestore;
const quotedHost = host.includes(':') ? `[${host}]` : host;
http.get(`http://${quotedHost}:${port}/emulator/v1/projects/${testEnv.projectId}:ruleCoverage.html`, (res) => {
res.pipe(fstream, { end: true });
res.on("end", resolve);
res.on("error", reject);
});
});
console.log(`View firestore rule coverage information at ${coverageFile}\n`);
});
beforeEach(async () => {
await testEnv.clearFirestore();
superDb = testEnv.authenticatedContext('super', {isSuper: true}).firestore();
// Other auth users...
unauthedDb = testEnv.unauthenticatedContext().firestore();
});
The Test
Though the test I've been normally using looks a bit different, I adjusted it to make it simpler, and to make it match the Rules Playground settings. I get the same behavior with this test:
describe("Facilities", () => {
it('Users can read facilities they belong to', async function() {
await testEnv.withSecurityRulesDisabled(async function() {
await setDoc(doc(superDb, 'Facilities/-Bdc9QkjnNePNeoabzJBOxYM-sYk'), { name: 'Test' });
await setDoc(doc(superDb, 'Facilities/-Bdc9QkjnNePNeoabzJBOxYM-sYk/Members/-Bdc9QkjnNePNeoabzJBOxYM-sYk'), { role: 0 });
});
let testDb = testEnv.authenticatedContext('BK5KNllRpUMmTzQRd4eRJiYv3Wl2').firestore();
await assertSucceeds(getDoc( doc(testDb, 'Facilities/-Bdc9QkjnNePNeoabzJBOxYM-sYk') ));
});
});
Emulator Test Results
I get this error:
FirebaseError:
Property isSuper is undefined on object. for 'get' @ L47
Or, with the isSuper() function removed, I get this instead:
FirebaseError:
false for 'get' @ L47
Rules Playground Test Results
The structure in Firestore approximately matches the image above. (See image.)
I get no error when testing when doing a get of Facilities/-Bdc9QkjnNePNeoabzJBOxYM-sYk with UID BK5KNllRpUMmTzQRd4eRJiYv3Wl2. I can verify that adjusting the UID and making a query with it will cause the read to fail.
What I Have Tried
I have verified that the document exists when I try this setup. Using the superuser's auth profile (which has isSuper() returning true) to get the data works. That is,
await assertSucceeds(getDoc( doc(superDb, 'Facilities/-Bdc9QkjnNePNeoabzJBOxYM-sYk') ));
is successful. This seems to show that the emulator does in-fact have the data available to it.
Is this a bug, with the emulator handling the read differently? Why am I getting different results?