81
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

1つめのFirestoreセキュリティルールテスト

Posted at

本記事はFirebase公式ドキュメントを噛み砕いた内容になってます。
公式ドキュメントにはテストツールについて触れられておらず個人的に進めずらかったので、環境構築の部分とテストツールとしてJestを利用する方法を織り交ぜてまとめました。

エミュレータの嬉しいところ

セキュリティルールのテストはFirestoreのローカルエミュレータを使用すると開発効率がUPします。

Firestoreのセキュリティルールが正しく記述できているかどうかをクライアント側でいちいち確認しようとすると、エラーが発生したときにPermission Deniedとしか出力されません。これでは原因調査がなかなか大変なのですが、ローカルエミュレータを使用するともう少し詳細な原因を提示してくれるのでルールの記述が捗って嬉しいです☺️

前準備として

FirebaseCLIでプロジェクトの設定は終わっている状態です。
firebase loginfirebase init firestoreでfirestore.rulesが作成されているとします。

サンプル リポジトリ

説明不足なところはサンプルを参考にしてください🙏
https://github.com/trueSuperior/Firestore-rules-test

Setup

package.json

{
  "devDependencies": {
    "@firebase/testing": "0.9.4",
    "@types/jest": "23.3.9",
    "jest": "23.6.0",
    "filesystem": "1.0.1",
    "source-map-support": "0.5.12",
    "ts-jest": "22.4.6",
    "ts-node": "8.1.0",
    "typescript": "3.4.5"
  }
}

@firebase/testingがfirestoreのエミュレータを操作するためのモジュールになります。そのほかTypeScriptJest等を入れます。
依存モジュールたちをインストールします。

npm i

別途tsconfig.jsonの作成とpackage.jsonにJestの設定が必要になります。

Firestoreエミュレータ

ローカルエミュレータのインストール

firebase setup:emulators:firestore

セキュリティルール

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read;
      allow create: if request.auth.uid == userId && request.resource.data.createdAt == request.time;
    }
    match /rooms/{roomId} {
      allow read;
      // If you create a room, you must set yourself as the owner.
      allow create: if request.resource.data.owner == request.auth.uid;
      // Only the room owner is allowed to modify it.
      allow update: if resource.data.owner == request.auth.uid;
    }
  }
}

サンプルとして公式のTypeScriptクイックスタートリポジトリのルールを使用します。

  • users, roomsコレクションは誰でも読み取り可能
  • ユーザー自身のuidと登録するusersのドキュメントIDが同一の場合のみusersのドキュメントが作成可能
  • 登録するroomsドキュメントのownerフィールドの値がユーザー自身のuidと同一の場合のみドキュメントを作成可能
  • 作成済みroomsドキュメントのownerフィールドの値が自身のuidと同一の場合のみデータの更新が可能

余談なのですが。上記のルールではroomsのownerフィールドは変更後の制限をしていないため、自身のデータであれば好きに書き換えることができます。場合によってはチートに利用されてしまうケースがあるかと思うので注意が必要です。

@firebase/testingモジュールについて

import * as firebase from '@firebase/testing'

モジュールで利用する機能は主に4つあります。

  • Firebaseアプリの作成
  • ルールファイルの読み込み
  • 読み書きの成功失敗をアサート
  • クリーンアップ

Firebaseアプリの作成

  • 特定ユーザーとして認証されたアプリ
const app = firebase.initializeTestApp({
   projectId: "my-test-project",
   auth: { uid: "alice", email: "alice@example.com" }
 })

authnullを設定すると認証していないユーザーで初期化されます。

  • 管理者として認証されたアプリ
const app = firebase.initializeAdminApp({ projectId: "my-test-project" })

ルールファイルの読み込み

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

読み書きの成功失敗をアサート

  • 失敗
firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get())
  • 成功
firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get())

クリーンアップ

  • プロジェクトに関連付けられているデータをクリア
firebase.clearFirestoreData({
 projectId: "my-test-project"
})
  • アプリの全消去
Promise.all(firebase.apps().map(app => app.delete()))

テストを書く

Jest記法について


import * as firebase from '@firebase/testing'
import * as fs from 'fs'

const testName = 'firestore-local-emulator-test'
const rulesFilePath = 'firestore.rules'

describe(testName, () => {
    // はじめに1度ルールを読み込ませる
    beforeAll(async () => {
        await firebase.loadFirestoreRules({
            projectId: testName,
            rules: fs.readFileSync(rulesFilePath, 'utf8')
        })
    })

    // test毎にデータをクリアする
    afterEach(async () => {
        await firebase.clearFirestoreData({ projectId: testName })
    })

    // 全テスト終了後に作成したアプリを全消去
    afterAll(async () => {
        await Promise.all(firebase.apps().map(app => app.delete()))
    })

    // describe('users collection tests', () => { ...
}

テスト構文はJestの記法になります。読むとなんとなくわかってもらえるかと思いますが簡単に説明すると以下になります。

  • describeでブロックの作成
  • before~, after~で事前準備や後始末などを行う
  • testでテストの実行

describeでブロックを作成するとテスト結果をみたときに文脈がわかりやすいのがいい感じです。個人的には'create', 'update'などでブロックをつくるのがおすすめです。

describeやtestの後に.onlyと繋げるとそのブロック以外のテストをスキップしてくれるので便利です。
describe.only('users collection tests', () => { ...
Jestの記法は他にもあるので興味があれば調べてみるとよいかと思います。

1つめのテストを書く


// users
describe('users collection tests', () => {
    describe('read', () => {
        test('should let anyone read any profile', async () => {
            const db = authedApp(null)
            const user = db.collection('users').doc('alice')
            await firebase.assertSucceeds(user.get())
        })
    })
})

function authedApp(auth: object): firebase.firestore.Firestore {
  return firebase
    .initializeTestApp({ projectId: testName, auth: auth })
    .firestore()
}

ようやく1つめのテストを作成することができましたのでさっそく実行します。

エミュレータの起動


firebase serve --only firestore

テストの実行


npm test

すると、、、
スクリーンショット 2019-05-27 20.29.17.png

おめでとうございます!🎉
まず1つめのテストを記述することができました。

サンプルリポジトリではその他のテストも記述しているのでよければ参考にしてみてください。

おわりに

npm test -- --watchでファイルの変更を監視してテストを走らせることができる機能もご活用ください。

参考記事

81
71
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
81
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?