获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

测试您的 Cloud Firestore 安全规则

在构建应用时,您可能希望锁定对 Cloud Firestore 数据库的访问。但是,在启动之前,您需要更细致入微的 Cloud Firestore 安全规则。使用 Cloud Firestore 模拟器,除了原型设计和测试应用的一般功能和行为之外,您还可以编写单元测试来检查 Cloud Firestore 安全规则的行为。

快速开始

对于一些具有简单规则的基本测试用例,请尝试快速入门示例

了解 Cloud Firestore 安全规则

使用移动和 Web 客户端库时,为无服务器身份验证、授权和数据验证实施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 身份验证令牌并相应地评估规则,从而允许在集成和手动测试中将您的应用程序直接连接到模拟器。

运行本地单元测试

使用 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()

运行一个测试设置函数,其上下文的行为就像安全规则被禁用一样。

这个方法接受一个回调函数,它接受安全规则绕过上下文并返回一个承诺。一旦承诺解决/拒绝,上下文将被销毁。

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 兼容)一起使用。

可视化规则评估

Cloud Firestore 模拟器可让您在 Emulator Suite UI 中可视化客户端请求,包括 Firebase 安全规则的评估跟踪。

打开Firestore > Requests选项卡以查看每个请求的详细评估顺序。

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

生成测试报告

运行一套测试后,您可以访问测试覆盖率报告,显示您的每个安全规则是如何评估的。

要获取报告,请在模拟器运行时查询其暴露的端点。对于浏览器友好的版本,请使用以下 URL:

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

这会将您的规则分解为表达式和子表达式,您可以将鼠标悬停以获取更多信息,包括评估的数量和返回的值。对于此数据的原始 JSON 版本,请在查询中包含以下 URL:

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

模拟器和生产之间的差异

  1. 您不必显式创建 Cloud Firestore 项目。模拟器会自动创建任何被访问的实例。
  2. Cloud Firestore 模拟器不适用于正常的 Firebase 身份验证流程。相反,在 Firebase 测试 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 安全规则实际上不允许的方式修改数据。如果您的规则使测试设置变得复杂,请尝试在设置步骤中使用RulesTestEnvironment.withSecurityRulesDisabled ,这样读取和写入不会触发PERMISSION_DENIED错误。

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