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

Lors du développement de votre application, vous voudrez peut-être verrouiller l'accès à votre base de données Cloud Firestore. Toutefois, avant le lancement, vous aurez besoin de Cloud Firestore Security Rules plus nuancées. Avec l'émulateur Cloud Firestore, en plus de prototyper et tester les fonctionnalités et le comportement généraux de votre application, vous pouvez écrire des tests unitaires qui vérifient le comportement de votre Cloud Firestore Security Rules.

Guide de démarrage rapide

Pour quelques scénarios de test basiques avec des règles simples, essayez l'exemple de guide de démarrage rapide.

Comprendre Cloud Firestore Security Rules

Installez Firebase Authentication et Cloud Firestore Security Rules pour l'authentification, l'autorisation et la validation des données sans serveur lorsque vous utilisez les bibliothèques clientes mobiles et Web.

Cloud Firestore Security Rules comprend 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.

Firebase Authentication vérifie les identifiants des utilisateurs et constitue la base des systèmes d'accès basés sur l'utilisateur et sur les rôles.

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

Pour en savoir plus sur Cloud Firestore Security Rules, consultez Premiers pas avec Cloud Firestore Security Rules.

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écuter 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 s'exécute jusqu'à ce que vous arrêtiez le processus :

firebase emulators:start --only firestore

Dans la plupart des 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. Pour ce faire, utilisez la commande emulators:exec :

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

Au démarrage, l'émulateur tente de s'exécuter sur un port par défaut (8080). Vous pouvez modifier 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 charge 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 votre Cloud Firestore Security Rules et applique ces règles à tous les projets. Si vous ne fournissez pas le chemin d'accès au fichier local ou utilisez la méthode loadFirestoreRules comme décrit ci-dessous, l'émulateur traite tous les projets comme ayant des règles ouvertes.
  • Bien que la plupart des SDK Firebase fonctionnent directement avec les émulateurs, seule la bibliothèque @firebase/rules-unit-testing accepte une simulation d'option auth dans les règles de sécurité, ce qui facilite considérablement les tests unitaires. En outre, la bibliothèque est compatible avec quelques fonctionnalités spécifiques à l'émulateur, telles que la suppression de toutes les données, comme indiqué ci-dessous.
  • Les émulateurs acceptent également les jetons d'authentification Firebase de production fournis par les SDK du client et évaluent les règles en conséquence, ce qui permet de connecter directement votre application aux émulateurs dans les tests d'intégration et les tests manuels.

Exécuter des tests unitaires locaux

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

Firebase distribue une bibliothèque de tests unitaires de règles de sécurité avec son SDK JavaScript version 9 ainsi qu'avec son SDK version 8. Les API de bibliothèques sont très différentes. Nous recommandons d'utiliser la bibliothèque de tests v9, qui est bien plus simple et nécessite moins d'opérations de configuration pour la connexion aux émulateurs. Cela permet d'éviter toute utilisation accidentelle des ressources de production. Dans un souci de rétrocompatibilité, nous laissons la bibliothèque de tests v8 disponible.

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

Nous vous recommandons vivement d'utiliser une version récente de Node.js afin de pouvoir utiliser la notation async/await. La quasi-totalité du comportement que vous souhaitez tester implique 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 a toujours connaissance des émulateurs et ne modifie jamais vos ressources de production.

Vous importez la bibliothèque à l'aide d'instructions d'importation modulaire v9. 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, les tests unitaires impliquent ce qui suit :

  • Créer et configurer un RulesTestEnvironment avec un appel vers initializeTestEnvironment
  • Configurer des données de test sans déclencher de règles à l'aide d'une méthode pratique vous permettant de les contourner temporairement, RulesTestEnvironment.withSecurityRulesDisabled
  • Configurer la suite de tests et les hooks avant/après spécifiques à chaque test en effectuant des appels pour nettoyer les données et l'environnement de test, tels que RulesTestEnvironment.cleanup() ou RulesTestEnvironment.clearFirestore()
  • Mettre en œuvre des scénarios de test qui imitent des états d'authentification à l'aide de RulesTestEnvironment.authenticatedContext et RulesTestEnvironment.unauthenticatedContext

Méthodes et fonctions utilitaires courantes

Consultez également la section 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. Pour que l'exécution aboutisse, les émulateurs doivent être en cours d'exécution.

La fonction accepte un objet facultatif définissant une TestEnvironmentConfig, qui peut être constituée d'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 Authentication authentifié. Les requêtes créées via le contexte renvoyé comportent un jeton Authentication fictif associé. Vous pouvez également transmettre un objet définissant des revendications personnalisées ou des un mécanisme de forçage pour les charges utiles de jeton Authentication.

Utilisez l'objet de contexte de test renvoyé dans vos tests pour accéder aux instances de l'é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 Authentication. Les requêtes créées via le contexte renvoyé ne disposent pas de jetons Firebase Auth associés.

Utilisez l'objet de contexte de test renvoyé dans vos tests pour accéder aux instances de l'é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 simulant une désactivation des règles de sécurité.

Cette méthode utilise une fonction de rappel, qui prend le contexte de contournement des règles de sécurité et renvoie une promesse. Le contexte est détruit une fois que la promesse est acceptée ou rejetée.

RulesTestEnvironment.cleanup()

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

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

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

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

La fonction indique que la promesse fournie encapsulant une opération d'émulateur sera acceptée, 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 indique que la promesse fournie encapsulant une opération d'émulateur sera rejetée, avec une violation des règles de sécurité.

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

Méthodes spécifiques à l'émulateur

Consultez également la section Méthodes de test et 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 appartenant au projectId configuré pour l'émulateur Firestore.

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

Cette méthode récupère 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).

Visualiser les évaluations de règles

L'émulateur Cloud Firestore vous permet de visualiser les requêtes des clients dans l'interface utilisateur de la suite d'émulateurs, y compris le traçage d'évaluation pour les règles de sécurité Firebase.

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

Surveillance des requêtes de l&#39;émulateur Firestore affichant les évaluations des règles de sécurité

Générer des rapports de test

Après avoir exécuté une suite de tests, vous pouvez accéder à des rapports sur la 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 adaptée aux navigateurs, utilisez l'URL suivante :

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

Cette opération casse vos règles en expressions et sous-expressions que vous pouvez survoler avec la souris pour obtenir 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'avez pas besoin de créer explicitement un projet Cloud Firestore. L'émulateur crée automatiquement une instance à laquelle vous accédez.
  2. L'émulateur Cloud Firestore ne fonctionne pas avec le flux Firebase Authentication standard. Au lieu de cela, dans le SDK Firebase Test, nous avons fourni la méthode initializeTestApp() dans la bibliothèque rules-unit-testing, qui accepte un champ auth. Le handle Firebase créé à l'aide de cette méthode se comportera comme s'il était authentifié comme n'importe quelle entité que vous fournissez. Si vous transmettez la valeur null, il se comporte comme un utilisateur non authentifié (les règles auth != null échouent, 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 conseils ci-dessous pour résoudre tout problème de comportement anormal. Ces notes sont rédigées en ayant la bibliothèque de tests unitaires des règles de sécurité à l'esprit, mais les approches générales s'appliquent à tous les SDK Firebase.

Comportement de test incohérent

Si vos tests réussissent et échouent occasionnellement, même si vous n'avez pas modifié les 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. Par conséquent, vérifiez que tout le code asynchrone est correctement séquencé. Vous pouvez corriger le séquencement en enchaînant les promesses ou en utilisant généreusement la notation await.

Examinez notamment les opérations asynchrones suivantes :

  • Définition de règles de sécurité, avec par exemple initializeTestEnvironment
  • Lecture et écriture de données, avec par exemple db.collection("users").doc("alice").get()
  • Assertions opérationnelles, y compris assertSucceeds et assertFails

Les tests ne s'affichent que la première fois que vous chargez l'émulateur.

L'émulateur est un programme avec état. Il stocke toutes les données écrites en mémoire, entraînant la perte de toutes les données à chaque arrêt de l'émulateur. Si vous exécutez plusieurs tests sur le même ID de projet, chaque test peut produire des données susceptibles d'influencer les tests ultérieurs. Pour contourner ce problème, vous pouvez utiliser l'une des méthodes suivantes :

  • Utilisez des ID de projet uniques pour chaque test. Notez que si vous optez pour cette méthode, vous devez appeler initializeTestEnvironment dans le cadre de chaque test. Les règles ne sont chargées automatiquement que pour l'ID de projet par défaut.
  • Restructurez vos tests afin qu'ils n'interagissent pas avec les 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.

Configuration du test très complexe

Lors de la configuration de votre test, vous pouvez modifier des données d'une manière que vos Cloud Firestore Security Rules n'autorisent pas a priori. Si vos règles rendent la configuration de test 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.

Ainsi, votre test peut effectuer des opérations en tant qu'utilisateur authentifié ou non authentifié à l'aide de RulesTestEnvironment.authenticatedContext et de unauthenticatedContext. Cela vous permet de vérifier que votre Cloud Firestore Security Rules autorise / refuse correctement les différents scénarios.