firestore の rule のテストは難しくないからみんな書こう(rule はむずい)

firestore の rule のテスト方法について書きたいと思います.

firestore に限らず firebase のサービスの rule はセキュリティを確保するために非常に重要ですが、独特の書き方と概念なのでちゃんと書くのは結構難しいです。 また、条件が増えてくると手作業でテストするのも現実ではありません。

そこで今回は @firebase/rules-unit-testing を使ったテスト方法について紹介したいと思います。内容的には以下の公式ドキュメントほぼそのままですが細かい導入方法などが欠けているのでそのあたりを補足したいと思います。

単体テストを作成する  |  Firebase

環境セットアップ

多分 firebase をいじっている人はすでに Firebase CLI (npm install -g firebase-tools で入るもの)が入っていると思うのでインストールの説明は省きます。

プロジェクトの作業ディレクトリで firebase init firestore して rule ファイルなどを作成します。エラーが出る場合は region の設定などが終わっていない or firestore をまだ有効化していない可能性があるのでエラーメッセージでググったりしてみてください。

firestore のリージョンは一回しか設定できないのでどこにすべきかよく検討してみてください(日本で展開するサービスなら asia-northeast1 (東京) が無難かと思います)。

テスト環境セットアップ

nodejs でテストを書きます。具体的な書き方がわからない場合はこのリポジトリが参考になります。

quickstart-testing/unit-test-security-rules at master · firebase/quickstart-testing · GitHub

以下の2つのコマンドを順番に叩いていけばセットアップ完了です

  • npm init -y
    • すでに npm init している場合は不要です. iOS や Android アプリを作っていてまだ行っていない場合はしてください.
  • npm install -D mocha @firebase/rules-unit-testing # テストに使うライブラリ
    • test runner に mocha を使っていますが好みがあれば別のものでいいと思います

package.json の scripts を以下のように編集しておきます。

{
 "scripts": {
    "test-firestore": "mocha --exit firestore.spec.js",
    "test": "mocha --exit *.spec.js"
  }
}

テストを書く

細かい書き方は先に紹介した公式ドキュメントやリポジトリを参考にしてください。ここではこれらのドキュメントの理解の助けになるように大まかな流れを説明します。

  1. rule の読み込み (firebase.loadFirestoreRules の呼び出し)
    • 1回だけやればいいので before でやっています
  2. firestore データの初期化(clearFirestoreData)
    • 各テストケースできれいな状態を作りたいので beforeEach でやっています
  3. ユニットテストを行います
  4. 最後のカバレッジレポートを出力します(after でやっています)

細かい補足

  • 初期データのセットアップなど管理者権限で行いたい操作は firebase.initializeAdminApp で初期化したインスタンスを使うと便利です
  • テスト中に認証ユーザーを変えて色々テストしたいので getAuthedFirestore のようなメソッドで毎回 initializeTestApp すると便利です
    • initializeTestApp で取得したインスタンスは特定のユーザーで認証している状態(or 全く認証していない状態)を作り出せます
  • テストは基本的に firebase.assertFailsfirebase.assertSucceeds を使って書きます.
    • 書き込みや読み込みしたときに失敗することを期待する場合は assertFails、 成功を期待する場合は assertSucceeds を使います

具体的なコードは以下のような感じになります。より詳細は quickstart-testing/unit-test-security-rules at master · firebase/quickstart-testing · GitHub を参考にしてください。

// 色々省略しています

const admin = firebase.initializeAdminApp({projectId: PROJECT_ID})

function getAuthedFirestore(auth) {
  return firebase
    .initializeTestApp({ projectId: PROJECT_ID, auth })
    .firestore();
}

beforeEach(async () => {
  await firebase.clearFirestoreData({ projectId: PROJECT_ID })
})

before(async () => {
  await firebase.loadFirestoreRules({
    projectId: PROJECT_ID,
    rules: fs.readFileSync("../firestore.rules", "utf-8")
  })
})

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

  const coverageFile = "firestore-coverage.html"
  const fstream = fs.createWriteStream(coverageFile)

  await new Promise((resolve, reject) => {
    http.get(COVERAGE_URL, (res) => {
      res.pipe(fstream, { end: true });

      res.on("end", resolve);
      res.on("error", reject);
    });
  });

  console.log(`View firestore rule coverage information at ${coverageFile}\n`);
})

describe("firestore/hogehoge_data", () => {

  it("認証していないユーザーは読めない", () => {
    const db = getAuthedFirestore(null)
    firebase.assertFails(db.collection("hogehoge_data").get())
  })

  it("認証ユーザーは読める", () => {
    const db = getAuthedFirestore({ uid: "user_name" })
    firebase.assertSucceeds(db.collection("hogehoge_data").get())
  })

  it("認証ユーザーでも書き込み不可", () => {
    const db = getAuthedFirestore({ uid: "user_name" })
    firebase.assertFails(db.collection("hogehoge_data").add({text: "hoge"}))
  })
})

describe("firestore/hugahuga_data", () => {
  it("認証していないと読み書き不可", () => {
    const db = getAuthedFirestore(null)
    firebase.assertFails(db.collection("hugahuga_data").doc("hoge").get())
    firebase.assertFails(db.collection("hugahuga_data").doc("hoge").set({data: "data"}))
  })
})

テストの実行

テストコードで FIRESTORE_EMULATOR_HOST という firebase が設定する環境変数を参照しているので以下のように firebase コマンド経由でテストを実行します.

firebase emulators:exec --only firestore "npm run test-firestore"

こうすると エミュレータの起動 -> テスト実行という感じに実行されます. ruby でよくやる bundle exec hogehoge と同じです.

テストが無事実行されるとカバレッジリポートも生成されるので見ておきましょう. このレポートはリポジトリに含めずに gitignore しちゃってもいいと思います。

デプロイ

テストが無事通ったら firebase deploy --only firestore で rule をデプロイしましょう.

まとめ

firestore の rule の書き方を説明しました。 公式のドキュメントだと説明が省かれている箇所があるので補足するような情報も載せておきました。

ぜひ rule のテストをして安心安全なアプリ開発を行ってください。

こぼれ話

ずっと前に書いた以下の記事が未だに見られていて嬉しいのですが、情報が古いので怖くもあります。

firebase realtime database でよく使う rule - Qiita