1. Avant de commencer
Les outils backend sans serveur tels que Cloud Firestore et Cloud Functions sont très faciles à utiliser, mais peuvent être difficiles à tester. La suite Firebase Local Emulator vous permet d'exécuter des versions locales de ces services sur votre ordinateur de développement afin que vous puissiez 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 supérieur (pour installer Node.js, utilisez nvm , pour vérifier votre version, exécutez
node --version
) - Java 7 ou supérieur (pour installer Java, utilisez ces instructions , pour vérifier votre version, exécutez
java -version
)
Ce que tu feras
Dans cet atelier de programmation, vous exécuterez et déboguerez une application d'achat en ligne simple alimentée par plusieurs services Firebase :
- Cloud Firestore : une base de données NoSQL sans serveur, évolutive à l'échelle mondiale, dotée de fonctionnalités en temps réel.
- Cloud Functions : un code backend sans serveur qui s'exécute en réponse à des événements ou à des requêtes HTTP.
- Firebase Authentication : un service d'authentification géré qui s'intègre à d'autres produits Firebase.
- Firebase Hosting : hébergement rapide et sécurisé pour les applications web.
Vous connecterez l’application à Emulator Suite pour permettre le développement local.
Vous apprendrez également à :
- Comment connecter votre application à Emulator Suite et comment les différents émulateurs sont connectés.
- Comment fonctionnent les règles de sécurité Firebase et comment tester les règles de sécurité Firestore par rapport à 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 Emulator Suite.
2. Configurer
Obtenez le code source
Dans cet atelier de programmation, vous commencez avec une version de l'exemple The Fire Store qui est presque terminée. 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, où vous travaillerez pour le reste de cet atelier de programmation :
$ cd emulators-codelab/codelab-initial-state
Maintenant, installez les dépendances pour pouvoir exécuter le code. Si votre connexion Internet est plus lente, cela peut prendre une minute ou deux :
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Obtenez la CLI Firebase
L'Emulator Suite fait partie de la Firebase CLI (interface de ligne de commande) qui peut être installée sur votre machine avec la commande suivante :
$ npm install -g firebase-tools
Ensuite, confirmez 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
Connectez-vous à votre projet Firebase
Si vous n'avez pas de projet Firebase, dans la console Firebase , créez un nouveau projet Firebase. Notez l'ID de projet que vous choisissez, vous en aurez besoin plus tard.
Nous devons maintenant connecter ce code à votre projet Firebase. Exécutez d’abord 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écutez les émulateurs
Dans cette section, vous exécuterez l’application localement. Cela signifie qu'il est temps de démarrer Emulator Suite.
Démarrez les émulateurs
Depuis le répertoire source de l'atelier de programmation, exécutez la commande suivante pour démarrer les émulateurs :
$ firebase emulators:start --import=./seed
Vous devriez voir une sortie comme celle-ci :
$ 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 que vous voyez le message Tous les émulateurs démarrés , l'application est prête à être utilisée.
Connectez l'application Web aux émulateurs
Sur la base du tableau des journaux, nous pouvons voir que l'émulateur Cloud Firestore écoute sur le port 8080
et que l'émulateur d'authentification é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 frontend à 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 standards de Firestore et Auth :
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 ordinateur local (servi par l'émulateur d'hébergement), le client Firestore pointe également vers l'émulateur local plutôt que vers une base de données de production.
Ouvrez l'interface utilisateur de l'émulateur
Dans votre navigateur Web, accédez à http://127.0.0.1:4000/ . Vous devriez voir l’interface utilisateur d’Emulator Suite.
Cliquez pour voir l'interface utilisateur 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écutez l'application
Ouvrez l'application
Dans votre navigateur Web, accédez à http://127.0.0.1:5000 et vous devriez voir The Fire Store s'exécuter localement sur votre ordinateur !
Utilisez l'application
Choisissez un article sur la page d'accueil et cliquez sur Ajouter au panier . Malheureusement, vous rencontrerez l'erreur suivante :
Corrigeons ce bug ! Parce que tout fonctionne dans les émulateurs, nous pouvons expérimenter sans nous soucier d’affecter les données réelles.
5. Déboguer l'application
Trouvez le bug
Ok, regardons dans la console du développeur Chrome. Appuyez sur Control+Shift+J
(Windows, Linux, Chrome OS) ou Command+Option+J
(Mac) pour voir l'erreur sur la console :
Il semble qu'il y ait eu une erreur dans la méthode addToCart
, jetons un coup d'œil à cela. Où essayons-nous d'accéder à quelque chose appelé uid
dans cette méthode et pourquoi serait-il null
? À l'heure actuelle, la méthode ressemble à ceci 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);
}
Ah ! Nous ne sommes pas connectés à l'application. Selon la documentation sur l'authentification Firebase , lorsque nous ne sommes pas connectés, auth.currentUser
est null
. Ajoutons un chèque pour cela :
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Testez l'application
Maintenant, actualisez la page, puis cliquez sur Ajouter au panier . Vous devriez obtenir une erreur plus intéressante cette fois :
Mais si vous cliquez sur Connexion dans la barre d'outils supérieure, puis cliquez à nouveau sur Ajouter au panier , vous verrez que le panier est mis à jour.
Cependant, il ne semble pas du tout que les chiffres soient corrects :
Ne vous inquiétez pas, nous corrigerons ce bug bientôt. Tout d’abord, examinons en profondeur ce qui s’est réellement passé lorsque vous avez ajouté un article à votre panier.
6. Déclencheurs de fonctions locales
En cliquant sur Ajouter au panier, vous lancez une chaîne d'événements impliquant plusieurs émulateurs. Dans les journaux Firebase CLI, vous devriez voir quelque chose comme les messages suivants 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 produire ces journaux et la mise à jour de l'interface utilisateur que vous avez observée :
1) Écriture Firestore - Client
Un nouveau document est ajouté à la collection Firestore /carts/{cartId}/items/{itemId}/
. Vous pouvez voir ce code dans la fonction addToCart
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);
}
2) Fonction Cloud déclenchée
La fonction Cloud calculateCart
écoute tous les événements d'écriture (création, mise à jour ou suppression) qui surviennent sur les éléments du panier à l'aide du déclencheur onWrite
, que vous pouvez voir dans functions/index.js
:
fonctions/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 et additionne la quantité totale et le prix, puis elle met à jour le document "panier" avec les nouveaux totaux (voir cartRef.update(...)
ci-dessus).
4) Lecture Firestore - Client
L'interface Web est abonnée pour recevoir des mises à jour sur les modifications apportées au panier. Il obtient une mise à jour en temps réel après que la fonction Cloud ait écrit les nouveaux totaux et mis à jour l'interface utilisateur, 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ésumer
Bon travail! Vous venez de configurer une application entièrement locale qui utilise trois émulateurs Firebase différents pour des tests entièrement locaux.
Mais attendez, il y a plus ! Dans la section suivante, vous apprendrez :
- Comment écrire des tests unitaires qui utilisent les émulateurs Firebase.
- Comment utiliser les émulateurs Firebase pour déboguer vos règles de sécurité.
7. Créez 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 préoccupé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 Emulator est un excellent moyen de prototyper ces règles.
Dans l'éditeur, ouvrez le fichier emulators-codelab/codelab-initial-state/firestore.rules
. Vous verrez que nous avons trois sections principales dans nos règles :
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;
}
}
}
À l’heure actuelle, 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 transitent et que nous ne divulguons aucune information sensible.
Au cours de cet atelier de programmation, conformément au principe du moindre privilège, nous verrouillerons tous les documents et ajouterons progressivement des accès jusqu'à ce que tous les utilisateurs disposent de tous les accès dont ils ont besoin, mais pas plus. Mettons à jour 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écutez les émulateurs et les tests
Démarrez les émulateurs
Sur la ligne de commande, assurez-vous que vous êtes dans emulators-codelab/codelab-initial-state/
. Il se peut que les émulateurs soient toujours exécutés à partir des étapes précédentes. Sinon, redémarrez les émulateurs :
$ firebase emulators:start --import=./seed
Une fois les émulateurs exécutés, vous pouvez exécuter des tests localement sur eux.
Exécutez les tests
Sur la ligne de commande dans un nouvel onglet de terminal depuis le répertoire emulators-codelab/codelab-initial-state/
Accédez d’abord au répertoire des fonctions (nous resterons ici 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 vers le haut de la sortie :
# 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
Nous avons actuellement quatre échecs. Au fur et à mesure que vous créez le fichier de règles, vous pouvez mesurer les progrès en regardant davantage de tests réussir.
9. Accès sécurisé au panier
Les deux premiers échecs sont les tests « panier » qui testent que :
- Les utilisateurs peuvent uniquement créer et mettre à jour leurs propres paniers
- Les utilisateurs ne peuvent lire que leurs propres paniers
fonctions/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 autorisent désormais uniquement l'accès en lecture et en écriture au propriétaire du panier.
Pour vérifier 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 tentée. - Si un projet Firebase utilise Firebase Authentication , l'objet
request.auth
décrit l'utilisateur qui fait 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é. Vous pouvez confirmer que l'émulateur a mis à jour les règles en recherchant dans l'onglet exécutant l'émulateur le message Rules updated
:
Réexécutez les tests et vérifiez que les deux premiers tests réussissent désormais :
$ 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
Bon travail! Vous avez désormais un accès sécurisé aux paniers. Passons au prochain test d'échec.
11. Vérifiez le flux « Ajouter au panier » dans l'interface utilisateur
À l'heure actuelle, bien que les propriétaires de panier lisent et écrivent dans leur panier, ils ne peuvent pas lire ou écrire des articles individuels dans leur panier. En effet, même 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 quelque chose à votre panier. Vous obtenez une erreur Permission Denied
, visible depuis 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 ou lire des articles que depuis leur propre panier :
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'utilisateur actuel a le même UID que le propriétaireUID sur le document de panier. Puisqu'il n'est pas nécessaire de spécifier des règles différentes pour create, update, delete
, vous pouvez utiliser une règle write
, qui s'applique à toutes les requêtes modifiant des données.
Mettez à jour la règle pour les documents de la sous-collection d'éléments. Le get
au conditionnel 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 refaire le test. Faites défiler vers le haut de la sortie et vérifiez que d'autres 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
Bon! Maintenant, tous nos tests réussissent. Nous avons un test en attente, mais nous y reviendrons en quelques étapes.
14. Vérifiez à nouveau le flux « Ajouter au panier »
Revenez au frontal Web ( http://127.0.0.1:5000 ) et ajoutez un article au panier. Il s'agit d'une étape importante pour confirmer que nos tests et règles correspondent aux fonctionnalités requises par le client. (Rappelez-vous que la dernière fois que nous avons essayé, les utilisateurs de l'interface utilisateur n'ont pas pu ajouter d'articles à leur panier !)
Le client recharge automatiquement les règles lorsque le firestore.rules
est enregistré. Alors, essayez d'ajouter quelque chose au panier.
résumer
Bon travail! 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 donnerait l’assurance qu’à l’avenir, les données de notre panier bénéficieront de ces contrôles d’accès, même si d’autres modifient les règles.
Mais attendez, il y a plus !
si vous continuez, vous apprendrez :
- Comment écrire une fonction déclenchée par un événement Firestore
- Comment créer des tests qui fonctionnent sur plusieurs émulateurs
15. Configurer les tests Cloud Functions
Jusqu'à présent, nous nous sommes concentrés sur l'interface de notre application Web et sur les règles de sécurité Firestore. Mais cette application utilise également Cloud Functions pour maintenir le panier de l'utilisateur à jour, nous souhaitons donc également tester ce code.
La suite Emulator facilite grandement le test des fonctions Cloud, même des fonctions qui utilisent Cloud Firestore et d'autres services.
Dans l'éditeur, ouvrez le fichier emulators-codelab/codelab-initial-state/functions/test.js
et faites défiler jusqu'au dernier test du fichier. 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
, cela ressemblera à ceci :
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 votre véritable ID de projet Firebase. :
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Si vous avez oublié votre ID de projet, vous pouvez trouver votre ID de projet Firebase dans les paramètres du projet dans la console Firebase :
16. Parcourez les tests de fonctions
Étant donné que ce test valide l'interaction entre Cloud Firestore et Cloud Functions, il implique plus de configuration que les tests des ateliers de programmation précédents. Passons en revue ce test et ayons une idée de ce à quoi il s'attend.
Créer un panier
Cloud Functions s'exécute dans un environnement de serveur de confiance et peut utiliser l'authentification du compte de service utilisée par le SDK Admin. Tout d’abord, vous initialisez une application à l’aide initializeAdminApp
au lieu de initializeApp
. Ensuite, vous créez un 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 panier afin de déclencher la fonction. Ajoutez deux éléments pour vous assurer que vous testez 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 des tests
Utilisez onSnapshot()
pour enregistrer un auditeur pour toute modification apportée au document du panier. onSnapshot()
renvoie une fonction que vous pouvez appeler pour désenregistrer l'écouteur.
Pour ce test, ajoutez deux articles qui coûtent ensemble 9,98 $. Ensuite, vérifiez si le panier contient les itemCount
et totalPrice
attendus. Si tel est le cas, alors 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écutez les tests
Il se peut que les émulateurs exécutés à partir des tests précédents soient toujours en cours d'exécution. Sinon, 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 en cours d'exécution) et accédez au répertoire des fonctions. Il se peut que cela soit toujours ouvert à partir des tests des règles de sécurité.
$ cd functions
Exécutez maintenant les tests unitaires, vous devriez voir 5 tests au total :
$ 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 qu'il s'agisse d'une erreur de délai d'attente. En effet, le test attend que la fonction soit correctement mise à jour, mais il ne le fait jamais. Nous sommes maintenant prêts à écrire la fonction pour satisfaire le test.
18. Écrivez 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 met à jour avec celles codées en dur.
Récupérer et parcourir le
sous-collection items
Initialisez une nouvelle constante, itemsSnap
, pour qu'elle soit la sous-collection items
. Ensuite, parcourez 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 le prix total et le nombre d'articles
Tout d’abord, initialisons les valeurs de totalPrice
et itemCount
à zéro.
Ensuite, ajoutez la logique à notre bloc d'itération. Tout d’abord, vérifiez que l’article a un prix. Si l'article n'a pas de quantité spécifiée, laissez-la par défaut 1
. Ensuite, ajoutez 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 une journalisation pour faciliter le débogage des é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 réexécutez les tests. Vous n'avez pas besoin de redémarrer les émulateurs car ils récupèrent automatiquement les modifications apportées aux fonctions. Vous devriez voir tous les tests 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)
Bon travail!
20. Essayez-le en utilisant l'interface utilisateur de Storefront
Pour le test final, revenez à l'application Web ( http://127.0.0.1:5000/ ) et ajoutez un article au panier.
Confirmez que le panier est mis à jour avec le total correct. Fantastique!
résumer
Vous avez parcouru un cas de test complexe entre Cloud Functions pour Firebase et Cloud Firestore. Vous avez écrit une fonction Cloud pour réussir le test. Vous avez également confirmé que la nouvelle fonctionnalité fonctionne dans l'interface utilisateur ! Vous avez fait tout cela 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, personnalisé des règles de sécurité pour protéger les données et testé les règles de sécurité à l'aide des émulateurs locaux.