Counts the Clouds

Firebase Firestoreのrulesをテストする
2021.07.14

Firebase
Firebase Firestore
Jest

joshua-chai-weurBA3Pyts-unsplash.jpg

rulesのイメージがつかめない

テスト用を選択した場合の初期状態のルール。1ヶ月自由に読み書きできるルール。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2021, 8, 5);
    }
  }
}

例えば、認証しないと読み書きできないようにするには以下のようにするが、これで何が変わるのかいまひとつ確証がない。

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

そこで、テストしてみることにした。

エミュレータとJestをセットアップ

とりあえずプロジェクトのディレクトリにFirestoreのテスト用のディレクトリを切った。

Jestのセットアップを行う。

% yarn init -y
% yarn add -D @firebase/rules-unit-testing jest

npm scriptにテストを追加

"scripts": {
  "test": "jest"
}

こちらを参考に。

firestoreのエミュレータもセットアップ。簡単なので詳細は公式などにゆずる。

セキュリティルールをテストする

“Cloud Firestore セキュリティ ルールをテストする”という記事を見ると、アプリの初期化にfirebase.initializeTestAppfirebase.initializeAdminApp2通りの方法があって、後者はセキュリティルールは無視するとのこと。

initializeAdminApp({ projectId: string }) => FirebaseApp

このメソッドは、初期化された管理者 Firebase アプリを返します。このアプリは、読み取りと書き込みを行う際にセキュリティ ルールを迂回します。テストの状態を設定するために、管理者として認証されたアプリを作成するには、このメソッドを次のように使用します。

そこで、管理者アプリの場合、通常アプリで認証ありとなし、の3パターンのテストを書いてみた。

const fs = require('fs')
const firebase = require('@firebase/rules-unit-testing')
const PROJECT_ID = 'test-project'

beforeAll(async () => {
  const rules = fs.readFileSync('path/to/firestore.rules', 'utf8')
  await firebase.loadFirestoreRules({ projectId: PROJECT_ID, rules })
})

afterAll(async () => {
  await Promise.all(firebase.apps().map((app) => app.delete()))
})

describe('rules', () => {
  afterEach(async () => {
    await firebase.clearFirestoreData({ projectId: PROJECT_ID })
  })

  test('admin user can set document', async () => {
    const db = firebase.initializeAdminApp({ projectId: PROJECT_ID }).firestore()
    const docRef = db.collection('__trash').doc('alice')

    await firebase.assertSucceeds(
      docRef.set({
        name: 'alice',
        createAt: new Date(),
      })
    )
  })

  test('authorized user can set document', async () => {
    const db = firebase.initializeTestApp({
      projectId: PROJECT_ID,
      auth: { uid: "alice", email: "alice@example.com" },
    }).firestore()
    const docRef = db.collection('__trash').doc('alice')

    await firebase.assertSucceeds(
      docRef.set({
        name: 'alice',
        createAt: new Date(),
      })
    )
  })

  test('unauthorized user can not set document', async () => {
    const db = firebase.initializeTestApp({ projectId: PROJECT_ID }).firestore()
    const docRef = db.collection('__trash').doc('alice')

    await firebase.assertFails(
      docRef.set({
        name: 'alice',
        createAt: new Date(),
      })
    )
  })
})

initializeAdminAppの実行にはfirebase-adminが必要。

Cannot find module 'firebase-admin' from 'node_modules/@firebase/rules-unit-testing/dist/index.cjs.js'

単にインストールすればよい。

% yarn add -D firebase-admin

実行してみる。

% firebase emulators:start
% yarn test
yarn run v1.22.4
$ export FIRESTORE_EMULATOR_HOST=localhost:8080; jest
 PASS  __test__/firestore.rules.test.js
  rules
 admin user can set document (881 ms)
 authorized user can set document (344 ms)
 unauthorized user can not set document (57 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.843 s, estimated 8 s
Ran all test suites.
Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.

テストで確認ができたのでデプロイ。

% firebase deploy --only firestore:rules

Jest did not exit one second after the test run has completed.

Jest実行時に--detectOpenHandlesをつけると、connectionが切れるまで待ってくれるが、2分以上かかる。テスト自体はできているので放置する。

Warning: FIRESTORE_EMULATOR_HOST not set, using default value localhost:8080

とりあえず環境変数をつけるようにした。

"scripts": {
  "test": "export FIRESTORE_EMULATOR_HOST=localhost:8080; jest"
}

ESETのファイアウォールに止められて、Emulator UIが無言で失敗したりした。対話モードで調整する必要があった。