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を導入。

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

#### package.jsonの編集

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

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

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

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

```bash
firebase setup:emulators:firestore
```

### エミュレータの起動

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

```bash
firebase serve --only firestore
```

## テストの準備

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

```bash
mkdir __test__
touch __test__/firestore.test.js
```

## テストの実装

### ルール

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

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

### テスト

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

```js: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エミュレータを起動

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

```bash
firebase serve --only firestore
```

#### テストの実行

テストを実行します。

```bash
npm test
```

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

```bash
> 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のテストについては概要は[ここら](https://firebase.google.com/docs/firestore/security/test-rules-emulator?hl=ja)あたりを。
* 詳しくは[ここ](https://tech-blog.sgr-ksmt.org/2018/12/11/194022/)らあたりを。
* jestについては[ここら](https://qiita.com/chimame/items/e97883fd46b67529d59f)あたりを。

## 少しだけ応用

テスト内容を一部変更

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

### テスト内容を変更

```diff
        //読取りテスト
        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のルールに変更してテストを実行してみます。

```js: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に出力されるようです。

```diff: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が使われているのがわかります。

```diff
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"
          }
        }
      }
    }
  }
}

```

## テストのデプロイ

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

```bash
firebase deploy --only firestore:rules
```