「Unit testing security rules with the Firebase Emulator Suite」でセキュリティルールのtestを学ぶ

公開日時

今更ながらFirebase Live 2020で発表された「Unit testing security rules with the Firebase Emulator Suite」を観た。

Firestoreのセキュリティルールは設定をミスると大事故になりうるのでローカルでセキュリティルールのtestができるようになったのはありがたい。

mochaと@firebase/testingを使ってシンプルなセキュリティルールのテストを作成した。

@firebase/testingについてはnpmのサイトを見るとdeprecatedになっており、今は@firebase/rules-unit-testingが推奨されているとのこと。

以下のサンプルコードでは@firebase/rules-unit-testingを使うようにした。

  • yarn
yarn add -D mocha firebase-admin @firebase/rules-unit-testing
  • firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }

    match /readonly/{docId} {
      allow read: if true;
      allow write: if false;
    }

    match /users/{userId} {
      allow write: if (request.auth.uid == userId);
    }

    match /posts/{postId} {
      allow read: if (resource.data.visibility == "public") || (resource.data.authorId == request.auth.uid);
    }
  }
}
  • test.js
const assert = require("assert");
const firebase = require("@firebase/rules-unit-testing");

const MY_PROJECT_ID = "my-social-app-d9e97";
const myId = "user_abc";
const theirId = "user_xyz";
const myAuth = { uid: myId, email: "abc@gmai.com" };

function getFirestore(auth) {
  return firebase
    .initializeTestApp({ projectId: MY_PROJECT_ID, auth: auth })
    .firestore();
}

function getAdminFirestore() {
  return firebase.initializeAdminApp({ projectId: MY_PROJECT_ID }).firestore();
}

beforeEach(async () => {
  await firebase.clearFirestoreData({ projectId: MY_PROJECT_ID });
});

describe("Our social app", () => {
  it("Understands basic addition", () => {
    assert.strictEqual(2 + 2, 4);
  });

  it("Can read items in the read-only collection", async () => {
    const db = getFirestore();
    const testDoc = db.collection("readonly").doc("testDoc");
    await firebase.assertSucceeds(testDoc.get());
  });

  it("Can't write to items in the read-only collection", async () => {
    const db = getFirestore();
    const testDoc = db.collection("readonly").doc("testDoc2");
    await firebase.assertFails(testDoc.set({ foo: "bar" }));
  });

  it("Can write to a user document with the same ID as out 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 different ID as out 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();
    const testQuery = db
      .collection("posts")
      .where("visibility", "==", "public");
    await firebase.assertSucceeds(testQuery.get());
  });

  it("Can query personal posts", async () => {
    const db = getFirestore(myAuth);
    const testQuery = db.collection("posts").where("authorId", "==", myId);
    await firebase.assertSucceeds(testQuery.get());
  });

  it("Can't query all posts", async () => {
    const db = getFirestore();
    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({ auhorId: theirId, visibility: "public" });

    const db = getFirestore(myAuth);
    const testRead = db.collection("posts").doc(postId);
    await firebase.assertSucceeds(testRead.get());
  });

  it("Can't read a private post belonging to the user", async () => {
    const admin = getAdminFirestore();
    const postId = "private_post";
    const setupDoc = admin.collection("posts").doc(postId);
    await setupDoc.set({ auhorId: theirId, visibility: "private" });

    const db = getFirestore(myAuth);
    const testRead = db.collection("posts").doc(postId);
    await firebase.assertFails(testRead.get());
  });
});

after(async () => {
  await firebase.clearFirestoreData({ projectId: MY_PROJECT_ID });
});

参考


Related #firebase

SharedArrayBuffer updates in Android Chrome 88 and Desktop Chrome 92

クロスオリジン分離対応を実施

Firebase Emulator Suiteで起動しているFunctionsから本番のFirestoreにアクセスする

functionsのみエミュレータを使うようにするとできる

Firebase Functions呼び出し時に Error: function terminated. が発生した場合

firebase functions:logで詳細を確認できる

Cloud BuildでFirebase Hostingのデプロイを行う

リポジトリへのpush以外をトリガーにしたい場合に使用

Firebase FunctionsでonCallで実装しているにも関わらずCORSエラーが発生した場合

Cloud Functions(GCP)の管理画面を確認してみる

JestでFirestoreセキュリティルールのテストを書く

Github ActionsでCIを回せるようになった