1. Avant de commencer
Les outils de backend sans serveur tels que Cloud Firestore et Cloud Functions sont très faciles à utiliser, mais peuvent être difficiles à tester. La suite d'émulateurs locaux Firebase vous permet d'exécuter des versions locales de ces services sur votre machine de développement afin de développer votre application rapidement et en toute sécurité.
Conditions préalables
- Un éditeur simple tel que Visual Studio Code, Atom ou Sublime Text
- Node.js 10.0.0 ou version ultérieure (pour installer Node.js, utilisez nvm ; pour vérifier votre version, exécutez
node --version
) - Java 7 ou version ultérieure (pour installer Java, suivez ces instructions ; pour vérifier votre version, exécutez
java -version
)
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez exécuter et déboguer une application d'achat en ligne simple qui utilise plusieurs services Firebase :
- Cloud Firestore : une base de données NoSQL sans serveur, évolutive à l'échelle mondiale et offrant des fonctionnalités en temps réel.
- Cloud Functions : code de backend sans serveur qui s'exécute en réponse à des événements ou des requêtes HTTP.
- Firebase Authentication : service d'authentification géré qui s'intègre aux autres produits Firebase.
- Firebase Hosting : hébergement rapide et sécurisé pour les applications Web.
Vous allez connecter l'application à Emulator Suite pour activer le développement local.
Vous apprendrez également à :
- Comment connecter votre application à Emulator Suite et comment les différents émulateurs sont connectés.
- Fonctionnement des règles de sécurité Firebase et test des règles de sécurité Firestore avec un émulateur local.
- Comment écrire une fonction Firebase déclenchée par des événements Firestore et comment écrire des tests d'intégration qui s'exécutent sur la suite d'émulateurs.
2. Configurer
Obtenir le code source
Dans cet atelier de programmation, vous allez commencer avec une version presque complète de l'exemple The Fire Store. La première chose à faire est donc de cloner le code source :
$ git clone https://github.com/firebase/emulators-codelab.git
Accédez ensuite au répertoire de l'atelier de programmation, dans lequel vous travaillerez pour le reste de cet atelier :
$ cd emulators-codelab/codelab-initial-state
Installez maintenant les dépendances pour pouvoir exécuter le code. Si votre connexion Internet est lente, cette opération peut prendre une ou deux minutes :
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Obtenir la CLI Firebase
La suite d'émulateurs fait partie de la CLI (interface de ligne de commande) Firebase, qui peut être installée sur votre machine à l'aide de la commande suivante :
$ npm install -g firebase-tools
Ensuite, vérifiez que vous disposez de la dernière version de la CLI. Cet atelier de programmation devrait fonctionner avec la version 9.0.0 ou ultérieure, mais les versions ultérieures incluent davantage de corrections de bugs.
$ firebase --version 9.6.0
Se connecter à votre projet Firebase
Créer un projet Firebase
- Connectez-vous à la console Firebase à l'aide de votre compte Google.
- Cliquez sur le bouton pour créer un projet, puis saisissez un nom de projet (par exemple,
Emulators Codelab
).
- Cliquez sur Continuer.
- Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase, puis cliquez sur Continuer.
- (Facultatif) Activez l'assistance IA dans la console Firebase (appelée "Gemini dans Firebase").
- Pour cet atelier de programmation, vous n'avez pas besoin de Google Analytics. Désactivez donc l'option Google Analytics.
- Cliquez sur Créer un projet, attendez que votre projet soit provisionné, puis cliquez sur Continuer.
Connecter votre code à votre projet Firebase
Nous devons maintenant associer ce code à votre projet Firebase. Commencez par exécuter la commande suivante pour vous connecter à la CLI Firebase :
$ firebase login
Exécutez ensuite la commande suivante pour créer un alias de projet. Remplacez $YOUR_PROJECT_ID
par l'ID de votre projet Firebase.
$ firebase use $YOUR_PROJECT_ID
Vous êtes maintenant prêt à exécuter l'application.
3. Exécuter les émulateurs
Dans cette section, vous allez exécuter l'application en local. Il est donc temps de démarrer l'Emulator Suite.
Démarrer les émulateurs
Depuis le répertoire source du codelab, exécutez la commande suivante pour démarrer les émulateurs :
$ firebase emulators:start --import=./seed
Le résultat devrait ressembler à ceci :
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
Une fois le message Tous les émulateurs ont démarré affiché, l'application est prête à être utilisée.
Connecter l'application Web aux émulateurs
D'après le tableau des journaux, l'émulateur Cloud Firestore écoute sur le port 8080
et l'émulateur Authentication écoute sur le port 9099
.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
Connectons votre code d'interface à l'émulateur plutôt qu'à la production. Ouvrez le fichier public/js/homepage.js
et recherchez la fonction onDocumentReady
. Nous pouvons voir que le code accède aux instances Firestore et Auth standards :
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Mettons à jour les objets db
et auth
pour qu'ils pointent vers les émulateurs locaux :
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
Désormais, lorsque l'application s'exécute sur votre machine locale (servie par l'émulateur Hosting), le client Firestore pointe également vers l'émulateur local plutôt que vers une base de données de production.
Ouvrir l'interface utilisateur de l'émulateur
Dans votre navigateur Web, accédez à http://127.0.0.1:4000/. L'interface utilisateur de la suite d'émulateurs devrait s'afficher.
Cliquez pour afficher l'UI de l'émulateur Firestore. La collection items
contient déjà des données en raison des données importées avec l'indicateur --import
.
4. Exécuter l'application
Ouvrir l'appli
Dans votre navigateur Web, accédez à http://127.0.0.1:5000. Vous devriez voir The Fire Store s'exécuter localement sur votre ordinateur.
Utiliser l'application
Sélectionnez un article sur la page d'accueil, puis cliquez sur Ajouter au panier. Malheureusement, vous rencontrerez l'erreur suivante :
Corrigeons ce bug ! Comme tout s'exécute dans les émulateurs, nous pouvons faire des tests sans nous soucier d'affecter les données réelles.
5. Déboguer l'application
Trouver le bug
OK, examinons la console pour les développeurs Chrome. Appuyez sur Control+Shift+J
(Windows, Linux, ChromeOS) ou Command+Option+J
(Mac) pour afficher l'erreur dans la console :
Il semble qu'une erreur se soit produite dans la méthode addToCart
. Examinons cela. Où essayons-nous d'accéder à un élément appelé uid
dans cette méthode et pourquoi serait-il null
? Pour l'instant, la méthode se présente comme suit dans public/js/homepage.js
:
public/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
Eurêka Nous ne sommes pas connectés à l'application. Selon la documentation Firebase Authentication, lorsque nous ne sommes pas connectés, auth.currentUser
est null
. Ajoutons une vérification :
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Tester l'application
Maintenant, actualisez la page, puis cliquez sur Ajouter au panier. Cette fois, vous devriez obtenir une erreur plus claire :
Toutefois, si vous cliquez sur Se connecter dans la barre d'outils supérieure, puis à nouveau sur Ajouter au panier, vous verrez que le panier est mis à jour.
Toutefois, il semble que les chiffres ne soient pas du tout corrects :
Ne vous inquiétez pas, nous allons corriger ce bug rapidement. Commençons par examiner en détail ce qui s'est passé lorsque vous avez ajouté un article à votre panier.
6. Déclencheurs de fonctions locales
Lorsque l'utilisateur clique sur Ajouter au panier, une chaîne d'événements impliquant plusieurs émulateurs se déclenche. Dans les journaux de la CLI Firebase, vous devriez voir des messages semblables à ceux ci-dessous après avoir ajouté un article à votre panier :
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Quatre événements clés se sont produits pour générer ces journaux et la mise à jour de l'UI que vous avez observée :
1) Écriture Firestore – Client
Un document est ajouté à la collection Firestore /carts/{cartId}/items/{itemId}/
. Vous pouvez voir ce code dans la fonction addToCart
à l'intérieur de public/js/homepage.js
:
public/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2) Déclenchement de la fonction Cloud
La fonction Cloud calculateCart
écoute tous les événements d'écriture (création, mise à jour ou suppression) qui se produisent sur les articles du panier à l'aide du déclencheur onWrite
, que vous pouvez voir dans functions/index.js
:
functions/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3) Écriture Firestore – Administrateur
La fonction calculateCart
lit tous les articles du panier, additionne la quantité et le prix totaux, puis met à jour le document "cart" avec les nouveaux totaux (voir cartRef.update(...)
ci-dessus).
4) Lecture Firestore – Client
L'interface Web est abonnée pour recevoir des informations sur les modifications apportées au panier. Il reçoit une mise à jour en temps réel après que la fonction Cloud a écrit les nouveaux totaux et mis à jour l'UI, comme vous pouvez le voir dans public/js/homepage.js
:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Résumé
Bravo ! Vous venez de configurer une application entièrement locale qui utilise trois émulateurs Firebase différents pour les tests entièrement locaux.
Et ce n'est pas tout ! Dans la section suivante, vous allez découvrir :
- Écrire des tests unitaires qui utilisent les émulateurs Firebase
- Découvrez comment utiliser les émulateurs Firebase pour déboguer vos règles de sécurité.
7. Créer des règles de sécurité adaptées à votre application
Notre application Web lit et écrit des données, mais jusqu'à présent, nous ne nous sommes pas vraiment souciés de la sécurité. Cloud Firestore utilise un système appelé "Règles de sécurité" pour déclarer qui a accès aux données en lecture et en écriture. La suite d'émulateurs est un excellent moyen de prototyper ces règles.
Dans l'éditeur, ouvrez le fichier emulators-codelab/codelab-initial-state/firestore.rules
. Vous remarquerez que nos règles comportent trois sections principales :
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
Pour le moment, n'importe qui peut lire et écrire des données dans notre base de données. Nous voulons nous assurer que seules les opérations valides sont effectuées et qu'aucune information sensible n'est divulguée.
Dans cet atelier de programmation, nous allons suivre le principe du moindre privilège. Nous allons donc verrouiller tous les documents et ajouter progressivement des accès jusqu'à ce que tous les utilisateurs disposent de tous les accès dont ils ont besoin, mais pas plus. Modifions les deux premières règles pour refuser l'accès en définissant la condition sur false
:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. Exécuter les émulateurs et les tests
Démarrer les émulateurs
Sur la ligne de commande, assurez-vous d'être dans emulators-codelab/codelab-initial-state/
. Il est possible que les émulateurs soient toujours en cours d'exécution depuis les étapes précédentes. Si ce n'est pas le cas, redémarrez les émulateurs :
$ firebase emulators:start --import=./seed
Une fois les émulateurs en cours d'exécution, vous pouvez exécuter des tests localement sur ceux-ci.
Exécuter les tests
Sur la ligne de commande dans un nouvel onglet de terminal à partir du répertoire emulators-codelab/codelab-initial-state/
Commencez par accéder au répertoire des fonctions (nous y resterons pour le reste de l'atelier de programmation) :
$ cd functions
Exécutez maintenant les tests Mocha dans le répertoire des fonctions et faites défiler le résultat jusqu'en haut :
# Run the tests $ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts 1) can be created and updated by the cart owner 2) can be read only by the cart owner shopping cart items 3) can be read only by the cart owner 4) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 0 passing (364ms) 1 pending 4 failing
Pour le moment, nous avons quatre échecs. À mesure que vous créez le fichier de règles, vous pouvez mesurer votre progression en observant le nombre de tests réussis.
9. Accès sécurisé au panier
Les deux premiers échecs concernent les tests du "panier", qui vérifient les points suivants :
- Les utilisateurs ne peuvent créer et modifier que leurs propres paniers.
- Les utilisateurs ne peuvent lire que leurs propres paniers
functions/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
Faisons en sorte que ces tests réussissent. Dans l'éditeur, ouvrez le fichier de règles de sécurité, firestore.rules
, et mettez à jour les instructions dans match /carts/{cartID}
:
firestore.rules
rules_version = '2';
service cloud.firestore {
// UPDATE THESE LINES
match /carts/{cartID} {
allow create: if request.auth.uid == request.resource.data.ownerUID;
allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
}
// ...
}
}
Ces règles n'autorisent désormais l'accès en lecture et en écriture qu'au propriétaire du panier.
Pour valider les données entrantes et l'authentification de l'utilisateur, nous utilisons deux objets disponibles dans le contexte de chaque règle :
- L'objet
request
contient des données et des métadonnées sur l'opération en cours. - Si un projet Firebase utilise Firebase Authentication, l'objet
request.auth
décrit l'utilisateur qui effectue la demande.
10. Tester l'accès au panier
La suite Emulator met automatiquement à jour les règles chaque fois que firestore.rules
est enregistré. Pour vérifier que l'émulateur a mis à jour les règles, recherchez le message Rules updated
dans l'onglet exécutant l'émulateur :
Exécutez à nouveau les tests et vérifiez que les deux premiers sont désormais réussis :
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items 1) can be read only by the cart owner 2) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 2 passing (482ms) 1 pending 2 failing
Bravo ! Vous avez désormais sécurisé l'accès aux paniers. Passons au prochain test ayant échoué.
11. Vérifier le flux "Ajouter au panier" dans l'UI
Pour le moment, bien que les propriétaires de paniers puissent lire et écrire dans leur panier, ils ne peuvent pas lire ni écrire des éléments individuels dans leur panier. En effet, si les propriétaires ont accès au document du panier, ils n'ont pas accès à la sous-collection d'articles du panier.
Il s'agit d'un état défectueux pour les utilisateurs.
Revenez à l'interface utilisateur Web, qui s'exécute sur http://127.0.0.1:5000,
, et essayez d'ajouter un article à votre panier. Vous obtenez une erreur Permission Denied
, visible dans la console de débogage, car nous n'avons pas encore accordé aux utilisateurs l'accès aux documents créés dans la sous-collection items
.
12. Autoriser l'accès aux articles du panier
Ces deux tests confirment que les utilisateurs ne peuvent ajouter des articles à leur propre panier ni en lire que les articles qui s'y trouvent :
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
Nous pouvons donc écrire une règle qui autorise l'accès si l'UID de l'utilisateur actuel est identique à l'ownerUID du document de panier. Comme il n'est pas nécessaire de spécifier différentes règles pour create, update, delete
, vous pouvez utiliser une règle write
, qui s'applique à toutes les requêtes qui modifient des données.
Mettez à jour la règle pour les documents de la sous-collection "items". La valeur get
dans la condition lit une valeur de Firestore (dans ce cas, ownerUID
sur le document du panier).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13. Tester l'accès aux articles du panier
Nous pouvons maintenant relancer le test. Faites défiler le résultat jusqu'en haut et vérifiez que davantage de tests réussissent :
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items ✓ can be read only by the cart owner (111ms) ✓ can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 4 passing (401ms) 1 pending
Pratique ! Tous nos tests réussissent désormais. Un test est en attente, mais nous y reviendrons dans quelques étapes.
14. Vérifier à nouveau le parcours d'ajout au panier
Revenez à l'interface Web ( http://127.0.0.1:5000) et ajoutez un article au panier. Cette étape est importante pour confirmer que nos tests et nos règles correspondent aux fonctionnalités requises par le client. (Pour rappel, la dernière fois que nous avons testé l'UI, les utilisateurs n'ont pas pu ajouter d'articles à leur panier.)
Le client recharge automatiquement les règles lorsque le firestore.rules
est enregistré. Essayez donc d'ajouter un article au panier.
Résumé
Bravo ! Vous venez d'améliorer la sécurité de votre application, une étape essentielle pour la préparer à la production. S'il s'agissait d'une application de production, nous pourrions ajouter ces tests à notre pipeline d'intégration continue. Cela nous permettra d'être sûrs que nos données de panier d'achat disposeront de ces contrôles d'accès à l'avenir, même si d'autres personnes modifient les règles.
Mais ce n'est pas tout !
Si vous continuez, vous apprendrez :
- Écrire une fonction déclenchée par un événement Firestore
- Créer des tests fonctionnant sur plusieurs émulateurs
15. Configurer des tests Cloud Functions
Jusqu'à présent, nous nous sommes concentrés sur le frontend de notre application Web et sur les règles de sécurité Firestore. Toutefois, cette application utilise également Cloud Functions pour maintenir le panier de l'utilisateur à jour. Nous souhaitons donc également tester ce code.
La suite d'émulateurs facilite le test des fonctions Cloud, y compris celles qui utilisent Cloud Firestore et d'autres services.
Dans l'éditeur, ouvrez le fichier emulators-codelab/codelab-initial-state/functions/test.js
et faites-le défiler jusqu'au dernier test. Pour le moment, il est marqué comme "En attente" :
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Pour activer le test, supprimez .skip
afin que la ligne ressemble à l'exemple ci-dessous :
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Ensuite, recherchez la variable REAL_FIREBASE_PROJECT_ID
en haut du fichier et remplacez-la par l'ID de votre projet Firebase :
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Si vous avez oublié l'ID de votre projet, vous pouvez le retrouver dans les paramètres du projet de la console Firebase :
16. Parcourir les tests Functions
Étant donné que ce test valide l'interaction entre Cloud Firestore et Cloud Functions, il nécessite plus de configuration que les tests des ateliers de programmation précédents. Passons en revue ce test pour nous faire une idée de ce qu'il attend.
Créer un panier
Cloud Functions s'exécute dans un environnement de serveur de confiance et peut utiliser l'authentification par compte de service utilisée par le SDK Admin . Tout d'abord, vous initialisez une application à l'aide de initializeAdminApp
au lieu de initializeApp
. Créez ensuite une DocumentReference pour le panier auquel nous allons ajouter des articles et initialisez le panier :
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
Déclencher la fonction
Ensuite, ajoutez des documents à la sous-collection items
de notre document de panier pour déclencher la fonction. Ajoutez deux éléments pour vous assurer de tester l'ajout qui se produit dans la fonction.
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
Définir les attentes concernant le test
Utilisez onSnapshot()
pour enregistrer un écouteur pour toute modification apportée au document du panier. onSnapshot()
renvoie une fonction que vous pouvez appeler pour annuler l'enregistrement de l'écouteur.
Pour ce test, ajoutez deux articles dont le prix total est de 9,98 $. Vérifiez ensuite si le panier contient les itemCount
et totalPrice
attendus. Si c'est le cas, la fonction a fait son travail.
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. Exécuter les tests
Il est possible que les émulateurs des tests précédents soient toujours en cours d'exécution. Si ce n'est pas le cas, démarrez les émulateurs. Depuis la ligne de commande, exécutez
$ firebase emulators:start --import=./seed
Ouvrez un nouvel onglet de terminal (laissez les émulateurs s'exécuter) et accédez au répertoire des fonctions. Vous l'avez peut-être déjà ouvert lors des tests des règles de sécurité.
$ cd functions
Exécutez maintenant les tests unitaires. Vous devriez voir un total de cinq tests :
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (82ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (42ms) shopping cart items ✓ items can be read by the cart owner (40ms) ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. 1) should sum the cost of their items 4 passing (2s) 1 failing
Si vous examinez l'échec spécifique, il semble s'agir d'une erreur de délai d'attente. En effet, le test attend que la fonction se mette à jour correctement, mais cela ne se produit jamais. Nous sommes maintenant prêts à écrire la fonction pour satisfaire le test.
18. Écrire une fonction
Pour corriger ce test, vous devez mettre à jour la fonction dans functions/index.js
. Bien qu'une partie de cette fonction soit écrite, elle n'est pas complète. Voici à quoi ressemble la fonction actuellement :
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
La fonction définit correctement la référence du panier, mais au lieu de calculer les valeurs de totalPrice
et itemCount
, elle les remplace par des valeurs codées en dur.
Récupérez et itérez sur le
items
subcollection
Initialisez une nouvelle constante, itemsSnap
, pour qu'elle soit la sous-collection items
. Parcourez ensuite tous les documents de la collection.
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
Calculer totalPrice et itemCount
Commençons par initialiser les valeurs de totalPrice
et itemCount
à zéro.
Ajoutez ensuite la logique à notre bloc d'itération. Tout d'abord, vérifiez que l'article a un prix. Si aucune quantité n'est spécifiée pour l'article, laissez la valeur par défaut 1
. Ajoutez ensuite la quantité au total cumulé de itemCount
. Enfin, ajoutez le prix de l'article multiplié par la quantité au total cumulé de totalPrice
:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
Vous pouvez également ajouter la journalisation pour déboguer les états de réussite et d'erreur :
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. Réexécuter les tests
Sur la ligne de commande, assurez-vous que les émulateurs sont toujours en cours d'exécution et relancez les tests. Vous n'avez pas besoin de redémarrer les émulateurs, car ils détectent automatiquement les modifications apportées aux fonctions. Tous les tests devraient réussir :
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (306ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (59ms) shopping cart items ✓ items can be read by the cart owner ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. ✓ should sum the cost of their items (800ms) 5 passing (1s)
Bravo !
20. Essayer avec l'interface utilisateur Storefront
Pour le test final, revenez à l'application Web ( http://127.0.0.1:5000/) et ajoutez un article au panier.
Vérifiez que le panier est mis à jour avec le bon total. Fantastique !
Résumé
Vous avez parcouru un cas de test complexe entre Cloud Functions pour Firebase et Cloud Firestore. Vous avez écrit une fonction Cloud pour que le test réussisse. Vous avez également confirmé que la nouvelle fonctionnalité fonctionnait dans l'UI. Vous avez effectué toutes ces opérations localement, en exécutant les émulateurs sur votre propre machine.
Vous avez également créé un client Web qui s'exécute sur les émulateurs locaux, adapté les règles de sécurité pour protéger les données et testé les règles de sécurité à l'aide des émulateurs locaux.