测试安全规则

在构建应用时,您可能需要锁定对 Firestore 数据库的访问权限。但是,在发布之前,您需要更精细的 Firestore 安全规则。借助 Firestore 模拟器,除了为应用设计原型并测试其常规功能和行为之外,您还可以编写单元测试,以便检查 Firestore 安全规则的行为。

快速入门

如需了解一些设置了简单规则的基本测试用例,请试试快速入门示例

了解 Firestore 安全规则

使用移动和 Web 客户端库时,请实现 Firebase AuthenticationFirestore 安全规则,以进行无服务器身份验证、授权和数据验证。

Firestore 安全规则包含两部分:

  1. 一个 match 语句,用于标识数据库中的文档。
  2. allow 表达式,用于控制对这些文档的访问权限。

Firebase 身份验证可验证用户凭据,并为基于用户和角色的访问系统定基础。

系统会按照您的安全规则评估 Firestore 移动/Web 客户端库的每个数据库请求,然后再读取或写入任何数据。如果规则拒绝了对任何指定文档路径的访问请求,则整个请求将会失败。

如需详细了解 Firestore 安全规则,请参阅 Firestore 安全规则使用入门

安装模拟器

如需安装 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 字段中指定的规则。它需要包含 Firestore 安全规则的本地文件的名称,并将这些规则应用于所有项目。如果您不提供本地文件路径或使用 loadFirestoreRules 方法(如下所述),模拟器会将所有项目视为采用开放规则。
  • 尽管大多数 Firebase SDK 都直接支持模拟器,但只有 @firebase/rules-unit-testing 库支持模拟安全规则中的 auth,这样更便于进行单元测试。此外,该库还支持几项特定于模拟器的功能(例如清除所有数据),具体如下所示。
  • 模拟器还将接受通过客户端 SDK 提供的生产 Firebase 身份验证令牌,并据此评估规则,从而可以在集成和手动测试中将您的应用直接连接到模拟器。

运行本地单元测试

使用 v9 JavaScript SDK 运行本地单元测试

Firebase 使用版本 9 的 JavaScript SDK 及其版本 8 SDK 分发安全规则单元测试库。库 API 明显不同。我们建议使用 v9 测试库,该库更精简,连接到模拟器所需的设置更少,从而可以安全地避免意外使用生产资源。为了实现向后兼容性,我们将继续提供 v8 测试库

使用 @firebase/rules-unit-testing 模块与本地运行的模拟器交互。如果您遇到超时或 ECONNREFUSED 错误,请仔细检查模拟器是否确实正在运行。

我们强烈建议您使用最新版本的 Node.js,以便使用 async/await 表示法。您可能需要测试的几乎所有行为都涉及异步函数,而测试模块旨在与基于 Promise 的代码搭配使用。

v9 规则单元测试库始终知道模拟器,且绝不会影响生产资源。

您可以使用 v9 模块化导入语句导入该库。例如:

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  RulesTestEnvironment,
} 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.

导入后,实现单元测试涉及:

  • 通过调用 initializeTestEnvironment 创建和配置 RulesTestEnvironment
  • 设置测试数据而不触发规则,使用一种暂时性绕过 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,其行为与经过身份验证的 Authentication 用户类似。通过返回的上下文创建的请求将附加模拟身份验证令牌。(可选)传递定义身份验证令牌载荷的自定义声明或替换值的对象。

在测试中使用返回的测试上下文对象来访问配置的任何模拟器实例,包括使用 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()

使用行为类似于安全规则已停用的上下文运行测试设置函数。

此方法采用一个回调函数,该函数会接受绕过安全规则的上下文并返回 promise。一旦 Promise 解析 / 拒绝,该上下文将被销毁。

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'), { ... });

特定于模拟器的方法

另请参阅 v9 SDK 中的常用测试方法和效用函数

RulesTestEnvironment.clearFirestore() => Promise<void>

此方法用于清除 Firestore 数据库中的数据,该数据库属于为 Firestore 模拟器配置的 projectId

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

此方法会获取用于此测试上下文的 Firestore 实例。返回的 Firebase JS 客户端 SDK 实例可以与客户端 SDK API(v9 模块化模块或 v9 兼容型库)搭配使用。

直观呈现规则评估

通过 Firestore 模拟器,您可以在模拟器套件界面中直观呈现客户端请求,包括 Firebase 安全规则的评估跟踪。

打开 Firestore 和“请求”标签页,以查看每个请求的详细评估序列。

显示安全规则评估的 Firestore 模拟器请求监视器

生成测试报告

运行一系列测试后,您可以访问测试范围报告,其中显示了每个安全规则的评估方式。

如需获取这些报告,请在模拟器运行时查询其上的公开端点。如需适合浏览器的版本,请使用以下网址:

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

此报告页面将您的规则分解为表达式和子表达式,您可以将鼠标悬停在相应表达式上以了解更多信息(包括评估次数和返回的值)。如需这些数据的原始 JSON 版本,请在查询中包含以下网址:

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

模拟器和生产数据库的区别

  1. 您无需显式创建 Firestore 项目。模拟器会自动创建任何所访问的实例。
  2. Firestore 模拟器不适用于常规 Firebase 身份验证流程。不过,我们在 Firebase Test SDK 的 rules-unit-testing 库中提供了 initializeTestApp() 方法,该方法接受 auth 字段。使用此方法创建的 Firebase 句柄的行为将如同它已经以您提供的任何实体身份成功通过身份验证一样。如果您传入 null,该句柄的行为将与未经身份验证的用户相同(例如,auth != null 规则将失败)。

排查已知问题

使用 Firestore 模拟器时,您可能会遇到以下已知问题。请按照以下指导来排查您遇到的任何不正常行为。这些注释是使用安全规则单元测试库编写的,但常规方法适用于所有 Firebase SDK。

测试行为不一致

如果您的测试偶尔会通过和失败,即使没有对测试本身进行任何更改,您可能也需要验证它们是否正确排序。 与模拟器的大多数交互都是异步的,因此请仔细检查所有异步代码是否均已正确排序。您可以通过链式 Promise 或大量使用 await 表示法来解决排序问题。

尤其应检查以下异步操作:

  • 设置安全规则,例如使用 initializeTestEnvironment
  • 读取和写入数据,例如,使用 db.collection("users").doc("alice").get()
  • 操作断言,包括 assertSucceedsassertFails

测试仅在您首次加载模拟器时通过

模拟器是有状态的。它将写入其中的所有数据都存储在内存中,因此每当模拟器关闭时,所有数据都会丢失。如果您针对同一项目 ID 运行多个测试,则每个测试都会生成可能影响后续测试的数据。您可以使用以下任何方法来绕过此行为:

  • 为每个测试使用唯一的项目 ID。请注意,如果您选择这样做,则需要在每次测试中调用 initializeTestEnvironment;系统仅会自动为默认项目 ID 加载规则。
  • 重新构建您的测试,使其不与之前写入的数据进行交互(例如,为每个测试使用不同的集合)。
  • 删除测试期间写入的所有数据。

测试设置非常复杂

在设置测试时,您可能需要按照 Firestore 安全规则实际上不允许的方式修改数据。如果您的规则使得测试设置变得复杂,请尝试在设置步骤中使用 RulesTestEnvironment.withSecurityRulesDisabled,这样读写操作就不会触发 PERMISSION_DENIED 错误。

之后,您的测试可以分别使用 RulesTestEnvironment.authenticatedContextunauthenticatedContext 作为经过身份验证的用户或未经身份验证的用户执行操作。这样您就可以验证 Firestore 安全规则是否正确允许 / 拒绝不同情况。