１つめのFirestoreセキュリティルールテスト


本記事はFirebase公式ドキュメントを噛み砕いた内容になってます。
公式ドキュメントにはテストツールについて触れられておらず個人的に進めずらかったので、環境構築の部分とテストツールとしてJestを利用する方法を織り交ぜてまとめました。

## エミュレータの嬉しいところ

セキュリティルールのテストはFirestoreのローカルエミュレータを使用すると開発効率がUPします。

Firestoreのセキュリティルールが正しく記述できているかどうかをクライアント側でいちいち確認しようとすると、エラーが発生したときにPermission Deniedとしか出力されません。これでは原因調査がなかなか大変なのですが、ローカルエミュレータを使用するともう少し詳細な原因を提示してくれるのでルールの記述が捗って嬉しいです☺️


## 前準備として

FirebaseCLIでプロジェクトの設定は終わっている状態です。
`firebase login` や `firebase init firestore`でfirestore.rulesが作成されているとします。

## サンプル リポジトリ

説明不足なところはサンプルを参考にしてください🙏
https://github.com/trueSuperior/Firestore-rules-test

## Setup

### package.json

```json
{
  "devDependencies": {
    "@firebase/testing": "0.9.4",
    "@types/jest": "23.3.9",
    "jest": "23.6.0",
    "filesystem": "1.0.1",
    "source-map-support": "0.5.12",
    "ts-jest": "22.4.6",
    "ts-node": "8.1.0",
    "typescript": "3.4.5"
  }
}
```

`@firebase/testing`がfirestoreのエミュレータを操作するためのモジュールになります。そのほか`TypeScript`、`Jest`等を入れます。
依存モジュールたちをインストールします。

```sh
npm i
```


別途`tsconfig.json`の作成と`package.json`にJestの設定が必要になります。

### Firestoreエミュレータ

ローカルエミュレータのインストール

```sh
firebase setup:emulators:firestore
```

## セキュリティルール

```js
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read;
      allow create: if request.auth.uid == userId && request.resource.data.createdAt == request.time;
    }
    match /rooms/{roomId} {
      allow read;
      // If you create a room, you must set yourself as the owner.
      allow create: if request.resource.data.owner == request.auth.uid;
      // Only the room owner is allowed to modify it.
      allow update: if resource.data.owner == request.auth.uid;
    }
  }
}
```

サンプルとして公式の[TypeScriptクイックスタート](https://github.com/firebase/quickstart-nodejs/tree/master/firestore-emulator/typescript-quickstart)リポジトリのルールを使用します。

- `users`, `rooms`コレクションは誰でも読み取り可能
- ユーザー自身のuidと登録する`users`のドキュメントIDが同一の場合のみ`users`のドキュメントが作成可能
- 登録する`rooms`ドキュメントの`owner`フィールドの値がユーザー自身のuidと同一の場合のみドキュメントを作成可能
- 作成済み`rooms`ドキュメントの`owner`フィールドの値が自身のuidと同一の場合のみデータの更新が可能

> 余談なのですが。上記のルールではroomsのownerフィールドは変更後の制限をしていないため、自身のデータであれば好きに書き換えることができます。場合によってはチートに利用されてしまうケースがあるかと思うので注意が必要です。

## @firebase/testingモジュールについて

```typescript
import * as firebase from '@firebase/testing'
```

モジュールで利用する機能は主に４つあります。

- Firebaseアプリの作成
- ルールファイルの読み込み
- 読み書きの成功失敗をアサート
- クリーンアップ

### Firebaseアプリの作成

- 特定ユーザーとして認証されたアプリ

```typescript
const app = firebase.initializeTestApp({
   projectId: "my-test-project",
   auth: { uid: "alice", email: "alice@example.com" }
 })
```
`auth`に`null`を設定すると認証していないユーザーで初期化されます。

- 管理者として認証されたアプリ

```typescript
const app = firebase.initializeAdminApp({ projectId: "my-test-project" })
```

### ルールファイルの読み込み

```typescript
firebase.loadFirestoreRules({
   projectId: "my-test-project",
   rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
 })
 ```

### 読み書きの成功失敗をアサート

- 失敗

```typescript
firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get())
```

- 成功

```typescript
firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get())
```

### クリーンアップ

- プロジェクトに関連付けられているデータをクリア

```typescript
firebase.clearFirestoreData({
 projectId: "my-test-project"
})
```

- アプリの全消去

```typescript
Promise.all(firebase.apps().map(app => app.delete()))
```

## テストを書く

### Jest記法について

```typescript

import * as firebase from '@firebase/testing'
import * as fs from 'fs'

const testName = 'firestore-local-emulator-test'
const rulesFilePath = 'firestore.rules'

describe(testName, () => {
    // はじめに１度ルールを読み込ませる
    beforeAll(async () => {
        await firebase.loadFirestoreRules({
            projectId: testName,
            rules: fs.readFileSync(rulesFilePath, 'utf8')
        })
    })

    // test毎にデータをクリアする
    afterEach(async () => {
        await firebase.clearFirestoreData({ projectId: testName })
    })

    // 全テスト終了後に作成したアプリを全消去
    afterAll(async () => {
        await Promise.all(firebase.apps().map(app => app.delete()))
    })

    // describe('users collection tests', () => { ...
}
```

テスト構文はJestの記法になります。読むとなんとなくわかってもらえるかと思いますが簡単に説明すると以下になります。

- `describe`でブロックの作成
- `before~`, `after~`で事前準備や後始末などを行う
- `test`でテストの実行

`describe`でブロックを作成するとテスト結果をみたときに文脈がわかりやすいのがいい感じです。個人的には'create', 'update'などでブロックをつくるのがおすすめです。

> describeやtestの後に.onlyと繋げるとそのブロック以外のテストをスキップしてくれるので便利です。
> `describe.only('users collection tests', () => { ...`
> Jestの記法は他にもあるので興味があれば調べてみるとよいかと思います。

### １つめのテストを書く

```typescript

// users
describe('users collection tests', () => {
    describe('read', () => {
        test('should let anyone read any profile', async () => {
            const db = authedApp(null)
            const user = db.collection('users').doc('alice')
            await firebase.assertSucceeds(user.get())
        })
    })
})

function authedApp(auth: object): firebase.firestore.Firestore {
  return firebase
    .initializeTestApp({ projectId: testName, auth: auth })
    .firestore()
}
```

ようやく１つめのテストを作成することができましたのでさっそく実行します。

エミュレータの起動

```sh

firebase serve --only firestore
```

テストの実行

```sh

npm test
```

すると、、、
<img width="899" alt="スクリーンショット 2019-05-27 20.29.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203147/0e7f9c7a-81e7-a139-dbd2-fa3b93b7190c.png">

おめでとうございます！🎉
まず１つめのテストを記述することができました。

[サンプルリポジトリ](https://github.com/trueSuperior/Firestore-rules-test)ではその他のテストも記述しているのでよければ参考にしてみてください。

## おわりに
`npm test -- --watch`でファイルの変更を監視してテストを走らせることができる機能もご活用ください。


## 参考記事
- https://firebase.google.com/docs/firestore/security/test-rules-emulator?hl=ja
- https://techlife.cookpad.com/entry/2018/11/05/143000