これまで通り、こちらのリポジトリを元に実装していこうと思います。
以下、今回の対象となるやつらです。
test
├── Executor.ts
├── firestore
│   ├── setupTest.ts
│   └── vegetable.spec.ts
└── libs
    └── firebaseUtil.ts
coverage
└── firestore_rules
    └── vegetable.html
jest.config.js
tsconfig.test.json
firestore.rules前提条件
- Node.jsがインストールされていること。
- Nuxtプロジェクトがすでにセットアップされていること。
- Firebaseプロジェクトが設定されていること。
- Typescriptを使っている
細かいとこは省きますので、ハンズオン形式でトライする場合は差分の確認をおすすめします。
セットアップ
まずは必要なパッケージを入れていきます。
$ yarn add @types/jest jest ts-jest -DFirebaseのセキュリティルールに沿ってテストを書いていくのでこちらも。
$ yarn add @firebase/rules-unit-testing -Dテストケースの確認
まずはセキュリティルールを確認しましょう。
- 書き込み権限は認証済みユーザーのみ許可。
- 読み取り権限は未認証でも許可。
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }
    match /vegetables/{vegetableId} {
      allow get: if true;
      allow list: if true;
      allow create: if isAuthenticated();
      allow update: if isAuthenticated();
      allow delete: if isAuthenticated();
    }
  }
}readとwrite権限だけでも動作するのですが、せっかくテストするので詳細に書いてあげましょう。
セキュリティルールを読み込む
前提として、127.0.0.1:8080 でfirestoreが起動していることが必要です。
ルート直下にある firestore.rules を読み込みます。
import {
  initializeTestEnvironment as _initializeTestEnvironment,
  RulesTestEnvironment,
} from '@firebase/rules-unit-testing'
import { readFileSync } from 'fs'
let testEnv: RulesTestEnvironment
export const initializeTestEnvironment = async (projectId: string) => {
  testEnv = await _initializeTestEnvironment({
    projectId,
    firestore: {
      host: '127.0.0.1',
      port: 8080,
      rules: readFileSync('firestore.rules', 'utf8'),
    },
  })
}
export const getTestEnv = () => testEnvルールに合わせて共通処理を
test/Executor.ts にセキュリティルールでやることをまとめておき、各specで使い回します。
import { type KeyPair } from '@/types/KeyPair'
import { FirebaseFirestore } from '@firebase/firestore-types'
import {
  QuerySnapshot,
  deleteDoc,
  doc,
  getDoc,
  getDocs,
  setDoc,
  updateDoc
} from 'firebase/firestore'
export default class Executor {
  db: FirebaseFirestore
  collectionName: string
  constructor(
    db: FirebaseFirestore,
    collectionName: string
  ) {
    this.db = db
    this.collectionName = collectionName
  }
  collectionRef() {
    return this.db.collection(this.collectionName)
  }
  documentRef(id: string) {
    return doc(this.collectionRef(), id)
  }
  async get(id: string) {
    return getDoc(this.documentRef(id))
  }
  async all(): Promise<QuerySnapshot> {
    return getDocs(this.collectionRef())
  }
  async create(id: string, data: KeyPair): Promise<void> {
    return setDoc(this.documentRef(id), data)
  }
  async update(id: string, data: KeyPair): Promise<void> {
    return updateDoc(this.documentRef(id), data)
  }
  async delete(id: string): Promise<void> {
    return deleteDoc(this.documentRef(id))
  }
}テストを書いていく
まず、セキュリティルールからどのようにテストを書いていくかを考えていきます。
権限は以下の通り。
| 認証済み | 未認証 | |
|---|---|---|
| get | ⚪︎ | ⚪︎ | 
| list | ⚪︎ | ⚪︎ | 
| create | ⚪︎ | × | 
| update | ⚪︎ | × | 
| delete | ⚪︎ | × | 
認証済み、未認証をdescribeで括ります。
難しいことはしません。firebaseが用意してくれている assertSucceeds, assertFails 関数で確認していきましょう。
import Executor from '@/test/Executor'
import { FirebaseFirestore } from '@firebase/firestore-types'
import {
  assertSucceeds,
  assertFails,
} from '@firebase/rules-unit-testing'
import { getTestEnv } from '@/test/libs/firebaseUtil'
import { setupTest } from '@/test/firestore/setupTest'
import { Vegetable } from '@/models/Vegetable'
import {
  beforeEach,
  describe,
  it,
} from '@jest/globals'
setupTest('vegetable')
describe('Firestore Vegetable rules', () => {
  let db: FirebaseFirestore
  let executor: Executor
  beforeEach(async () => {
    await getTestEnv().withSecurityRulesDisabled(async context => {
      const adminDB = context.firestore()
      executor = new Executor(adminDB, Vegetable.MODEL_NAME)
      const vegetables = [
        { id: 'vege_1_id', name: 'carrot' },
        { id: 'vege_2_id', name: 'pumpkin' },
        { id: 'vege_3_id', name: 'potato' },
      ]
      for (const vege of vegetables) {
        await executor.create(vege.id, { name: vege.name })
      }
    })
  })
  describe('認証済み', () => {
    beforeEach(async () => {
      db = getTestEnv().authenticatedContext('user_01').firestore()
      executor = new Executor(db, Vegetable.MODEL_NAME)
    })
    it('取得:全てできること', async () => {
      await assertSucceeds(executor.get('vege_1_id'))
      await assertSucceeds(executor.get('vege_2_id'))
      await assertSucceeds(executor.get('vege_3_id'))
    })
    it('一覧:全てできること', async () => {
      await assertSucceeds(executor.all())
    })
    it('登録:全てできること', async () => {
      await assertSucceeds(executor.create('new_id', { name: 'new_name' }))
    })
    it('更新:全てできること', async () => {
      await assertSucceeds(executor.update('vege_1_id', { name: 'update_name' }))
      await assertSucceeds(executor.update('vege_2_id', { name: 'update_name' }))
      await assertSucceeds(executor.update('vege_3_id', { name: 'update_name' }))
    })
    it('削除:全てできること', async () => {
      await assertSucceeds(executor.delete('vege_1_id'))
      await assertSucceeds(executor.delete('vege_2_id'))
      await assertSucceeds(executor.delete('vege_3_id'))
    })
  })
  describe('未認証', () => {
    beforeEach(async () => {
      db = getTestEnv().unauthenticatedContext().firestore()
      executor = new Executor(db, Vegetable.MODEL_NAME)
    })
    it('取得:全てできること', async () => {
      await assertSucceeds(executor.get('vege_1_id'))
      await assertSucceeds(executor.get('vege_2_id'))
      await assertSucceeds(executor.get('vege_3_id'))
    })
    it('一覧:全てできること', async () => {
      await assertSucceeds(executor.all())
    })
    it('登録:全てできないこと', async () => {
      await assertFails(executor.create('new_id', { name: 'new_name' }))
    })
    it('更新:全てできないこと', async () => {
      await assertFails(executor.update('vege_1_id', { name: 'update_name' }))
      await assertFails(executor.update('vege_2_id', { name: 'update_name' }))
      await assertFails(executor.update('vege_3_id', { name: 'update_name' }))
    })
    it('削除:全てできないここと', async () => {
      await assertFails(executor.delete('vege_1_id'))
      await assertFails(executor.delete('vege_2_id'))
      await assertFails(executor.delete('vege_3_id'))
    })
  })
})注意
ちょっとしたポイントをまとめておきます。
- 認証済みは authenticatedContextで設定。
- 未認証は unauthenticatedContextを使おう。
- setupTestはケバブケースで書こう。
実行方法
package.jsonに "test": "jest" と設定してあるので、yarn test で実行します。
以下のように出力されるはず。
PASS  test/firestore/vegetable.spec.ts (5.831 s)
  Firestore Vegetable rules
    認証済み
      ✓ 取得:全てできること (554 ms)
      ✓ 一覧:全てできること (101 ms)
      ✓ 登録:全てできること (95 ms)
      ✓ 更新:全てできること (105 ms)
      ✓ 削除:全てできること (107 ms)
    未認証
      ✓ 取得:全てできること (152 ms)
      ✓ 一覧:全てできること (100 ms)
      ✓ 登録:全てできないこと (84 ms)
      ✓ 更新:全てできないこと (142 ms)
      ✓ 削除:全てできないここと (87 ms)
Test Suites: 1 passed, 1 total
Tests:       10 passed, 10 total
Snapshots:   0 total
Time:        5.911 s, estimated 6 sカバレッジは coverage/firestore_rules/xxx.html のように出力されます。
