測試 Cloud Firestore 安全性規則

建構應用程式時,您可能會想要鎖定 Cloud Firestore 資料庫的存取權。不過,在您推出應用程式前,您會需要更精細的 Cloud Firestore 安全性規則。使用 Cloud Firestore 模擬器時,您不僅可以設計原型並測試應用程式的一般功能與行為,也可以撰寫單元測試來檢查 Cloud Firestore 安全性規則的行為。

快速入門導覽課程

如需使用簡單規則的幾個基本測試案例,請參考快速入門導覽課程範例

瞭解 Cloud Firestore 安全性規則

使用行動和網路用戶端程式庫時,請實作 Firebase 驗證Cloud Firestore 安全性規則,以利進行無伺服器驗證、授權和資料驗證。

Cloud Firestore 安全性規則包含兩項要素:

  1. match 陳述式,用於識別資料庫中的文件。
  2. 控制這些文件存取權的 allow 運算式。

Firebase 驗證會驗證使用者的憑證,並提供以使用者和角色為基礎的存取系統基礎。

從 Cloud Firestore 行動/網路用戶端程式庫發出的每個資料庫要求,在讀取或寫入任何資料前,系統都會根據您的安全性規則進行評估。如果規則拒絕存取任何指定的文件路徑,整個要求都會失敗。

如要進一步瞭解 Cloud Firestore 安全性規則,請參閱「開始使用 Cloud Firestore 安全性規則」。

安裝模擬器

如要安裝 Cloud Firestore 模擬器,請使用 Firebase CLI 並執行下列指令:

firebase setup:emulators:firestore

執行模擬器

請先在工作目錄中初始化 Firebase 專案。這是使用 Firebase CLI 的常見第一步。

firebase init

使用下列指令啟動模擬器。模擬器會一直執行,直到您終止程序為止:

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 欄位中指定的規則。該指令會預期包含 Cloud Firestore 安全性規則的本機檔案名稱,並將這些規則套用至所有專案。如果您未提供本機檔案路徑或使用以下所述的 loadFirestoreRules 方法,模擬器會將所有專案視為具有開啟規則。
  • 雖然大部分 Firebase SDK 可直接與模擬器搭配使用,但只有 @firebase/rules-unit-testing 程式庫支援模擬安全性規則中的 auth,因此單元測試更加容易。此外,這個程式庫也支援一些模擬器特定功能,例如清除所有資料,如下所示。
  • 模擬器也會接受透過用戶端 SDK 提供的實際工作環境 Firebase 驗證權杖,並據此評估規則,以便在整合和手動測試中將應用程式直接連線至模擬器。

執行本機單元測試

使用第 9 版 JavaScript SDK 執行本機單元測試

Firebase 發布安全性規則單元測試程式庫,以及版本 9 JavaScript SDK 和版本 8 SDK。不同的程式庫 API 有顯著差異建議您使用 v9 測試程式庫,這個程式庫更為簡化,且連線模擬器所需的設定也較少,因此能安全地避免誤用實際工作環境資源。為了顧及回溯相容性,我們會繼續提供 v8 測試程式庫

使用 @firebase/rules-unit-testing 模組與在本機執行的模擬器互動。如果發生逾時或 ECONNREFUSED 錯誤,請再次確認模擬器是否正常運作。

強烈建議您使用新版 Node.js,以便使用 async/await 標記法。您可能會想測試的所有行為都涉及非同步函式,而測試模組的設計宗旨是處理以 Promise 為基礎的程式碼。

v9 Rules 單元測試程式庫會持續知道模擬器,絕不會更動您的正式環境資源。

您可使用 v9 模組匯入陳述式匯入程式庫。例如:

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment
} from "@firebase/rules-unit-testing"

// Use `const { … } = require("@firebase/rules-unit-testing")` if imports are not supported
// Or we suggest `const testing = require("@firebase/rules-unit-testing")` if necessary.

匯入後,實作單元測試涉及:

  • 建立及設定 RulesTestEnvironment 呼叫 initializeTestEnvironment
  • 使用可讓您暫時略過規則 RulesTestEnvironment.withSecurityRulesDisabled 的測試資料,在不觸發規則的情況下設定測試資料。
  • 在掛鉤前後設定測試套件,並透過呼叫清除測試資料和環境,例如 RulesTestEnvironment.cleanup()RulesTestEnvironment.clearFirestore()
  • 使用 RulesTestEnvironment.authenticatedContextRulesTestEnvironment.unauthenticatedContext 實作模擬驗證狀態的測試案例。

常用方法與公用程式函式

另請參閱 v9 SDK 中模擬器專屬的測試方法

initializeTestEnvironment() => RulesTestEnvironment

這個函式會初始化規則單元測試的測試環境。請先呼叫這個函式進行測試設定。要成功執行,必須執行模擬器。

此函式接受選用定義 TestEnvironmentConfig 的選用物件,該物件可包含專案 ID 和模擬器設定。

let testEnv = await initializeTestEnvironment({
  projectId: "demo-project-1234",
  firestore: {
    rules: fs.readFileSync("firestore.rules", "utf8"),
  },
});

RulesTestEnvironment.authenticatedContext({ user_id: string, tokenOptions?: TokenOptions }) => RulesTestContext

這個方法會建立 RulesTestContext,其行為類似於已驗證的驗證使用者。透過傳回結構定義建立的要求會附加模擬驗證權杖。您也可以選擇傳遞定義自訂憑證附加資訊或覆寫驗證權杖酬載的物件。

在測試中使用傳回的測試內容物件,存取已設定的所有模擬器執行個體,包括透過 initializeTestEnvironment 設定的模擬器執行個體。

// Assuming a Firestore app and the Firestore emulator for this example
import { setDoc } from "firebase/firestore";

const alice = testEnv.authenticatedContext("alice", { … });
// Use the Firestore instance associated with this context
await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

RulesTestEnvironment.unauthenticatedContext() => RulesTestContext

這個方法會建立 RulesTestContext,運作方式類似於未透過驗證登入的用戶端。透過傳回結構定義建立的要求不會附加 Firebase 驗證權杖。

在測試中使用傳回的測試內容物件,存取已設定的所有模擬器執行個體,包括透過 initializeTestEnvironment 設定的模擬器執行個體。

// Assuming a Cloud Storage app and the Storage emulator for this example
import { getStorage, ref, deleteObject } from "firebase/storage";

const alice = testEnv.unauthenticatedContext();

// Use the Cloud Storage instance associated with this context
const desertRef = ref(alice.storage(), 'images/desert.jpg');
await assertSucceeds(deleteObject(desertRef));

RulesTestEnvironment.withSecurityRulesDisabled()

使用情境在安全性規則已停用時執行測試設定函式。

這個方法採用回呼函式,該函式會接收安全性規則略過內容,並傳回承諾。一旦承諾解決 / 拒絕,系統就會刪除結構定義。

RulesTestEnvironment.cleanup()

這個方法會刪除在測試環境中建立的所有 RulesTestContexts,並清除基礎資源,以允許簡潔結束作業。

這個方法不會以任何方式變更模擬器的狀態。如要重設測試之間的資料,請使用應用程式模擬器專屬的清除資料方法。

assertSucceeds(pr: Promise<any>)) => Promise<any>

這是測試案例公用程式。

此函式宣告已包裝的 Promise 包裝模擬器作業,而不會違反安全性規則違規情況。

await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

assertFails(pr: Promise<any>)) => Promise<any>

這是測試案例公用程式。

該函式宣告提供的 Promise 包裝模擬器作業,將因安全性規則違規而遭到拒絕。

await assertFails(setDoc(alice.firestore(), '/users/bob'), { ... });

模擬器專用方法

另請參閱「第 9 版 SDK 中的常見測試方法和公用程式函式」。

RulesTestEnvironment.clearFirestore() => Promise<void>

這個方法會清除 Firestore 資料庫中的資料,但這些資料屬於為 Firestore 模擬器設定的 projectId

RulesTestContext.firestore(settings?: Firestore.FirestoreSettings) => Firestore;

這個方法會取得此測試結構定義的 Firestore 執行個體。傳回的 Firebase JS 用戶端 SDK 執行個體可與用戶端 SDK API (v9 模組化或 v9 相容性) 搭配使用。

以圖表呈現規則評估

Cloud Firestore 模擬器可讓您在模擬器套件 UI 中,以視覺化方式呈現用戶端要求,包括 Firebase 安全性規則的評估追蹤記錄。

開啟「Firestore」>「Request」分頁,查看每項要求的詳細評估序列。

顯示安全性規則評估作業的 Firestore 模擬器要求監控項目

產生測試報告

執行一系列的測試後,您可以存取測試涵蓋範圍報表,瞭解每個安全性規則的評估方式。

如要取得報告,請在模擬器執行時查詢已公開的端點。如果是適用於瀏覽器的版本,請使用下列網址:

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

這會將規則分為不同的運算式和子運算式,您可以將滑鼠遊標移到運算式和子運算式上,以便查看更多資訊,包括傳回的評估次數和值。針對這項資料的原始 JSON 版本,請在查詢中加入以下網址:

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

模擬器和正式版的差異

  1. 您不必明確建立 Cloud Firestore 專案。而模擬器會自動建立任何存取的執行個體。
  2. Cloud Firestore 模擬器不適用於一般 Firebase 驗證流程。相反地,在 Firebase Test SDK 中,我們已在 rules-unit-testing 程式庫中提供 initializeTestApp() 方法,此方法會採用 auth 欄位。使用這個方法建立的 Firebase 處理程序會如同您提供的各項實體,順利完成驗證。如果您傳入 null,系統會將其行為視為未經驗證的使用者 (例如 auth != null 規則會失敗)。

排解已知問題

使用 Cloud Firestore 模擬器時,您可能會遇到下列已知問題。請按照下方指引,排解目前發生的任何異常行為問題。這些附註是以安全性規則單元測試程式庫所撰寫,但一般方法適用於所有 Firebase SDK。

測試行為不一致

如果測試偶爾會通過且失敗,即使測試本身未進行任何變更,您可能需要確認測試是否已正確排序。模擬器與模擬器的互動多半都非同步,因此請再次確認所有非同步程式碼的順序是否正確。如要修正序列,您可以鏈結承諾項目或使用 await 標記法。

請特別留意下列非同步作業:

  • 設定安全性規則,例如 initializeTestEnvironment
  • 讀取及寫入資料,例如 db.collection("users").doc("alice").get()
  • 操作斷言,包括 assertSucceedsassertFails

只有在首次載入模擬器時,測試才會通過

模擬器處於有狀態狀態。它會將寫入的所有資料儲存在記憶體中,因此每當模擬器關閉,所有資料都會遺失。如果您針對相同的專案 ID 進行多次測試,每項測試產生的資料可能會影響後續測試。您可以使用下列任一方法略過這個行為:

  • 請為每個測試使用不重複的專案 ID。請注意,如果選擇此選項,您必須在每次測試中呼叫 initializeTestEnvironment。只有預設專案 ID 會自動載入規則。
  • 重新建構測試,使其與先前寫入的資料互動 (例如,針對每個測試使用不同的集合)。
  • 刪除在測試期間寫入的所有資料。

測試設定非常複雜

設定測試時,您可能會想要以 Cloud Firestore 安全性規則實際上禁止的方式修改資料。如果您的規則使測試設定較為複雜,請嘗試在設定步驟中使用 RulesTestEnvironment.withSecurityRulesDisabled,因此讀取和寫入不會觸發 PERMISSION_DENIED 錯誤。

之後,您的測試可分別使用 RulesTestEnvironment.authenticatedContextunauthenticatedContext,以已驗證或未驗證的使用者的身分執行作業。這可讓您驗證 Cloud Firestore 安全性規則是否正確允許 / 拒絕不同的情況。