firebase/rules-unit-testingで初期データを登録したいけどPERMISSION_DENIEDが出る

3 min read読了の目安(約3000字

下記のような権限エラーが出た

FirebaseError: 7 PERMISSION_DENIED: Null value error. for 'create' @ L128

結論

  1. エミュレータは最初に firebase.json ファイルの firestore.rules フィールドで指定されたルールを読み込みます。
  2. 上記より、エミュレーター起動時に先にセキュリティールールが適用されているので、isAdmin等のユーザーロールを見てwriteを許可しているもの等は権限エラーになります。
  3. firebase.initializeAdminAppで同じprojectIdのAppのFirestoreのデータを書き込めば、セキュリティールールやAuthの設定を無視して書き込む事ができます。

セキュリティールール(抜粋)

match /users/{userId} {
  allow read: if isAdmin() || isMyUser(userId);
  allow write: if isAdmin() || isMyUser(userId);
}

firebaseTestingの設定用関数を作る

firebaseTesting.ts
import * as firebase from '@firebase/rules-unit-testing'
import fs from "fs"
import path from "path"

const filePath = path.join(__dirname, './firestore.rules')
const rules = fs.readFileSync(filePath, "utf8")

const projectId = `rules-test-${Date.now()}`;
type Auth = { uid: string; email?: string }
type DataValue = { [key: string ]: any }
type Data = { [ke: string]: DataValue }

const addMockData = async (data: Data) => {
  const adminApp = firebase.initializeAdminApp({ projectId })
  const adminDB = adminApp.firestore()
  for (const key in data) {
    const ref = adminDB.doc(key);
    await ref.set(data[key]);
  }
}

// データベース設定
export const setup = async (auth?: Auth, data?: Data) => {
  const app = await firebase.initializeTestApp({
    projectId,
    auth
  })

  // モックデータが有ればAdminAppを使って流し込む
  data && await addMockData(data)

  // セキュリティールールの適用
  await firebase.loadFirestoreRules({
    projectId,
    rules
  })

  const db = app.firestore();
  return db;
}

// アプリの全削除
export const deleteAppAll = async () => {
  await Promise.all(firebase.apps().map(app => app.delete()));
}

// データベースの初期化
export const clearDB = async () => {
  await firebase.clearFirestoreData({ projectId });
}

使い方

firestoreRules.test.ts
import * as firebase from '@firebase/rules-unit-testing'
import { setup, deleteAppAll, clearDB } from './firebaseTesting'

const currentUserUid = 'current-user-uid';
const receiveUserUid = 'receive-user-uid';

const USER = 'users';

process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'

describe('Firestore Security Rules', () => {
  // テスト毎にデータの初期化
  afterEach(async () => await clearDB())
  
  // 全テストが完了したらappを全て削除
  afterAll(async () => await deleteAppAll())
  
  describe('ユーザー', () => 
    test('読取り', async () => {
      const currentUserPath = `${USER}/${currentUserUid}`;
      const receiveUserPath = `${USER}/${receiveUserUid}`;
      
      const auth = { uid: currentUserUid }
      const data = {
        [currentUserPath]: { name: 'ログインユーザーさん', role: 'admin' },
        [receiveUserPath]: { name: '読み取られるユーザーさん', role: 'nomal' },
      }
      
      const db = await setup(auth, data);
      
      const getFnc = db.doc(receiveCmsUserPath).get();
      await firebase.assertSucceeds(getFnc);
    })
  })
})

参考

単体テストを作成する by firebase公式ドキュメント
エミュレータを使用したFirestoreセキュリティルールのテスト