「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

YouTubeの「Firebase Release Notes」プレイリスト

最新のアップデートの概要を把握するのにちょうど良い

Nuxt.jsのservice workerで環境変数を切り替えられるようにする

設定情報をハードコードしたくなかったので、ビルド時にnodeコマンドでファイル生成を行い環境変数を切り替えられるように対応した。

CloudFunctionsを使ってFirestoreのサブコレクションを削除する

CloudFunctions内でFirebase CLIのdeleteコマンドを呼び出すことで一括削除ができる

Firebase Hostingのリリース履歴を一括削除する

いつの間にか一括削除機能が追加されていた