コンソールへ移動

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

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

クイックスタート

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

Cloud Firestore セキュリティ ルールを理解する

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

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

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

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

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

Cloud Firestore セキュリティ ルールの詳細については、Cloud Firestore セキュリティ ルールの利用開始方法をご覧ください。

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

Cloud Firestore エミュレータをインストールするには、Firebase CLI を使用し、次の手順に沿って操作します。

  1. Cloud Firestore エミュレータをインストールします。

     firebase setup:emulators:firestore
     

  2. 次のコマンドを使用してエミュレータを起動します。すべてのテストでエミュレータが実行されます。

     firebase serve --only firestore
     

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

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

  • 現在エミュレータをサポートしている唯一の SDK は Node.js SDK です。エミュレータとのやりとりを容易にするために、@firebase/testing モジュールを用意しました。
  • エミュレータはオプションの --rules CLI フラグをサポートしています。Cloud Firestore セキュリティ ルールが含まれているローカル ファイルの名前をこのフラグに指定すると、そのセキュリティ ルールがすべてのプロジェクトに適用されます。次で説明するとおり、ローカル ファイル パスを指定しなかったり、loadFirestoreRules メソッドを使用しなかったりすると、エミュレータはすべてのプロジェクトをオープン ルールとして扱います。

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

@firebase/testing モジュールを使用して、ローカルで動作するエミュレータを操作します。タイムアウトまたは ECONNREFUSED エラーが発生する場合は、エミュレータが実行されているか確認してください。

async/await の表記法を使用できるようにするため、新しいバージョンの Node.js を使用することを強くおすすめします。テスト対象となる可能性のある動作のほとんどには非同期関数があります。また、テスト モジュールは Promise ベースのコードで動作するように設計されています。

公開されているモジュールのメソッドは以下のとおりです。

  • initializeTestApp({ projectId: <name>, auth: <auth> }) => FirebaseApp

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

     firebase.initializeTestApp({
       projectId: "my-test-project",
       auth: { uid: "alice", email: "alice@example.com" }
     });
    
  • initializeAdminApp({ projectId: <name> }) => FirebaseApp

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

    firebase.initializeAdminApp({ projectId: "my-test-project" });
    
  • apps() => [FirebaseApp] このメソッドは、現在初期化されているテストと管理アプリをすべて返します。これを使用して、テスト間またはテスト後にアプリを消去します。

     Promise.all(firebase.apps().map(app => app.delete()))
    
  • loadFirestoreRules({ projectId: <name>, rules: <rules> }) => 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: <name> }) => 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. Cloud Firestore プロジェクトを明示的に作成する必要はありません。エミュレータは、アクセスされるインスタンスを自動的に作成します。
  2. Cloud Firestore エミュレータは、他の Firebase プロダクトと動的なやり取りを行いません。特に、通常の Firebase Authentication フローは機能しません。その代わりに、テスト モジュールの中に auth フィールドを受け取る initializeTestApp() メソッドを用意しました。このメソッドを使用して作成された Firebase ハンドルは、どのようなエンティティを指定しても正常に認証されたかのように動作します。null を渡すと、認証されていないユーザーとして動作します(たとえば auth != null ルールが失敗します)。
  3. Cloud Firestore エミュレータはデータを保持し、このデータ保持により、結果に影響することがあります。独立してテストを実行するには、独立したテストごとに異なるプロジェクト ID を割り当てます。firebase.initializeAdminAppfirebase.initializeTestApp を呼び出すときに、固有の ID、タイムスタンプ、またはランダムな整数を projectID に付加します。さらに、JavaScript の async または await キーワードを通じて Promise が解決するのを待つことで、競合状態を解決することもできます。

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

Cloud Firestore エミュレータを使用する際には、次のような既知の問題が発生する可能性があります。発生する異常な動作をトラブルシューティングするには、以下のガイダンスに従ってください。

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

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

特に、以下の非同期オペレーションを再確認します。 - firebase.loadFirestoreRules などを使用したセキュリティ ルールの設定。 - db.collection("users").doc("alice").get() などを使用したデータの読み書き。 - firebase.assertSucceedsfirebase.assertFails を含む動作可能なアサーション。

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

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

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

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

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

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