测试 Cloud Firestore 安全规则

构建应用期间,您也许应该锁定对自己的 Cloud Firestore 数据库的访问。不过,在发布之前,您需要设置更精细的 Cloud Firestore Security Rules。借助 Cloud Firestore 模拟器,您不仅可以对应用的常规功能和行为进行原型设计和测试,还可以编写单元测试,以检查 Cloud Firestore Security Rules的行为。

快速入门

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

了解 Cloud Firestore Security Rules

使用移动和 Web 客户端库时,您可以实现 Firebase AuthenticationCloud Firestore Security Rules来处理无服务器的身份验证、授权和数据验证。

Cloud Firestore Security Rules 包含两部分:

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

Firebase Authentication 能够验证用户的凭据,为基于用户和角色的访问权限系统奠定基础。

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

如需详细了解 Cloud Firestore Security Rules,请参阅Cloud Firestore Security Rules 使用入门

安装模拟器

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

运行本地单元测试

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

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

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

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

版本 9 规则单元测试库始终知晓模拟器的存在,绝不会访问生产资源。

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

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.

导入后,需要完成以下操作才能实现单元测试:

  • 通过调用 initializeTestEnvironment 来创建和配置 RulesTestEnvironment
  • 设置测试数据而不触发规则 - 使用一种允许您暂时绕过规则的便捷方法 (RulesTestEnvironment.withSecurityRulesDisabled)。
  • 设置测试套件和每个测试运行前后的钩子 - 使用用于清理测试数据和环境的调用,例如 RulesTestEnvironment.cleanup()RulesTestEnvironment.clearFirestore()
  • 实现使用 RulesTestEnvironment.authenticatedContextRulesTestEnvironment.unauthenticatedContext 来模拟身份验证状态的测试用例。

常用的方法和实用函数

另请参阅 SDK 版本 9 中专用于模拟器的测试方法

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 用户类似。通过返回的上下文创建的请求将会附加模拟的 Authentication 令牌。(可选)为 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,其行为方式与未通过 Authentication 登录的客户端类似。通过返回的上下文创建的请求不会附加 Firebase Auth 令牌。

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

专用于模拟器的方法

另请参阅 SDK 版本 9 中的常用测试方法和实用函数

RulesTestEnvironment.clearFirestore() => Promise<void>

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

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

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

直观呈现规则评估

借助 Cloud Firestore 模拟器,您可以在 Emulator Suite 界面中直观呈现客户端请求,包括对 Firebase 安全规则的评估跟踪。

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

显示安全规则评估的 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 Authentication 流程。不过,我们在 Firebase Test SDK 的 rules-unit-testing 库中提供了 initializeTestApp() 方法,该方法接受 auth 字段。使用此方法创建的 Firebase 句柄的行为就好像它已作为您提供的任何实体成功通过了身份验证一样。如果您传入 null,其行为将与未经身份验证的用户相同(例如,auth != null 规则将失败)。

排查已知问题

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

测试行为不一致

在未对测试本身进行任何更改的情况下,如果您的测试时而通过时而失败,您可能需要确认它们有正确的排序。与模拟器的大多数交互都是异步的,因此请仔细检查所有异步代码都有正确的排序。您可以通过链接 Promise 或按需要使用 await 记号来解决排序问题。

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

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

测试仅在第一次加载模拟器时通过

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

  • 为每个测试使用独一无二的项目 ID。请注意,如果您选择执行此操作,则需要在每个测试过程中调用 initializeTestEnvironment;规则仅针对默认项目 ID 自动加载。
  • 重新构建您的测试,使它们不与以前写入的数据交互(例如,为每个测试使用其他集合)。
  • 删除测试期间写入的所有数据。

测试设置非常复杂

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

然后,您的测试可以分别使用 RulesTestEnvironment.authenticatedContextunauthenticatedContext 以经过身份验证或未经身份验证的用户身份执行操作。这样您就可以验证 Cloud Firestore Security Rules是否正确允许/拒绝了不同案例。