セキュリティ ルールのテスト

アプリを構築する過程で Firestore データベースへのアクセスを制限する場合もありますが、リリースする前には、より詳細な Firestore セキュリティ ルールが必要になります。Firestore エミュレータを使用すると、Firestore セキュリティ ルールの動作をチェックする単体テストを作成できます。

クイックスタート

簡単なルールを使った基本的なテストケースについては、JavaScript クイックスタートまたは TypeScript クイックスタートをお試しください。

Firestore セキュリティ ルールについて

モバイル クライアント ライブラリやウェブ クライアント ライブラリを使用する場合は、サーバーレス認証、承認、データ検証を行うために Firebase AuthenticationFirestore セキュリティ ルールを実装します。

Firestore セキュリティ ルールには、次の 2 つが含まれています。

  1. データベース内のドキュメントを識別する match ステートメント
  2. それらのドキュメントへのアクセスを制御する allow

Firebase Authentication はユーザーの認証情報を検証し、ユーザーベースとロールベースのアクセス システムの基盤を提供します。

Firestore のモバイル クライアント ライブラリやウェブ クライアント ライブラリから送られたすべてのデータベース リクエストは、データの読み取りまたは書き込みの前に、セキュリティ ルールと照合して評価されます。指定されたドキュメント パスのいずれかへのアクセスがルールによって拒否されると、リクエスト全体が失敗します。

Firestore セキュリティ ルールの詳細については、Firestore セキュリティ ルールを使ってみるをご覧ください。

エミュレータをインストールする

Firestore エミュレータをインストールするには、Firebase CLI を使用して以下のコマンドを実行します。

firebase setup:emulators:firestore

エミュレータを実行する

次のコマンドを使用してエミュレータを起動します。エミュレータは、プロセスを終了するまで実行されます。

firebase emulators:start --only firestore

多くの場合、エミュレータを起動し、テストスイートを実行して、テストの実行後にエミュレータをシャットダウンします。これは emulators:exec コマンドで簡単に実施できます。

firebase emulators:exec --only firestore "./my-test-script.sh"

エミュレータが起動すると、デフォルト ポート(8080)での実行が試行されます。デフォルト ポート以外のエミュレータ ポートで実行するには、firebase.json ファイルの "emulators" セクションを変更します。

{
  // ...
  "emulators": {
    "firestore": {
      "port": "YOUR_PORT"
    }
  }
}

エミュレータを実行する前に

エミュレータを使用する前に、次の点に注意してください。

  • エミュレータは最初に firebase.json ファイルの firestore.rules フィールドで指定されたルールを読み込みます。Firestore セキュリティ ルールが含まれているローカル ファイルの名前をこのフラグに指定すると、そのセキュリティ ルールがすべてのプロジェクトに適用されます。次で説明するとおり、ローカル ファイルパスを指定しない場合、あるいは loadFirestoreRules メソッドを使用しない場合、エミュレータはすべてのプロジェクトをオープンルールが適用されるものとして扱います。
  • 多くの SDK はエミュレータで動作しますが、セキュリティ ルールで auth の擬似的再現をサポートしているのは @firebase/testing Node.js モジュールのみです。したがって、このモジュールでは単体テストがはるかに簡単になります。さらにこのモジュールは、以下にリストされているエミュレータ固有の機能(すべてのデータのクリアなど)もサポートしています。
  • エミュレータは、クライアント SDK から提供される本番環境の Firebase Auth トークンも受け入れ、それに応じてルールを評価します。そのため、統合テストと手動テストでアプリケーションをエミュレータに直接接続できます。

ローカルテストを実行する

initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp

このメソッドは、オプションで指定されたプロジェクト ID と auth 変数に対応する、初期化された Firebase アプリを返します。テストで使用するために、特定のユーザーとして認証されたアプリを作成するには、このメソッドを次のように使用します。

firebase.initializeTestApp({
  projectId: "my-test-project",
  auth: { uid: "alice", email: "alice@example.com" }
});

initializeAdminApp({ projectId: string }) => FirebaseApp

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

firebase.initializeAdminApp({ projectId: "my-test-project" });
    

apps() => [FirebaseApp]。このメソッドは、現在初期化されているテストと管理アプリをすべて返します。テスト間またはテスト後にアプリを消去するには、このメソッドを次のように使用します。

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

loadFirestoreRules({ projectId: string, rules: Object }) => Promise

このメソッドは、ローカルで実行されているデータベースにルールを送信し、ルールを文字列として指定するオブジェクトを取ります。データベースのルールを設定するには、このメソッドを次のように使用します。

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

assertFails(pr: Promise) => Promise

このメソッドは、入力が成功した場合は拒否され、入力が拒否された場合は成功する Promise を返します。データベースの読み取りや書き込みが失敗したかどうかをアサートするには、このメソッドを次のように使用します。

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

assertSucceeds(pr: Promise) => Promise

このメソッドは、入力が成功した場合は成功し、入力が拒否された場合は拒否される Promise を返します。データベースの読み取りや書き込みが成功したかどうかをアサートするには、このメソッドを次のように使用します。

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

clearFirestoreData({ projectId: string }) => Promise

このメソッドは、ローカルで実行中の Firestore インスタンス内の特定のプロジェクトに関連付けられているすべてのデータを消去します。このメソッドを使用して、テスト後にクリーンアップします。

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

テストレポートを生成する

一連のテストを実行した後、それぞれのセキュリティ ルールの評価を示したテスト カバレッジ レポートにアクセスできます。

レポートを取得するには、エミュレータの実行中に公開されたエンドポイントに対してクエリを実行します。ブラウザでの表示に適したバージョンを参照するには、次の URL を使用します。

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html

これによりルールが式やサブ式に分割されます。それぞれの式の上にマウスカーソルを重ねて、評価回数や返された値などの詳細情報を確認できます。このデータの未加工の JSON バージョンを取得するには、クエリに次の URL を含めます。

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage

エミュレータと本番環境の違い

  1. Firestore プロジェクトを明示的に作成する必要はありません。アクセスされるインスタンスは、エミュレータが自動的に作成します。
  2. Firestore エミュレータは、通常の Firebase Authentication フローで動作しません。そこで、Firebase Test SDK の中には auth フィールドを受け取る initializeTestApp() メソッドが用意されています。このメソッドを使用して作成された Firebase ハンドルは、どのようなエンティティを指定しても正常に認証されたかのように動作します。null を渡すと、認証されていないユーザーとして動作します(たとえば auth != null ルールは失敗します)。

既知の問題のトラブルシューティング

Firestore エミュレータを使用する際には、次のような既知の問題が発生する可能性があります。発生する異常な動作をトラブルシューティングするには、以下のガイダンスに従ってください。これらの注は Firebase Test SDK を想定して記述されていますが、一般的なアプローチはすべての Firebase SDK に適用されます。

テスト動作に一貫性がない

テスト自体に変更を加えていないのに、テストに合格したりしなかったりする場合は、テストの順序が正しいことを確認する必要があります。エミュレータとのやり取りのほとんどは非同期であるため、すべての非同期コードの順序が正しいことを再確認してください。順序を修正するには、Promise をチェーン化するか、await 表記を多数使用できます。

特に、以下の非同期オペレーションを確認してください。

  • firebase.loadFirestoreRules などを使用したセキュリティ ルールの設定。
  • db.collection("users").doc("alice").get() などを使用したデータの読み書き。
  • firebase.assertSucceedsfirebase.assertFails を含む動作可能なアサーション。

エミュレータを初めて読み込むときにのみテストに合格する

エミュレータはステートフルです。書き込まれたすべてのデータをメモリに保存するので、エミュレータがシャットダウンするたびにデータは失われます。同じプロジェクト ID に対して複数のテストを実行している場合は、各テストで後続のテストに影響するデータが生成される可能性があります。次のいずれかの方法を使用すれば、この動作を回避できます。

  • テストごとに一意のプロジェクト ID を使用する。
  • 過去に書き込まれたデータを扱わないようにテストを再構成する(テストごとに異なるコレクションを使用するなど)。
  • テスト中に書き込まれたすべてのデータを削除する。

テストのセットアップが非常に複雑である

Firestore セキュリティ ルールで実際に許可されないシナリオをテストする必要が生じることもあります。たとえば、認証されないユーザーとしてデータを編集することはできないので、認証されないユーザーがデータを編集できるかどうかテストするのは困難です。

ルールが原因でテストのセットアップが複雑になる場合は、管理者が承認したクライアントを使ってルールをバイパスしてみてください。firebase.initializeAdminApp を使うとこれを試すことができます。管理者が承認したクライアントからの読み書きはルールをバイパスするので、PERMISSION_DENIED エラーが発生しません。