くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Firebaseのセキュリティルールを単体テストする(@firebase/rules-unit-testing+vitest)

Firebase Emulatorを使った単体テストについて、
いろいろ調べてみたときの備忘録(*´ω`*)

@firebase/rules-unit-testingを使えばOK(*´ω`*)

環境的には、この記事と一緒でVitestをつかってる

テストしたいルール

firestore.rulesはこんな感じ

// ./firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow read: if request.auth.uid == uid;
      allow write: if request.auth.uid == uid;
    }

    match /@users/{uid} {
      allow read: if request.auth.uid == uid;
      allow write: if request.auth.uid == uid;
    }

    // Default Rules
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

テストコード

テストコードはこんな感じ

// ./__tests__/security-rule.test.ts
import {
  RulesTestEnvironment,
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
} from "@firebase/rules-unit-testing";
import { Firestore, doc, setDoc } from "firebase/firestore";
import fs from "node:fs";
import { resolve } from "pathe";
import { ulid } from "ulid";
import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  it,
} from "vitest";

// utilな関数
async function setDocSuccess(fb: Firestore, path: string, data: object) {
  await assertSucceeds(setDoc(doc(fb, path), data));
}
async function setDocFails(fb: Firestore, path: string, data: object) {
  await assertFails(setDoc(doc(fb, path), data));
}

// テスト本体
describe("firebase.security-rule", async () => {
  let testEnv: RulesTestEnvironment | null = null;

  // TestEnvironmentの初期化とfirestore.rulesの読み込み
  beforeAll(async () => {
    const filePath = resolve("./firestore.rules");
    const rules = fs.readFileSync(filePath, "utf8");
    testEnv = await initializeTestEnvironment({
      projectId: "my-test-project",
      firestore: { rules },
    });
  });

  afterAll(async () => {
    // テスト環境で作成されたすべての RulesTestContexts を破棄
    testEnv?.cleanup();
  });

  beforeEach(async () => {});
  afterEach(async () => {
    // Firestoreのクリア
    await testEnv?.clearFirestore();
  });

  // 認証時のテスト
  it("test-context-auth", async () => {
    const uid = ulid();
    const user = testEnv.authenticatedContext(uid);
    const firestore = user.firestore() as unknown as Firestore;

    await setDocSuccess(firestore, `/users/${uid}`, { data: "data" });
    await setDocSuccess(firestore, `/@users/${uid}`, { data: "data" });
  });

  // 未認証時のテスト
  it("test-context-guest", async () => {
    const uid = ulid();
    const user = testEnv.unauthenticatedContext();
    const firestore = user.firestore() as unknown as Firestore;

    await setDocFails(firestore, `/users/${uid}`, { data: "data" });
    await setDocFails(firestore, `/@users/${uid}`, { data: "data" });
  });
});

テストを実行

firebase emulators:execを使うと、
エミュレータを起動しつつ、テストを実行できる。

長くなりがちなので、package.jsonscriptsを用意。

{
  "scripts": {
    "test": "env-cmd -f .env.test pnpm em:run 'pnpm vitest-es'",
    "em:run": "firebase emulators:exec --only auth,firestore --project=my-test-project",
    "vitest-es": "NODE_OPTIONS=\"--enable-source-maps --experimental-vm-modules\" vitest --single-thread --run"
  },
}

あとは、実行すればOK

$ pnpm test ./__tests__/security-rule.test.ts

ハマったポイント

同じコンテキストからfirebase()を複数回取得はNG

こんな感じで、同じRulesTestContextから
複数回firestore()を取得しようとすると

it("...", async () => {
  const user = testEnv.authenticatedContext(uild());

  await setDocFails(user.firestore(), `/users/${uid}`, { data: "data" });
  await setDocFails(user.firestore(), `/@users/${uid}`, { data: "data" });
});

こんなエラーになる。。

FirebaseError: Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.

なので、こんな感じであればOK。

it("...", async () => {
  const user = testEnv.authenticatedContext(uild());
  const user2 = testEnv.authenticatedContext(uild());

  await setDocFails(user.firestore(), `/users/${uid}`, { data: "data" });
  await setDocFails(user2.firestore(), `/@users/${uid}`, { data: "data" });
});

もちろん、RulesTestContextをbeforeAllとかで作成して、使い回す場合も同じく問題になるので注意。


以上!! 便利。。(´ω`)

参考にしたサイト様