Como testar regras de segurança

Durante o desenvolvimento do seu app, talvez seja interessante bloquear o acesso ao banco de dados do Firestore. No entanto, antes do lançamento, você precisará de regras de segurança mais detalhadas do Firestore. Com o emulador do Firestore, além de prototipar e testar os recursos e comportamentos gerais do app, é possível escrever testes de unidade que verificam o comportamento das suas regras de segurança do 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.

Noções básicas sobre as regras de segurança do Firestore

Implemente o Firebase Authentication e as regras de segurança do 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 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 Firestore ao 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, toda a solicitação falhará.

Saiba mais sobre as regras de segurança do Firestore em Primeiros passos com as regras de segurança do Firestore.

Instalar o emulador

Para instalar o emulador do 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 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 é compatível com a simulação de auth nas regras de segurança, o que facilita muito os testes de unidade. Além disso, a biblioteca é compatível com alguns recursos específicos de emulador, como limpar todos os dados, conforme listado abaixo.
  • Os emuladores também aceitarão tokens de autenticação de produção do Firebase, fornecidos pelos SDKs do cliente e avaliarão as regras, 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 JavaScript v9

O Firebase distribui uma biblioteca de testes de unidade de regras de segurança com o SDK do JavaScript versão 9 e o SDK versão 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, assim, evitar 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ê receber tempos limites ou 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 toque nos recursos de produção.

Você importa a biblioteca usando instruções de importação modular v9. Por 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:

  • Como criar e configurar um RulesTestEnvironment com uma chamada para initializeTestEnvironment.
  • Configurar dados de teste sem acionar regras, usando um método de conveniência que permite ignorá-las temporariamente, RulesTestEnvironment.withSecurityRulesDisabled.
  • Configurar o 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().
  • Implementação de casos de teste que imitam estados de autenticação usando RulesTestEnvironment.authenticatedContext e RulesTestEnvironment.unauthenticatedContext.

Métodos comuns e funções de utilitário

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 teste 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 de 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 por meio do contexto retornado terão um token de autenticação simulado. Opcionalmente, transmita um objeto que define declarações personalizadas ou substituições para payloads de tokens de autenticação.

Use o objeto de contexto de teste retornado nos seus testes para acessar qualquer instância de emulador configurada, incluindo as instâncias 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 por meio do Authentication. As solicitações criadas por meio do contexto retornado não terão tokens do Firebase Auth anexados.

Use o objeto de contexto de teste retornado nos seus testes para acessar qualquer instância de emulador configurada, incluindo as instâncias 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 retorno de chamada, que usa o contexto das regras de segurança e ignora uma promessa. O contexto será destruído quando a promessa for resolvida / rejeitada.

RulesTestEnvironment.cleanup()

Esse método destrói todas as RulesTestContexts criadas no ambiente de teste e limpa os recursos subjacentes, permitindo uma saída limpa.

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

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

Esta é uma função de utilitário 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 de utilitário 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 do emulador

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

RulesTestEnvironment.clearFirestore() => Promise<void>

Esse método limpa os dados no banco de dados do Firestore que pertencem 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 JavaScript pode ser usada com as APIs do SDK do cliente (v9 modular ou v9 compat).

Visualizar avaliações de regras

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

Abra a guia Firestore > Solicitações para ver a sequência de avaliação detalhada de 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 que mostram como cada uma das suas 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 você pode passar o mouse para ver mais informações, incluindo o número de avaliações e valores retornados. Para a versão em JSON bruta desses dados, inclua o seguinte URL na consulta:

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

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

  1. Não é necessário criar explicitamente um projeto do Firestore. O emulador criará automaticamente qualquer instância que for acessada.
  2. O emulador do Firestore não funciona com o fluxo normal do Firebase Authentication. Em vez disso, no SDK de teste do Firebase, nós fornecemos o método initializeTestApp() na biblioteca rules-unit-testing, que usa um campo auth. O gerenciador do Firebase criado com base nesse método se comportará como se tivesse sido autenticado com sucesso como qualquer entidade que você fornecer. Caso null seja transmitido, ele se comportará como um usuário não autenticado. Por exemplo, as regras auth != null falharão.

Resolver problemas conhecidos

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

O comportamento de 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. Use qualquer um dos métodos a seguir 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 forma que as regras de segurança do Firestore não permitam. Se as regras tornarem a configuração de teste complexa, tente usar RulesTestEnvironment.withSecurityRulesDisabled nas etapas de configuração. Portanto, as leituras e gravações não acionarão 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. Isso permite que você verifique se as regras de segurança do Firestore permitem / negam casos diferentes corretamente.