Développement local avec Firebase Emulator Suite

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. Firebase Local Emulator Suite vous permet d'exécuter des versions locales de ces services sur votre machine 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, utiliser NVM , pour vérifier votre version, exécutez node --version )
  • Java 7 ou plus (pour installer Java utiliser ces instructions , pour vérifier votre version, exécutez java -version )

Ce que tu vas faire

Dans cet atelier de programmation, vous allez exécuter et déboguer une application d'achat en ligne simple qui est alimentée par plusieurs services Firebase :

  • Cloud Firestore: a, globalement évolutive Serverless, base de données NoSQL avec des capacités en temps réel.
  • Fonctions Cloud: un code back - end serverless qui fonctionne en réponse à des événements ou des demandes HTTP.
  • Firebase authentification: un service d'authentification géré intégré avec d' autres produits Firebase.
  • Hébergement Firebase: rapide et sécurisé pour les applications d' hébergement web.

Vous connecterez l'application à Emulator Suite pour permettre le développement local.

2589e2f95b74fa88.png

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

Ensuite, déplacez-vous dans le répertoire codelab, où vous travaillerez pour le reste de ce codelab :

$ 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 l'interface de ligne de commande Firebase

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, vérifiez que vous disposez de la dernière version de la CLI. Ce laboratoire de programmation devrait fonctionner avec la version 9.0.0 ou supérieure, mais les versions ultérieures incluent davantage de corrections de bogues.

$ firebase --version
9.6.0

Connectez-vous à votre projet Firebase

Si vous ne disposez pas d' un 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. Remplacer $YOUR_PROJECT_ID avec 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é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

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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost: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 les émulateurs Tout a commencé message, l'application est prête à l' emploi.

Connectez l'application Web aux émulateurs

Sur la base de la table dans les journaux , nous pouvons voir que le l'émulateur Nuage Firestore écoute sur le port 8080 et l'émulateur d' authentification est à l' écoute sur le port 9099 .

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Connectons votre code frontend à l'émulateur, plutôt qu'à la production. Ouvrez le public/js/homepage.js fichier et trouver la onDocumentReady fonction. Nous pouvons voir que le code accède aux instances Firestore et Auth standard :

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

La mise à jour de laisser les db et auth objets pour pointer vers les émulateurs locales:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

  // ADD THESE LINES
  if (location.hostname === "localhost") {
    console.log("localhost detected!");
    auth.useEmulator("http://localhost:9099");
    db.useEmulator("localhost", 8080);
  }

Désormais, lorsque l'application s'exécute sur localhost (servi par l'émulateur Hosting), 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: // localhost: 4000 / . Vous devriez voir l'interface utilisateur de Emulator Suite.

Écran d'accueil de l'interface utilisateur des émulateurs

Cliquez pour voir l'interface utilisateur de l'émulateur Firestore. Les items collection contient déjà des données en raison des données importées avec le --import drapeau.

4ef88d0148405d36.png

4. Exécutez l'application

Ouvrez l'application

Dans votre navigateur Web, accédez à http: // localhost: 5000 et vous devriez voir le magasin feu roulant localement sur votre machine!

939f87946bac2ee4.png

Utiliser l'application

Choisissez un élément sur la page d' accueil et cliquez sur Ajouter au panier. Malheureusement, vous rencontrerez l'erreur suivante :

a11bd59933a8e885.png

Corrigons ce bug ! Parce que tout fonctionne dans les émulateurs, nous pouvons expérimenter et ne pas nous soucier d'affecter les données réelles.

5. Déboguer l'application

Trouver le bogue

Ok, regardons dans la console développeur Chrome. Appuyez sur Control+Shift+J (Windows, Linux, Chrome OS) ou Command+Option+J (Mac) pour voir l'erreur sur la console:

74c45df55291dab1.png

Il semble qu'il y avait une erreur dans la addToCart méthode, nous allons jeter un coup d' oeil. Où allons-nous essayer quelque chose d'accès appelé uid dans cette méthode et pourquoi serait - il null ? En ce moment , les regards de méthode comme celle - ci 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 les documents d' auth.currentUser null authentification Firebase , quand nous ne sommes pas connecté, auth.currentUser est null . Ajoutons une vérification 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;
    }

    // ...
  }

Tester l'application

Maintenant, rafraîchir la page puis cliquez sur Ajouter au panier. Vous devriez obtenir une erreur plus agréable cette fois :

c65f6c05588133f7.png

Mais si vous cliquez sur Connexion dans la barre d' outils supérieure, puis cliquez sur Ajouter au panier nouveau, vous verrez que le panier est mis à jour.

Cependant, il semble que les chiffres ne soient pas du tout corrects :

239f26f02f959eef.png

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

Cliquant sur Ajouter aux coups de pieds Panier hors d' une chaîne d'événements qui impliquent de multiples émulateurs. Dans les journaux de la CLI Firebase, 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 :

68c9323f2ad10f7a.png

1) Firestore Write - Client

Un nouveau document est ajouté à la collection Firestore /carts/{cartId}/items/{itemId}/ . Vous pouvez voir ce code dans la addToCart fonction à l' intérieur du 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 écoutes pour les événements d'écriture (création, mise à jour ou supprimer) qui se produisent à des éléments de panier en utilisant le onWrite déclencheur, que vous pouvez voir dans les 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) Firestore Write - Administrateur

La calculateCart fonction lit tous les articles dans le panier et ajoute la quantité totale et le prix, il 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 la fonction Cloud écrit les nouveaux totaux et met à 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.

db82eef1706c9058.gif

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 du tout inquiété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. Emulator Suite 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 passent et que nous ne divulguons aucune information sensible.

Au cours de cet atelier de programmation, en suivant le principe du moindre privilège, nous verrouillerons tous les documents et ajouterons progressivement l'accès jusqu'à ce que tous les utilisateurs aient tous les accès dont ils ont besoin, mais pas plus. La mise à jour de laisser les deux premières règles de refuser l' accès en définissant la condition 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émarrer les émulateurs

Sur la ligne de commande, assurez - vous que vous êtes dans emulators-codelab/codelab-initial-state/ . Vous pouvez toujours avoir les émulateurs exécutés à partir des étapes précédentes. Sinon, 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 eux.

Exécuter les tests

Sur la ligne de commande dans un nouvel onglet terminal à partir du répertoire emulators-codelab/codelab-initial-state/

Allez d'abord dans le répertoire des fonctions (nous resterons ici pour le reste de l'atelier de programmation) :

$ cd functions

Exécutez maintenant les tests moka dans le répertoire des fonctions et faites défiler jusqu'en 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

En ce moment, nous avons quatre échecs. Au fur et à mesure que vous créez le fichier de règles, vous pouvez mesurer les progrès en observant la réussite d'autres tests.

9. Accès sécurisé au panier

Les deux premiers échecs sont les tests « panier d'achat » 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 passer ces tests. Dans l'éditeur, ouvrez le fichier de règles de sécurité, firestore.rules et mettre à jour les déclarations 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 par le 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 :

10. Tester l'accès au panier

L'émulateur Suite met automatiquement à jour les règles à chaque fois firestore.rules est sauvé. Vous pouvez confirmer que l'émulateur a la mise à jour des règles en regardant dans l'onglet en cours d' exécution de l'émulateur pour le message Rules updated à Rules updated :

5680da418b420226.png

Réexécutez les tests et vérifiez que les deux premiers tests réussissent maintenant :

$ 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 maintenant 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 ni lire ni écrire des articles individuels dans leur panier. En effet , alors que les propriétaires ont accès au document de panier, ils n'ont pas accès à la sous - collection articles du panier.

Il s'agit d'un état cassé pour les utilisateurs.

Retour à l'interface utilisateur Web, qui est en cours d' exécution sur http://localhost:5000, et essayez d'ajouter quelque chose à votre panier. Vous obtenez une Permission Denied erreur, visible depuis la console de débogage, parce que nous n'avons pas encore accordé l' accès des utilisateurs aux documents créés dans les items sous - collection.

12. Autoriser l'accès aux articles du panier

Ces deux tests confirment que les utilisateurs ne peuvent ajouter ou lire des articles qu'à partir de 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 ownerUID sur le document du panier. Comme il n'y a pas besoin de spécifier des règles différentes pour create, update, delete à write create, update, delete , vous pouvez utiliser une write règle, applicable à toutes les demandes qui modifient les données.

Mettez à jour la règle pour les documents dans la sous-collection d'articles. Le get au conditionnel est en train de lire une valeur de Firestore dans ce cas, le ownerUID sur le document de 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 jusqu'en 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

Joli! Maintenant, tous nos tests réussissent. Nous avons un test en attente, mais nous y arriverons en quelques étapes.

14. Vérifiez à nouveau le flux « Ajouter au panier »

Retour à l'extrémité avant Web ( http: // localhost: 5000 ) et ajouter 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 !)

69ad26cee520bf24.png

Le client automatiquement les règles recharge lorsque les firestore.rules sont enregistrées. 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 que nos données de panier d'achat auront ces contrôles d'accès, même si d'autres modifient les règles.

ba5440b193e75967.gif

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 le frontend de notre application Web et 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.

Emulator Suite facilite le test des fonctions Cloud, même des fonctions qui utilisent Cloud Firestore et d'autres services.

Dans l'éditeur, ouvrez les emulators-codelab/codelab-initial-state/functions/test.js fichier et faites défiler jusqu'au dernier test dans le 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, retirez .skip , il ressemble à ceci:

describe("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

Ensuite, trouver la REAL_FIREBASE_PROJECT_ID variable en haut du fichier et le modifier à votre projet ID réel 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 de la console Firebase :

d6d0429b700d2b21.png

16. Parcourir 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. Parcourons ce test et avons une idée de ce qu'il attend.

Créer un panier

Les fonctions Cloud s'exécutent dans un environnement de serveur de confiance et peuvent 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 , nous ajouterons les articles à votre panier et initialisez:

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 à des items sous - collection de notre document de 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 de test

Utilisez onSnapshot() pour enregistrer un écouteur pour toute modification sur le document de panier. onSnapshot() retourne une fonction que vous pouvez appeler pour désenregistrer l'auditeur.

Pour ce test, ajoutez deux articles qui coûtent ensemble 9,98 $. Ensuite, vérifiez si le panier a prévu itemCount et totalPrice . Si c'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

Les émulateurs des tests précédents peuvent toujours être exécutés. Sinon, lancez les émulateurs. À partir de la ligne de commande, exécutez

$ firebase emulators:start --import=./seed

Ouvrir un nouvel onglet terminal (laisser les émulateurs en cours d' exécution) et se déplacer dans le répertoire des fonctions. Vous pouvez toujours l'avoir ouvert à partir des tests de 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 regardez l'échec spécifique, il semble qu'il s'agisse d'une erreur de délai d'attente. C'est parce que le test attend que la fonction se mette à jour correctement, mais il ne le fait jamais. Maintenant, nous sommes prêts à écrire la fonction pour satisfaire le test.

18. Écrire une fonction

Pour résoudre ce test, vous devez mettre à jour la fonction dans les 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 est mise correctement la référence du chariot, mais au lieu de calculer les valeurs de totalPrice et itemCount , il les met à jour les hardcoded.

Récupérer et parcourir le

items subcollection

Initialiser une nouvelle constante, itemsSnap , être les items sous - collection. 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, nous allons initialiser 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 ne dispose pas d' une quantité spécifiée, laissez - le par défaut à 1 . Ensuite, ajoutez la quantité au total en cours d' exécution de itemCount . Enfin, ajouter le prix de l'article multiplié par la quantité au total en cours d' exécution 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 aider au 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. Refaire 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 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, retour à l'application Web ( http: // localhost: 5000 / ) et ajouter un article au panier.

69ad26cee520bf24.png

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 for 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, des règles de sécurité personnalisées pour protéger les données et testé les règles de sécurité à l'aide des émulateurs locaux.

c6a7aeb91fe97a64.gif