Testar as regras de segurança do Cloud Firestore

Durante o desenvolvimento do seu aplicativo, 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. É possível criar testes de unidade com o emulador do Cloud Firestore, para verificar o comportamento das suas regras de segurança do Cloud Firestore.

Guia de 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).

Noções básicas sobre 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 em Primeiros passos com as regras de segurança do Cloud Firestore.

Instalar o emulador

Para instalar o emulador do Cloud 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

Lembre-se das 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 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, o que permite conectar o aplicativo diretamente aos emuladores em testes manuais e de integração.

Executar testes locais

Use o módulo @firebase/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.

Os seguintes métodos são expostos pelo módulo:

  • initializeTestApp({ projectId: <name>, auth: <auth> }) => FirebaseApp

    Esse método retorna um aplicativo do Firebase inicializado, correspondente ao ID do projeto e à variável de autenticação especificada nas opções. Use esse método para criar um aplicativo autenticado como um usuário específico a ser utilizado nos testes.

     firebase.initializeTestApp({
       projectId: "my-test-project",
       auth: { uid: "alice", email: "alice@example.com" }
     });
    
  • initializeAdminApp({ projectId: <name> }) => 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 esse método para criar um aplicativo autenticado como administrador e configurar o estado para 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: <name>, rules: <rules> }) => 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. Ele deve ser usado para informar se ocorreu uma falha na leitura ou na 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 será recusada se a entrada for recusada. Ele deve ser usado 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: <name> }) => Promise

    Esse método limpa todos os dados associados a um projeto específico na instância do Firestore 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. Se você quiser 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 ver mais informações, incluindo o número de avaliações e os 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. 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, 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 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 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.
  • 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 Cloud 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.