Testez vos règles de sécurité Cloud Firestore

Lorsque vous créez votre application, vous souhaiterez peut-être verrouiller l'accès à votre base de données Cloud Firestore. Cependant, avant de vous lancer, vous aurez besoin de règles de sécurité Cloud Firestore plus nuancées. Avec l'émulateur Cloud Firestore, en plus de prototyper et de tester les fonctionnalités générales et le comportement de votre application, vous pouvez écrire des tests unitaires qui vérifient le comportement de vos règles de sécurité Cloud Firestore.

Démarrage rapide

Pour quelques cas de test de base avec des règles simples, essayez l' exemple de démarrage rapide .

Comprendre les règles de sécurité de Cloud Firestore

Implémentez l'authentification Firebase et les règles de sécurité Cloud Firestore pour l'authentification, l'autorisation et la validation des données sans serveur lorsque vous utilisez les bibliothèques clientes mobiles et Web.

Les règles de sécurité Cloud Firestore comprennent deux éléments :

  1. Une instruction match qui identifie les documents dans votre base de données.
  2. Une expression allow qui contrôle l'accès à ces documents.

L'authentification Firebase vérifie les informations d'identification des utilisateurs et constitue la base des systèmes d'accès basés sur les utilisateurs et les rôles.

Chaque requête de base de données provenant d'une bibliothèque client mobile/Web Cloud Firestore est évaluée par rapport à vos règles de sécurité avant de lire ou d'écrire des données. Si les règles refusent l'accès à l'un des chemins de document spécifiés, la requête entière échoue.

Pour en savoir plus sur les règles de sécurité Cloud Firestore , consultez Premiers pas avec les règles de sécurité Cloud Firestore .

Installer l'émulateur

Pour installer l'émulateur Cloud Firestore, utilisez la CLI Firebase et exécutez la commande ci-dessous :

firebase setup:emulators:firestore

Exécutez l'émulateur

Commencez par initialiser un projet Firebase dans votre répertoire de travail. Il s'agit d'une première étape courante lors de l'utilisation de la CLI Firebase .

firebase init

Démarrez l'émulateur à l'aide de la commande suivante. L'émulateur fonctionnera jusqu'à ce que vous tuiez le processus :

firebase emulators:start --only firestore

Dans de nombreux cas, vous souhaitez démarrer l'émulateur, exécuter une suite de tests, puis arrêter l'émulateur après l'exécution des tests. Vous pouvez le faire facilement en utilisant la commande emulators:exec :

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

Une fois démarré, l'émulateur tentera de s'exécuter sur un port par défaut (8080). Vous pouvez changer le port de l'émulateur en modifiant la section "emulators" de votre fichier firebase.json :

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

Avant d'exécuter l'émulateur

Avant de commencer à utiliser l'émulateur, gardez à l'esprit les points suivants :

  • L'émulateur chargera initialement les règles spécifiées dans le champ firestore.rules de votre fichier firebase.json . Il attend le nom d'un fichier local contenant vos règles de sécurité Cloud Firestore et applique ces règles à tous les projets. Si vous ne fournissez pas le chemin du fichier local ou n'utilisez pas la méthode loadFirestoreRules comme décrit ci-dessous, l'émulateur traite tous les projets comme ayant des règles ouvertes.
  • Alors que la plupart des SDK Firebase fonctionnent directement avec les émulateurs, seule la bibliothèque @firebase/rules-unit-testing prend en charge auth simulée dans les règles de sécurité, ce qui rend les tests unitaires beaucoup plus faciles. De plus, la bibliothèque prend en charge quelques fonctionnalités spécifiques à l'émulateur, telles que la suppression de toutes les données, comme indiqué ci-dessous.
  • Les émulateurs accepteront également les jetons Firebase Auth de production fournis via les SDK clients et évalueront les règles en conséquence, ce qui permet de connecter votre application directement aux émulateurs lors des tests d'intégration et manuels.

Exécuter des tests unitaires locaux

Exécutez des tests unitaires locaux avec le SDK JavaScript v9

Firebase distribue une bibliothèque de tests unitaires de règles de sécurité avec à la fois son SDK JavaScript version 9 et son SDK version 8. Les API de la bibliothèque sont très différentes. Nous recommandons la bibliothèque de tests v9, qui est plus rationalisée et nécessite moins de configuration pour se connecter aux émulateurs et ainsi éviter en toute sécurité une utilisation accidentelle des ressources de production. Pour des raisons de compatibilité ascendante, nous continuons à rendre disponible la bibliothèque de tests v8 .

Utilisez le module @firebase/rules-unit-testing pour interagir avec l'émulateur qui s'exécute localement. Si vous obtenez des délais d'attente ou des erreurs ECONNREFUSED , vérifiez que l'émulateur est réellement en cours d'exécution.

Nous vous recommandons fortement d'utiliser une version récente de Node.js afin de pouvoir utiliser la notation async/await . Presque tous les comportements que vous souhaiterez peut-être tester impliquent des fonctions asynchrones et le module de test est conçu pour fonctionner avec du code basé sur Promise.

La bibliothèque de tests unitaires de règles v9 est toujours au courant des émulateurs et ne touche jamais à vos ressources de production.

Vous importez la bibliothèque à l'aide des instructions d'importation modulaires v9. Par exemple:

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment
} 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.

Une fois importés, la mise en œuvre des tests unitaires implique :

  • Création et configuration d'un RulesTestEnvironment avec un appel à initializeTestEnvironment .
  • Configuration des données de test sans déclencher de règles, à l'aide d'une méthode pratique qui vous permet de les contourner temporairement, RulesTestEnvironment.withSecurityRulesDisabled .
  • Configuration d'une suite de tests et de hooks avant/après par test avec des appels pour nettoyer les données de test et l'environnement, comme RulesTestEnvironment.cleanup() ou RulesTestEnvironment.clearFirestore() .
  • Implémentation de cas de test qui imitent les états d'authentification à l'aide de RulesTestEnvironment.authenticatedContext et RulesTestEnvironment.unauthenticatedContext .

Méthodes courantes et fonctions utilitaires

Consultez également les méthodes de test spécifiques à l'émulateur dans le SDK v9 .

initializeTestEnvironment() => RulesTestEnvironment

Cette fonction initialise un environnement de test pour les tests unitaires de règles. Appelez d’abord cette fonction pour la configuration du test. Une exécution réussie nécessite que les émulateurs soient en cours d'exécution.

La fonction accepte un objet facultatif définissant un TestEnvironmentConfig , qui peut consister en un ID de projet et des paramètres de configuration de l'émulateur.

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

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

Cette méthode crée un RulesTestContext , qui se comporte comme un utilisateur d'authentification authentifié. Les requêtes créées via le contexte renvoyé seront accompagnées d'un jeton d'authentification fictif. Vous pouvez éventuellement transmettre un objet définissant des revendications personnalisées ou des remplacements pour les charges utiles du jeton d'authentification.

Utilisez l'objet de contexte de test renvoyé dans vos tests pour accéder à toutes les instances d'émulateur configurées, y compris celles configurées avec 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

Cette méthode crée un RulesTestContext , qui se comporte comme un client non connecté via l'authentification. Les requêtes créées via le contexte renvoyé ne seront pas associées à des jetons d'authentification Firebase.

Utilisez l'objet de contexte de test renvoyé dans vos tests pour accéder à toutes les instances d'émulateur configurées, y compris celles configurées avec 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()

Exécutez une fonction de configuration de test avec un contexte qui se comporte comme si les règles de sécurité étaient désactivées.

Cette méthode prend une fonction de rappel, qui prend le contexte de contournement des règles de sécurité et renvoie une promesse. Le contexte sera détruit une fois la promesse résolue/rejetée.

RulesTestEnvironment.cleanup()

Cette méthode détruit tous RulesTestContexts créés dans l'environnement de test et nettoie les ressources sous-jacentes, permettant une sortie propre.

Cette méthode ne change en rien l’état des émulateurs. Pour réinitialiser les données entre les tests, utilisez la méthode d'effacement des données spécifique à l'émulateur d'application.

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

Il s'agit d'une fonction utilitaire de scénario de test.

La fonction affirme que la promesse fournie encapsulant une opération d'émulateur sera résolue sans violation des règles de sécurité.

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

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

Il s'agit d'une fonction utilitaire de scénario de test.

La fonction affirme que la promesse fournie encapsulant une opération d'émulateur sera rejetée en cas de violation des règles de sécurité.

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

Méthodes spécifiques à l'émulateur

Consultez également les méthodes de test et les fonctions utilitaires courantes dans le SDK v9 .

RulesTestEnvironment.clearFirestore() => Promise<void>

Cette méthode efface les données de la base de données Firestore qui appartiennent au projectId configuré pour l'émulateur Firestore.

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

Cette méthode obtient une instance Firestore pour ce contexte de test. L'instance du SDK client Firebase JS renvoyée peut être utilisée avec les API du SDK client (v9 modulaire ou compatible v9).

Visualisez les évaluations des règles

L'émulateur Cloud Firestore vous permet de visualiser les demandes des clients dans l'interface utilisateur d'Emulator Suite, y compris le suivi d'évaluation des règles de sécurité Firebase.

Ouvrez l'onglet Firestore > Requêtes pour afficher la séquence d'évaluation détaillée de chaque demande.

Moniteur de requêtes de l'émulateur Firestore affichant les évaluations des règles de sécurité

Générer des rapports de tests

Après avoir exécuté une suite de tests, vous pouvez accéder aux rapports de couverture des tests qui montrent comment chacune de vos règles de sécurité a été évaluée.

Pour obtenir les rapports, interrogez un point de terminaison exposé sur l'émulateur pendant son exécution. Pour une version conviviale pour le navigateur, utilisez l'URL suivante :

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

Cela divise vos règles en expressions et sous-expressions sur lesquelles vous pouvez passer la souris pour plus d'informations, y compris le nombre d'évaluations et les valeurs renvoyées. Pour la version JSON brute de ces données, incluez l'URL suivante dans votre requête :

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

Différences entre l'émulateur et la production

  1. Vous n'êtes pas obligé de créer explicitement un projet Cloud Firestore. L'émulateur crée automatiquement toute instance accessible.
  2. L'émulateur Cloud Firestore ne fonctionne pas avec le flux d'authentification Firebase normal. Au lieu de cela, dans le SDK Firebase Test, nous avons fourni la méthode initializeTestApp() dans la bibliothèque rules-unit-testing , qui prend un champ auth . Le handle Firebase créé à l’aide de cette méthode se comportera comme s’il s’était authentifié avec succès en tant qu’entité que vous fournissez. Si vous transmettez null , il se comportera comme un utilisateur non authentifié ( les règles auth != null échoueront, par exemple).

Résoudre les problèmes connus

Lorsque vous utilisez l'émulateur Cloud Firestore, vous pouvez rencontrer les problèmes connus suivants. Suivez les instructions ci-dessous pour résoudre tout comportement irrégulier que vous rencontrez. Ces notes sont rédigées en pensant à la bibliothèque de tests unitaires des règles de sécurité, mais les approches générales sont applicables à n'importe quel SDK Firebase.

Le comportement du test est incohérent

Si vos tests réussissent ou échouent occasionnellement, même sans aucune modification des tests eux-mêmes, vous devrez peut-être vérifier qu'ils sont correctement séquencés. La plupart des interactions avec l'émulateur sont asynchrones, alors vérifiez que tout le code asynchrone est correctement séquencé. Vous pouvez corriger le séquençage soit en enchaînant les promesses, soit en utilisant généreusement la notation await .

Examinez en particulier les opérations asynchrones suivantes :

  • Définir des règles de sécurité, avec par exemple initializeTestEnvironment .
  • Lire et écrire des données, avec par exemple db.collection("users").doc("alice").get() .
  • Assertions opérationnelles, notamment assertSucceeds et assertFails .

Les tests ne réussissent que la première fois que vous chargez l'émulateur

L'émulateur est avec état. Il stocke toutes les données qui y sont écrites en mémoire, de sorte que toutes les données sont perdues à chaque arrêt de l'émulateur. Si vous exécutez plusieurs tests sur le même identifiant de projet, chaque test peut produire des données susceptibles d'influencer les tests ultérieurs. Vous pouvez utiliser l’une des méthodes suivantes pour contourner ce problème :

  • Utilisez des ID de projet uniques pour chaque test. Notez que si vous choisissez de procéder ainsi, vous devrez appeler initializeTestEnvironment dans le cadre de chaque test ; les règles ne sont automatiquement chargées que pour l’ID de projet par défaut.
  • Restructurez vos tests afin qu'ils n'interagissent pas avec des données précédemment écrites (par exemple, utilisez une collection différente pour chaque test).
  • Supprimez toutes les données écrites lors d’un test.

La configuration des tests est très compliquée

Lors de la configuration de votre test, vous souhaiterez peut-être modifier les données d'une manière que vos règles de sécurité Cloud Firestore ne permettent pas réellement. Si vos règles rendent la configuration des tests complexe, essayez d'utiliser RulesTestEnvironment.withSecurityRulesDisabled dans vos étapes de configuration, afin que les lectures et les écritures ne déclenchent pas d'erreurs PERMISSION_DENIED .

Après cela, votre test peut effectuer des opérations en tant qu'utilisateur authentifié ou non authentifié en utilisant respectivement RulesTestEnvironment.authenticatedContext et unauthenticatedContext . Cela vous permet de valider que vos règles de sécurité Cloud Firestore autorisent/refusent correctement différents cas.