Firestoreのruleをテストする


firestoreのruleのテストは重要なのでメモ。

準備

作業場所の準備

作業ディレクトリを作ってfirebase init firestoreを実行。

mkdir rules-test
cd rules-test
firebase init firestore

私は既存プロジェクトを使うことが多いですが、テストなので新規でもなんでも。

❯ Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project

ルールやIndexを設定するファイル名を聞いてくるので、基本デフォでOK。

? What file should be used for Firestore Rules? firestore.rules
? What file should be used for Firestore indexes? firestore.indexes.json

テストに必要なモジュールのインストール

必要なモジュールをインストール。
テストツールとしてjest, firebaseのテストライブラリとしてfirebase/testing、設定ファイルを読み込むためfsを導入。

npm init -f
npm install -D jest @firebase/testing fs

package.jsonの編集

npm testでjestが実行されるようにしておきます。

package.json
.
  "scripts": {
    "test": "jest"
  },
.

エミュレータのセットアップ

これがないとローカルでテストできません。まだsetup(ダウンロード)してないなら落とす。

firebase setup:emulators:firestore

エミュレータの起動

これはテスト実行直前でもいいです。

firebase serve --only firestore

テストの準備

jestは標準で*.test.js, *.spec.jsやtestディレクトリ下のファイルをテストとして認識するので、今回はtestを作り、その下にfirebase.test.jsを置いてみます。

mkdir __test__
touch __test__/firestore.test.js

テストの実装

ルール

とりあえず認証してないとread, writeできないようにしています。

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write:if request.auth != null;
    }
  }
}

テスト

認証ユーザー情報を指定し、read, writeを実行。

firestore.test.js
const firebase = require('@firebase/testing');
const fs = require('fs');

const project_id = "my-project-id";

describe("Firestoreのテスト", () => {

    //実行前に一度だけ実行(初期化)
    beforeAll(
        async () => {
            await firebase.loadFirestoreRules({
                projectId: project_id,
                rules: fs.readFileSync('./firestore.rules', 'utf8'),
            });
        }
    );

    //ブロックが終わるたび実行
    afterEach(
        async () => {
            await firebase.clearFirestoreData({ projectId: project_id }); //データリセット
        }
    );

    //終わった後に一度だけ実行
    afterAll(
        async () => {
            await Promise.all(
                firebase.apps().map((app) => app.delete()) //生成したアプリを削除
            );
        }
    );

    //条件(projectIdとauth情報)をの指定を関数化
    //auth : {uid:'alice'}
    //auth : {uid:'alice', admin:true} admin
    //auth : null 未認証
    function authedApp(auth) {
        return firebase.initializeTestApp({
            projectId: project_id,
            auth: auth,
        }).firestore();
    }

    describe("messagesコレクションのルールテスト", () => {

        //読取りテスト
        test("messageの読取り", async () => {
            //条件(uidやprojectId)を指定してdbを生成
            const db = authedApp({ uid: 'alice' });
            //docRefを取得
            const message = db.collection("message").doc("alice");
            //取得ができるか
            await firebase.assertSucceeds(message.get());
        })

        //書き込みテスト
        test("messageの書き込み", async () => {
            //条件(uidやprojectId)を指定してdbを生成
            const db = authedApp({ uid: "alice" });
            //docRefを取得
            const message = db.collection("message").doc("alice");
            //書き込みができるか
            await firebase.assertSucceeds(
                message.set({ text: "hoge" })
            );
        })
    })

})

テスト実行

firestoreエミュレータを起動

別のコンソールを開いて、エミュレータを起動させておきます。

firebase serve --only firestore

テストの実行

テストを実行します。

npm test

下記のような感じでpassすればOKです。

> jest

 PASS  __test__/firestore.test.js
  Firestoreのテスト
    messagesコレクションのルールテスト
      ✓ messageの読取り (296ms)
      ✓ messageの書き込み (94ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.706s, estimated 8s

参考

  • firestoreのテストについては概要はここらあたりを。
  • 詳しくはここらあたりを。
  • jestについてはここらあたりを。

少しだけ応用

テスト内容を一部変更

読取りテストの認証情報をnull(認証されてない状態)にしてテストを実行してみます。

テスト内容を変更

        //読取りテスト
        test("messageの読取り", async () => {
            //条件(uidやprojectId)を指定してdbを生成
+            const db = authedApp(null);
            //docRefを取得
            const message = db.collection("message").doc("alice");
            //取得ができるか
            await firebase.assertSucceeds(message.get());
        })

そうするとテストは失敗します。

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.616s, estimated 2s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

ルールを変更

readは認証なしでもOKのルールに変更してテストを実行してみます。

firebase.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
      allow write:if request.auth != null;
    }
  }
}

正常にテストが通るはずです。

debug()関数の利用

認証情報をdebug()で囲んでやることにより、内容を出力することができます。
出力は、firestore-debug.logに出力されるようです。

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
+      allow write:if debug(request.auth) != null;
    }
  }
}

auth情報まるまる出力してるのでややこしそうですが、認証情報としてaliceが使われているのがわかります。

map_value {
  fields {
    key: "uid"
    value {
      string_value: "alice"
    }
  }
  fields {
    key: "token"
    value {
      map_value {
        fields {
+          key: "uid"
+          value {
+            string_value: "alice"
+          }
        }
        fields {
          key: "iat"
          value {
            int_value: 0
          }
        }
        fields {
          key: "sub"
          value {
            string_value: "alice"
          }
        }
      }
    }
  }
}

テストのデプロイ

編集したルール(だけ)をデプロイしたい場合は、下記のようにします。

firebase deploy --only firestore:rules