
テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】
2021.12.20
テスト駆動で商品データのルールを実装する
この記事では、セキュリティルールによるデータ検証について、商品データのルールを例に解説していきます。
前編・後編の二回に分け、テスト駆動で商品データのルールの実装を進めます。
条件設定で使用できるデータなどについても解説していたいと思います!
簡単な流れ
以下のような流れで解説を進めていきます。
前編
- 商品データコレクションの要件を設定
- テスト環境の準備
- テストデータ用のクラスを作成
- データ追加ルールを実装・解説
後編
- データ更新ルールを実装・解説
- データ取得ルールを実装・解説
- 全体テストで要件の最終チェック
- まとめ
今回は、前編パートの、「実装目標(要件)の設定からデータ追加ルールの実装まで」を解説していきます。
実装目標
商品データコレクション items を想定して、以下の要件を目標に実装を進めていきます。
商品データ追加の要件
- データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可
商品データ更新の要件
- データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可
- 商品管理者の変更不可
- ロックされたデータの更新不可
- 商品名の変更不可
商品データ取得の要件
- 認証されていないユーザはデータ取得不可
- 売り切れ商品のデータ取得不可
前準備 ~ テスト環境の準備 ~
初めに、テスト環境を準備します。
テスト環境のセットアップ
以下のコマンドを実行して、テスト環境をセットアップします。
1 2 3 4 | git clone "https://github.com/rightcode/firestore-security-rules-test_data-verification.git" sandbox cd sandbox git checkout refs/tags/test-environment-only |
上記のリポジトリに、この記事のコードがまとめてあります。
npm パッケージのインストール
以下のコマンドを実行して、npm パッケージをインストールしてください。
1 | npm install |
Firebase CLI ツールのインストール
まだ、Firebase CLI ツールをインストールしていない場合は、以下のコマンドを実行してインストールします。
1 | npm install -g firebase-tools |
Firestore エミュレータを起動
以下のコマンドを実行して、Firestore エミュレータを起動してください。
1 2 | firebase setup:emulators:firestore firebase emulators:start --only firestore |
以上で、テスト環境の準備は完了です。
コードを確認したい場合は、以下のコマンドより、コーディング済みのタグを指定してチェックアウトしてください。
1 2 3 4 5 | // テスト実装済み git checkout refs/tags/test-only // テストとルール実装済み git checkout refs/tags/test-and-rules |
テスト環境のセットアップの解説
テスト環境のセットアップについて、より詳細な内容は、以下の記事をご参照ください。
前準備 ~ テストデータクラス ~
テストを作成する前に、まず、テストデータ用のクラスを作成します。
tests/TestData.ts ファイルを作成して、以下の内容を記述してください。
テストデータクラスのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class TestData { // コレクションパスの取得 static getCollectionPath() { return "items" } // 商品IDの取得 static getItemId() { return "XXXXXXXXXX" } // 商品タイトルの取得 static getTitle() { return "吾輩は犬である!" } // 商品管理者の取得 static getAdminUser() { return "fuyutsuki"; } // 初期データの取得 static getInitialData() { return { item_id: this.getItemId(), title: this.getTitle(), admin_user: this.getAdminUser(), price: 1000, description: "猫じゃないよ。犬だよ。", locked: false, sold_out: false } } // 要件に沿った更新データの取得 static getValidUpdateData() { return { item_id: this.getItemId(), title: this.getTitle(), admin_user: this.getAdminUser(), price: 12000, description: "猫が好きです。でも犬はも~っと好きです。", locked: false, sold_out: false }; } } export default TestData; |
テストデータクラスの概要
getInitialData()
初期データの取得メソッド。
初期データの追加テストや、データの更新・取得テストの初期データの準備に使用します。
getValidUpdateData()
要件にあった更新データの取得メソッド。
データ更新テストに使用します。
商品データ追加ルールの実装
テスト作成の前に、データ追加の要件を再確認します。
- データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可
上記の要件に沿って、テストを作成します。
tests/data.add.test.ts ファイルを追加して、以下のようにコードを記述してください。
テストのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // Firestoreエミュレータのホストとポートを指定 process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080"; import {FirestoreTestSupporter} from "firestore-test-supporter"; import * as firebase from "@firebase/testing"; import path from "path"; // テストデータクラスの読み込み import TestData from "./TestData"; describe("データ追加テスト", () => { const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules")); // テストデータを変数に設定 const collectionPath = TestData.getCollectionPath(); const item_id = TestData.getItemId(); const initialData = TestData.getInitialData(); const admin_user = TestData.getAdminUser(); beforeEach(async () => { // セキュリティルールの読み込み await supporter.loadRules(); }); afterEach(async () => { // データのクリーンアップ await supporter.cleanup(); }); test('要件にあったデータの追加に成功', async () => { const db = supporter.getFirestoreWithAuth(admin_user); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertSucceeds(doc.set(initialData)) }); test('データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可', async () => { // 商品管理者と異なるユーザで認証 const db = supporter.getFirestoreWithAuth("other_user"); const doc = db.collection(collectionPath).doc(item_id); await firebase.assertFails(doc.set(initialData)) }); }); |
テストの概要
コードの冒頭
Firestore エミュレータのホスト指定をし、作成したテストデータクラスの読み込みを行っています。
デフォルトポートでエミュレータを起動している場合は、ホスト指定は必要ありません。
ですが、今回は 58080 ポートでエミュレータを起動しているので、ホストを 58080 ポートで指定しています。
「データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テスト
const db = supporter.getFirestoreWithAuth("other_user"); の部分で、商品管理者 admin_user とは異なるユーザ other_user で認証された Firestore クライアントを取得しています。
ルールの実装
商品データ追加用のテストが完成したので、テスト駆動でセキュリティルールの実装を進めていきます。
テスト駆動のスタイルに従い、まずはテストを実施してみて、テストが失敗することを確認します。
テストをスタート
以下のコマンドを実行し、テストをスタートしてください。
1 | npm run test-watch tests/data.add.test.ts |
テスト結果
各テストの結果に、以下のようなエラーメッセージが表示されて、テストが失敗します。
1 | ENOENT: no such file or directory, open '.../tests/firestore.rules' |
全アクセスを拒否するセキュリティルールを設定
tests/firestore.rules ファイルがないという事なので、 tests/firestore.rules ファイルを追加して、以下のように記述してください。
1 2 3 4 5 6 7 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 全てのアクセスを拒否 allow read, wreite: if false; } } |
まずは、全てのアクセスを拒否するとこらからスタートし、上から順にテストを通して行きます。
テスト結果
テストが自動で再実施され、テスト結果の冒頭に以下のような結果が表示されます。
1 2 3 4 | FAIL tests/data.add.test.ts データ追加テスト × 要件にあったデータの追加に成功 (2291ms) √ データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (53ms) |
テスト駆動開発では、失敗するテストを順次追加していくのが一般的です。
しかし、この記事では、解説のわかりやすさのため、テストをまとめて追加しています。
そのため、一部テストが通ってしまっていますが、とりあえずスルーしてください。
items コレクションへの全アクセスを許可するセキュリティルールを設定
「要件にあったデータの追加に成功」テストが失敗しているので、とりあえず items コレクションへのアクセスを全て許可してみます。
以下のようにコードを調整してください。
1 2 3 4 5 6 7 8 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow read, write: if true; } } } |
テスト結果
テスト結果は、以下の通りになります。
1 2 3 4 | FAIL tests/data.add.test.ts データ追加テスト √ 要件にあったデータの追加に成功 (2095ms) × データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (58ms) |
「要件にあったデータの追加に成功」テストが成功しました。
次に、「データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テストを通します。
ユーザ情報を許可条件の設定に使用する
以下のようにルールを調整してください。
1 2 3 4 5 6 7 8 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /items/{item_id}{ allow create: if request.auth.uid == request.resource.data.admin_user; } } } |
ここで、 request.auth フィールドにより、リクエストしたユーザの情報を、許可条件の設定に使用することができます。
uid のほか、 email や phone_number などを参照できます。
また、 request.resource.data フィールドにより、追加データの情報を条件に使用することができます。
なお、 request.auth のより詳しい内容は、以下を参照してください。
【参考】
Interface: Request | Firebase - auth
テスト結果
テスト結果は、以下の通りとなります。
1 2 3 4 | PASS tests/data.add.test.ts データ追加テスト √ 要件にあったデータの追加に成功 (2095ms) √ データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (56ms) |
テストが全て通りましたので、商品データ追加向けのセキュリティルールの実装はこれで完了です。
次のテストとの競合を避けるために、データ追加用のテストを終了してください。
後編へつづく!
以上、「実装目標の設定」から、「データ追加ルールを実装する」ところまで解説してみました。
後編では、データ更新と取得のルールを、今回と同じように、テスト駆動で実装していきます。
また、データベースにすでにあるデータの利用などについても解説していきます。
それでは、続きは後編で!
こちらの記事もオススメ!
後編の記事はこちら!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン