Firestore 'exists' rule fails during in emulator unit-test but succeeds Rules Playground

Viewed 25

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.)

Structure of Firestore

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.

Passed test and Failed test

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?

0 Answers
Related