Como testar regras de segurança

Durante o desenvolvimento do seu aplicativo, talvez seja interessante bloquear o acesso ao banco de dados do Firestore. No entanto, antes do lançamento, será necessário adicionar mais detalhes às regras de segurança do Firestore. Com o emulador do Firestore, você pode criar testes de unidade que verificam o comportamento das suas regras de segurança do Firestore.

Início rápido

Em alguns casos de teste básicos com regras simples, utilize o guia de início rápido do JavaScript ou do TypeScript (conteúdo dos links em inglês).

Como entender 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 duas partes:

  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.

Toda solicitação de uma biblioteca de cliente da Web/para dispositivos móveis do Firestore para o banco de dados é avaliada em relação às regras de segurança antes de ler ou gravar 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 Firestore em Primeiros passos com as regras de segurança do Firestore.

Instalar o emulador

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

firebase setup:emulators:firestore

Executar o emulador

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 que contém suas 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 muitos SDKs trabalham com emuladores, somente o @firebase/testing o módulo Node.js é compatível com simulação auth nas Regras de segurança, o que facilita muito os testes de unidade. Além disso, o módulo é 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 de acordo, o que permite conectar o aplicativo diretamente aos emuladores em testes manuais e de integração.

Executar testes locais

initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp

Esse método retorna um aplicativo do Firebase inicializado que corresponde ao código do projeto e à variável de autenticação especificados nas opções. Use esse método e crie um aplicativo autenticado como um usuário específico para ser utilizado em testes.

firebase.initializeTestApp({
  projectId: "my-test-project",
  auth: { uid: "alice", email: "alice@example.com" }
});

initializeAdminApp({ projectId: string }) => FirebaseApp

Esse método retorna um aplicativo de administrador inicializado do Firebase. As regras de segurança são ignoradas quando esse aplicativo executa leituras e gravações. Use-o para criar um aplicativo autenticado como administrador e definir o estado para os testes.

firebase.initializeAdminApp({ projectId: "my-test-project" });
    

apps() => [FirebaseApp] Esse método retorna todos os aplicativos de teste e administração inicializados e em execução no momento. Ele pode ser usado para limpar aplicativos entre ou após os testes.

Promise.all(firebase.apps().map(app => app.delete()))

loadFirestoreRules({ projectId: string, rules: Object }) => Promise

Esse método envia regras para um banco de dados em execução localmente. Ele utiliza um objeto que especifica as regras como uma string. Use-o para definir as regras do seu banco de dados.

firebase.loadFirestoreRules({
  projectId: "my-test-project",
  rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
});
    

assertFails(pr: Promise) => Promise

Esse método retorna uma promessa que será recusada se a entrada for concluída e que será concluída se a entrada for recusada. Use-o para informar se ocorreu uma falha na leitura ou gravação no banco de dados.

firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
    

assertSucceeds(pr: Promise) => Promise

Esse método retorna uma promessa que será concluída se a entrada for concluída e que será recusada se a entrada for recusada. Use-o para informar se uma leitura ou gravação no banco de dados foi concluída.

firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
    

clearFirestoreData({ projectId: string }) => Promise

Esse método limpa todos os dados associados a um projeto específico na instância do Firestore que estão em execução localmente. Use-o para limpar os dados após os testes.

firebase.clearFirestoreData({
  projectId: "my-test-project"
});
   

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 você pode passar o mouse para ver 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. Não é necessário criar um projeto do Firestore de maneira explícita. 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() no módulo de teste, que ocupa 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 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, é possível que você encontre os problemas conhecidos a seguir. Siga as orientações abaixo para resolver qualquer comportamento irregular que ocorrer. Essas observações foram escritas pensando no SDK de teste do Firebase, 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, firebase.loadFirestoreRules.
  • Ler e gravar dados com, por exemplo, db.collection("users").doc("alice").get().
  • Declarações operacionais, incluindo firebase.assertSucceeds e firebase.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 código 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 loadFirestoreRules 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

Pode ser necessário testar cenários que suas regras de segurança do Firestore não permitem. Por exemplo, testar se usuários não autenticados podem editar dados é uma tarefa difícil, porque não é possível editar dados como um usuário não autenticado.

Caso as regras estejam prejudicando a configuração do teste, utilize um cliente autorizado pelo administrador para ignorá-las. Faça isso com firebase.initializeAdminApp. Leituras e gravações de clientes autorizados pelo administrador ignoram regras e não acionam erros PERMISSION_DENIED.