Testar as regras de segurança do Cloud Firestore

Durante o desenvolvimento do seu app, talvez seja interessante bloquear o acesso ao banco de dados do Cloud Firestore. No entanto, antes do lançamento, será necessário adicionar mais detalhes às regras de segurança do Cloud Firestore. Com o emulador do Cloud Firestore, além de prototipar e testar os recursos e comportamentos gerais do app, é possível criar testes de unidade que verificam o comportamento das regras de segurança do Cloud Firestore.

Guia de início rápido

Para alguns casos de teste básicos com regras simples, utilize o exemplo do guia de início rápido.

Entender as regras de segurança do Cloud Firestore

Implemente o Firebase Authentication e as regras de segurança do Cloud Firestore para autenticação, autorização e validação de dados sem servidor ao usar as bibliotecas de cliente da Web e para dispositivos móveis.

As regras de segurança do Cloud Firestore incluem dois elementos:

  1. Uma instrução match que identifica documentos no banco de dados.
  2. Uma expressão allow que controla o acesso a esses documentos.

O Firebase Authentication verifica as credenciais dos usuários e fornece as bases para sistemas de acesso de acordo com usuários e papéis.

Todas as solicitações de bibliotecas de cliente da Web ou para dispositivos móveis do Cloud Firestore ao seu banco de dados são avaliadas em relação às regras de segurança antes da leitura ou gravação de dados. Se as regras negarem o acesso a qualquer um dos caminhos de documento especificados, a solicitação falhará como um todo.

Saiba mais sobre as regras de segurança do Cloud Firestore neste link.

Instalar o emulador

Para instalar o emulador do Cloud Firestore, use a CLI do Firebase e execute o comando abaixo:

firebase setup:emulators:firestore

Executar o emulador

Comece inicializando um projeto do Firebase no seu diretório de trabalho. Essa é uma primeira etapa comum ao usar a CLI do Firebase.

firebase init

Inicie o emulador usando o comando a seguir. O emulador será executado até você encerrar o processo:

firebase emulators:start --only firestore

Em muitos casos, você quer iniciar o emulador, executar um conjunto de testes e, em seguida, desligar o emulador. É possível fazer isso facilmente usando o comando emulators:exec:

firebase emulators:exec --only firestore "./my-test-script.sh"

Quando iniciado, o emulador tentará ser executado em uma porta padrão (8080). Para alterar a porta do emulador, modifique a seção "emulators" do arquivo firebase.json:

{
  // ...
  "emulators": {
    "firestore": {
      "port": "YOUR_PORT"
    }
  }
}

Antes de executar o emulador

Considere as seguintes informações antes de começar a usar o emulador:

  • Inicialmente, o emulador carregará as regras especificadas no campo firestore.rules do seu arquivo firebase.json. Ele espera receber o nome de um arquivo local com as regras de segurança do Cloud Firestore e aplica essas regras a todos os projetos. Se você não fornecer o caminho do arquivo local ou usar o método loadFirestoreRules, conforme descrito abaixo, o emulador tratará todos os projetos como tendo regras abertas.
  • Enquanto a maioria dos SDKs do Firebase trabalha com emuladores diretamente, somente a biblioteca @firebase/rules-unit-testing tem suporte à simulação de auth nas regras de segurança, o que facilita muito os testes de unidade. Além disso, a biblioteca tem suporte a alguns recursos específicos de emulador, como limpar todos os dados, conforme listado abaixo.
  • Os emuladores também vão aceitar tokens de produção do Firebase Authentication fornecidos pelos SDKs do cliente e avaliarão as regras de acordo, o que permite conectar o aplicativo diretamente aos emuladores em testes manuais e de integração.

Executar testes de unidade locais

Executar testes de unidade locais com o SDK para JavaScript v9

O Firebase distribui uma biblioteca de testes de unidade de regras de segurança com o SDK para JavaScript versão 9 e 8. As APIs da biblioteca são significativamente diferentes. Recomendamos a biblioteca de testes v9, que é mais simples e requer menos configuração para se conectar a emuladores e, portanto, evita o uso acidental de recursos de produção. Para oferecer compatibilidade com versões anteriores, continuamos a disponibilizar a biblioteca de testes v8.

Use o módulo @firebase/rules-unit-testing para interagir com o emulador executado localmente. Se você alcançar o tempo limite ou receber erros ECONNREFUSED, verifique se o emulador está sendo executado.

Recomendamos o uso de uma versão recente do Node.js para que você possa usar a notação async/await. Quase todo o comportamento que pode ser testado envolve funções assíncronas, e o módulo de teste é projetado para funcionar com código baseado em promessas.

A biblioteca de teste de unidade de regras v9 está sempre ciente dos emuladores e nunca interfere nos recursos de produção.

A biblioteca é importada usando instruções de importação modular v9. Exemplo:

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.

Depois de importados, a implementação de testes de unidade envolve:

  • a criação e a configuração de um RulesTestEnvironment com uma chamada para initializeTestEnvironment;
  • a configuração de dados de teste sem acionar regras, usando um método de conveniência que permite ignorá-las temporariamente, RulesTestEnvironment.withSecurityRulesDisabled;
  • a configuração do conjunto de testes e os hooks por teste antes/depois com as chamadas para limpar os dados e o ambiente de teste, como RulesTestEnvironment.cleanup() ou RulesTestEnvironment.clearFirestore();
  • a implementação de casos de teste que imitam estados de autenticação usando RulesTestEnvironment.authenticatedContext e RulesTestEnvironment.unauthenticatedContext.

Métodos comuns e funções utilitárias

Veja também métodos de testes específicos do emulador no SDK v9.

initializeTestEnvironment() => RulesTestEnvironment

Essa função inicializa um ambiente de teste para testes de unidade de regras. Chame essa função primeiro para configurar o teste. A execução bem-sucedida requer que os emuladores estejam em execução.

A função aceita um objeto opcional que define um TestEnvironmentConfig, que pode consistir em um ID do projeto e em configurações do emulador.

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

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

Esse método cria um RulesTestContext, que se comporta como um usuário autenticado do Authentication. As solicitações criadas pelo contexto retornado terão um token simulado do Authentication anexados. Opcionalmente, transmita um objeto que define declarações personalizadas ou substituições para payloads de tokens do Authentication.

Use o objeto de contexto de teste retornado nos testes para acessar qualquer instância configurada do emulador, incluindo as configuradas com 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

Esse método cria um RulesTestContext, que se comporta como um cliente não conectado pelo Authentication. As solicitações criadas pelo contexto retornado não terão tokens do Firebase Auth anexados.

Use o objeto de contexto de teste retornado nos testes para acessar qualquer instância configurada do emulador, incluindo as configuradas com 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()

Execute uma função de configuração de teste com um contexto que se comporte como se as regras de segurança estivessem desativadas.

Esse método usa uma função de callback com um contexto que ignora as regras de segurança e retorna uma promessa. O contexto será destruído quando a promessa for resolvida ou rejeitada.

RulesTestEnvironment.cleanup()

Esse método destrói todos os RulesTestContexts criados no ambiente de teste e limpa os recursos restantes, permitindo uma saída limpa.

Esse método não altera o estado dos emuladores. Para redefinir dados entre testes, use o método do aplicativo de dados limpos específico para emulador.

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

Esta é uma função utilitária do caso de teste.

A função declara que a promessa fornecida que envolve uma operação de emulador será resolvida sem violações das regras de segurança.

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

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

Esta é uma função utilitária do caso de teste.

A função declara que a promessa fornecida que envolve uma operação de emulador será rejeitada com uma violação das regras de segurança.

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

Métodos específicos para emulador

Veja também métodos de testes comuns e funções utilitárias no SDK v9.

RulesTestEnvironment.clearFirestore() => Promise<void>

Esse método limpa dados no banco de dados do Firestore que pertence ao projectId configurado para o emulador do Firestore.

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

Esse método recebe uma instância do Firestore para este contexto de teste. A instância retornada do SDK do cliente do Firebase para JS pode ser usada com as APIs do SDK do cliente (v9 modular ou v9 compatível).

Visualizar avaliações de regras

O emulador do Cloud Firestore permite visualizar solicitações de cliente na IU do Pacote do emulador, incluindo o rastreamento de avaliações das regras de segurança do Firebase.

Abra a guia Firestore > Solicitações para visualizar a sequência de avaliação detalhada para cada solicitação.

Monitor de solicitações do emulador do Firestore mostrando avaliações de regras de segurança

Gerar relatórios de teste

Depois de executar um conjunto de testes, é possível acessar relatórios de cobertura de teste que mostram como cada uma das regras de segurança foi avaliada.

Para acessar os relatórios, consulte um endpoint exposto no emulador enquanto ele está em execução. Para uma versão otimizada para navegadores, use o seguinte URL:

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

Isso divide suas regras em expressões e subexpressões em que é possível passar o mouse para encontrar mais informações, incluindo o número de execuções e os valores retornados. Para a versão em JSON bruta desses dados, inclua o seguinte URL em sua consulta:

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

Diferenças entre o emulador e a produção

  1. Você não precisa criar um projeto do Cloud Firestore de maneira explícita. O emulador criará automaticamente qualquer instância que for acessada.
  2. O emulador do Cloud Firestore não funciona com o fluxo normal do Firebase Authentication. Em vez disso, no SDK de teste do Firebase, fornecemos o método initializeTestApp() na biblioteca rules-unit-testing, que usa um campo auth. O gerenciador do Firebase, criado com esse método, se comportará como se tivesse sido autenticado da mesma forma que qualquer entidade fornecida por você. Caso null seja transmitido, ele vai se comportar como um usuário não autenticado. Por exemplo, as regras auth != null vão falhar.

Resolver problemas conhecidos

Ao usar o emulador do Cloud Firestore, talvez você se depare com os problemas conhecidos a seguir. Siga as orientações abaixo para resolver qualquer comportamento irregular que ocorrer. Essas observações foram escritas pensando na biblioteca de teste de unidade de regras de segurança, mas as abordagens gerais são aplicáveis a qualquer SDK do Firebase.

O comportamento do teste é inconsistente

Caso os testes estejam sendo aprovados e apresentando falha em algumas ocasiões, mesmo sem nenhuma alteração neles, pode ser necessário verificar se eles estão devidamente sequenciados. A maioria das interações com o emulador é assíncrona. Dessa forma, verifique novamente se todo o código assíncrono está sequenciado adequadamente. Para corrigir o sequenciamento, encadeie as promessas ou use a notação await livremente.

Especificamente, analise as seguintes operações assíncronas:

  • Definir regras de segurança com, por exemplo, initializeTestEnvironment.
  • Ler e gravar dados com, por exemplo, db.collection("users").doc("alice").get().
  • Declarações operacionais, incluindo assertSucceeds e assertFails.

Os testes são aprovados apenas na primeira vez que você carrega o emulador

O emulador com estado armazena todos os dados gravados na memória. Dessa forma, todos eles são perdidos sempre que o emulador é desligado. Se você estiver executando vários testes com o mesmo ID do projeto, cada um deles poderá produzir dados que podem influenciar os testes subsequentes. É possível usar qualquer um dos seguintes métodos para ignorar esse comportamento:

  • Use IDs de projetos exclusivos para cada teste. Se você optar por fazer isso, será necessário chamar initializeTestEnvironment como parte de cada teste. As regras são carregadas automaticamente apenas para o ID do projeto padrão.
  • Reestruture seus testes para que eles não interajam com dados gravados anteriormente (por exemplo, use uma coleção diferente para cada teste).
  • Exclua todos os dados gravados durante um teste.

A configuração do teste é muito complicada

Ao configurar seu teste, talvez você queira modificar os dados de uma maneira que as regras de segurança do Cloud Firestore não permitam. Se as regras estiverem tornando a configuração de teste complexa, tente usar RulesTestEnvironment.withSecurityRulesDisabled nas etapas de configuração para que as leituras e gravações não acionem erros PERMISSION_DENIED.

Depois disso, o teste pode executar operações como um usuário autenticado ou não autenticado usando RulesTestEnvironment.authenticatedContext e unauthenticatedContext, respectivamente. Com isso, você verifica se as regras de segurança do Cloud Firestore permitem ou negam casos diferentes corretamente.