Lokale Entwicklung mit der Firebase Emulator Suite

Mit Sammlungen den Überblick behalten Sie können Inhalte basierend auf Ihren Einstellungen speichern und kategorisieren.

1. Bevor Sie beginnen

Serverlose Backend-Tools wie Cloud Firestore und Cloud Functions sind sehr einfach zu verwenden, können aber schwer zu testen sein. Mit der Firebase Local Emulator Suite können Sie lokale Versionen dieser Dienste auf Ihrem Entwicklungscomputer ausführen, damit Sie Ihre App schnell und sicher entwickeln können.

Voraussetzungen

  • Ein einfacher Editor wie Visual Studio Code, Atom oder Sublime Text
  • Node.js 10.0.0 oder höher (um Node.js zu installieren, verwenden Sie nvm , um Ihre Version zu überprüfen, führen Sie node --version aus)
  • Java 7 oder höher (um Java zu installieren, verwenden Sie diese Anweisungen , um Ihre Version zu überprüfen, führen Sie java -version aus)

Was du tun wirst

In diesem Codelab werden Sie eine einfache Online-Shopping-App ausführen und debuggen, die von mehreren Firebase-Diensten unterstützt wird:

  • Cloud Firestore: eine global skalierbare, serverlose NoSQL-Datenbank mit Echtzeitfähigkeiten.
  • Cloud Functions : ein serverloser Back-End-Code, der als Reaktion auf Ereignisse oder HTTP-Anforderungen ausgeführt wird.
  • Firebase-Authentifizierung : ein verwalteter Authentifizierungsdienst, der sich in andere Firebase-Produkte integrieren lässt.
  • Firebase Hosting : schnelles und sicheres Hosting für Web-Apps.

Sie verbinden die App mit der Emulator Suite, um die lokale Entwicklung zu ermöglichen.

2589e2f95b74fa88.png

Außerdem erfahren Sie, wie Sie:

  • Wie Sie Ihre App mit der Emulator Suite verbinden und wie die verschiedenen Emulatoren verbunden sind.
  • Wie Firebase-Sicherheitsregeln funktionieren und wie Firestore-Sicherheitsregeln mit einem lokalen Emulator getestet werden.
  • Wie man eine Firebase-Funktion schreibt, die durch Firestore-Ereignisse ausgelöst wird, und wie man Integrationstests schreibt, die gegen die Emulator Suite ausgeführt werden.

2. Einrichten

Holen Sie sich den Quellcode

In diesem Codelab beginnen Sie mit einer fast vollständigen Version des Beispiels „The Fire Store“, also müssen Sie als Erstes den Quellcode klonen:

$ git clone https://github.com/firebase/emulators-codelab.git

Wechseln Sie dann in das Codelab-Verzeichnis, wo Sie für den Rest dieses Codelabs arbeiten werden:

$ cd emulators-codelab/codelab-initial-state

Installieren Sie jetzt die Abhängigkeiten, damit Sie den Code ausführen können. Wenn Sie eine langsamere Internetverbindung verwenden, kann dies ein oder zwei Minuten dauern:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

Holen Sie sich die Firebase-CLI

Die Emulator Suite ist Teil der Firebase CLI (Befehlszeilenschnittstelle), die mit dem folgenden Befehl auf Ihrem Computer installiert werden kann:

$ npm install -g firebase-tools

Bestätigen Sie als Nächstes, dass Sie über die neueste Version der CLI verfügen. Dieses Codelab sollte mit Version 9.0.0 oder höher funktionieren, aber spätere Versionen enthalten mehr Fehlerbehebungen.

$ firebase --version
9.6.0

Stellen Sie eine Verbindung zu Ihrem Firebase-Projekt her

Wenn Sie kein Firebase-Projekt haben, erstellen Sie in der Firebase-Konsole ein neues Firebase-Projekt. Notieren Sie sich die von Ihnen gewählte Projekt-ID, Sie benötigen sie später.

Jetzt müssen wir diesen Code mit Ihrem Firebase-Projekt verbinden. Führen Sie zunächst den folgenden Befehl aus, um sich bei der Firebase-CLI anzumelden:

$ firebase login

Führen Sie als Nächstes den folgenden Befehl aus, um einen Projektalias zu erstellen. Ersetzen Sie $YOUR_PROJECT_ID durch die ID Ihres Firebase-Projekts.

$ firebase use $YOUR_PROJECT_ID

Jetzt können Sie die App ausführen!

3. Führen Sie die Emulatoren aus

In diesem Abschnitt führen Sie die App lokal aus. Dies bedeutet, dass es an der Zeit ist, die Emulator Suite zu starten.

Starten Sie die Emulatoren

Führen Sie im Codelab-Quellverzeichnis den folgenden Befehl aus, um die Emulatoren zu starten:

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

Sie sollten eine Ausgabe wie diese sehen:

$ 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.

Sobald die Meldung Alle Emulatoren gestartet angezeigt wird, ist die App einsatzbereit.

Verbinden Sie die Web-App mit den Emulatoren

Anhand der Tabelle in den Protokollen können wir sehen, dass der Cloud Firestore-Emulator Port 8080 und der Authentication-Emulator Port 9099 überwacht.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Lassen Sie uns Ihren Front-End-Code mit dem Emulator und nicht mit der Produktion verbinden. Öffnen Sie die Datei public/js/homepage.js und suchen Sie die Funktion onDocumentReady . Wir können sehen, dass der Code auf die standardmäßigen Firestore- und Auth-Instanzen zugreift:

public/js/homepage.js

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

Aktualisieren wir die Objekte db und auth so, dass sie auf die lokalen Emulatoren verweisen:

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);
  }

Wenn die App jetzt auf localhost ausgeführt wird (vom Hosting-Emulator bereitgestellt), zeigt der Firestore-Client auch auf den lokalen Emulator und nicht auf eine Produktionsdatenbank.

Öffnen Sie die EmulatorUI

Navigieren Sie in Ihrem Webbrowser zu http://localhost:4000/ . Sie sollten die Benutzeroberfläche der Emulator Suite sehen.

Startbildschirm der Emulator-Benutzeroberfläche

Klicken Sie hier, um die Benutzeroberfläche für den Firestore-Emulator anzuzeigen. Die items enthält bereits Daten, da die Daten mit dem Flag --import importiert wurden.

4ef88d0148405d36.png

4. Führen Sie die App aus

Öffnen Sie die App

Navigieren Sie in Ihrem Webbrowser zu http://localhost:5000 und Sie sollten sehen, dass The Fire Store lokal auf Ihrem Computer läuft!

939f87946bac2ee4.png

Verwenden Sie die App

Wählen Sie einen Artikel auf der Startseite aus und klicken Sie auf In den Einkaufswagen . Leider werden Sie auf den folgenden Fehler stoßen:

a11bd59933a8e885.png

Lassen Sie uns diesen Fehler beheben! Da alles in den Emulatoren ausgeführt wird, können wir experimentieren und müssen uns keine Gedanken über die Beeinträchtigung realer Daten machen.

5. Debuggen Sie die App

Finden Sie den Fehler

Ok, schauen wir uns die Chrome-Entwicklerkonsole an. Drücken Sie Control+Shift+J (Windows, Linux, Chrome OS) oder Command+Option+J (Mac), um den Fehler auf der Konsole anzuzeigen:

74c45df55291dab1.png

Es scheint, als wäre ein Fehler in der addToCart Methode aufgetreten, schauen wir uns das an. Wo versuchen wir, auf etwas namens uid in dieser Methode zuzugreifen, und warum sollte es null sein? Im Moment sieht die Methode in public/js/homepage.js so aus:

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);
  }

Aha! Wir sind nicht in der App angemeldet. Gemäß den Firebase-Authentifizierungsdokumenten ist auth.currentUser null , wenn wir nicht angemeldet sind. Lassen Sie uns dafür eine Überprüfung hinzufügen:

public/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

Testen Sie die Anwendung

Aktualisieren Sie nun die Seite und klicken Sie dann auf In den Einkaufswagen . Diesmal sollten Sie einen schöneren Fehler erhalten:

c65f6c05588133f7.png

Wenn Sie jedoch in der oberen Symbolleiste auf Anmelden und dann erneut auf In den Einkaufswagen klicken, werden Sie sehen, dass der Einkaufswagen aktualisiert wird.

Es sieht jedoch nicht so aus, als ob die Zahlen überhaupt stimmen:

239f26f02f959eef.png

Keine Sorge, wir werden diesen Fehler bald beheben. Lassen Sie uns zunächst tief in das eintauchen, was tatsächlich passiert ist, als Sie einen Artikel in Ihren Warenkorb gelegt haben.

6. Auslöser lokaler Funktionen

Durch Klicken auf „In den Einkaufswagen“ wird eine Kette von Ereignissen gestartet, an denen mehrere Emulatoren beteiligt sind. In den Firebase-CLI-Protokollen sollten Sie etwa die folgenden Meldungen sehen, nachdem Sie Ihrem Einkaufswagen einen Artikel hinzugefügt haben:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

Es gab vier wichtige Ereignisse, die aufgetreten sind, um diese Protokolle und das von Ihnen beobachtete UI-Update zu erstellen:

68c9323f2ad10f7a.png

1) Firestore Write – Client

Der Firestore-Sammlung /carts/{cartId}/items/{itemId}/ wird ein neues Dokument hinzugefügt. Sie können diesen Code in der Funktion addToCart in public/js/homepage.js sehen:

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) Cloud-Funktion ausgelöst

Die Cloud-Funktion calculateCart lauscht auf alle Schreibereignisse (Erstellen, Aktualisieren oder Löschen), die Artikel im Einkaufswagen betreffen, indem sie den onWrite Trigger verwendet, den Sie in functions/index.js sehen können:

Funktionen/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 – Admin

Die Funktion calculateCart liest alle Artikel im Warenkorb und addiert die Gesamtmenge und den Gesamtpreis, dann aktualisiert sie das "Warenkorb"-Dokument mit den neuen Summen (siehe cartRef.update(...) oben).

4) Firestore-Lesen – Client

Das Web-Frontend ist abonniert, um Updates über Änderungen am Warenkorb zu erhalten. Es wird in Echtzeit aktualisiert, nachdem die Cloud-Funktion die neuen Summen geschrieben und die Benutzeroberfläche aktualisiert hat, wie Sie in public/js/homepage.js sehen können:

public/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

Rekapitulieren

Gute Arbeit! Sie richten einfach eine vollständig lokale App ein, die drei verschiedene Firebase-Emulatoren für vollständig lokale Tests verwendet.

db82eef1706c9058.gif

Aber warte, es gibt noch mehr! Im nächsten Abschnitt erfahren Sie:

  • So schreiben Sie Komponententests, die die Firebase-Emulatoren verwenden.
  • So verwenden Sie die Firebase-Emulatoren zum Debuggen Ihrer Sicherheitsregeln.

7. Erstellen Sie Sicherheitsregeln, die auf Ihre App zugeschnitten sind

Unsere Web-App liest und schreibt Daten, aber bisher haben wir uns überhaupt keine Sorgen um die Sicherheit gemacht. Cloud Firestore verwendet ein System namens „Sicherheitsregeln“, um anzugeben, wer Zugriff auf Lese- und Schreibzugriff auf Daten hat. Die Emulator Suite ist eine großartige Möglichkeit, diese Regeln zu prototypisieren.

Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/firestore.rules . Sie werden sehen, dass unsere Regeln drei Hauptabschnitte haben:

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;
    }
  }
}

Im Moment kann jeder Daten in unsere Datenbank lesen und schreiben! Wir möchten sicherstellen, dass nur gültige Operationen durchkommen und dass wir keine vertraulichen Informationen preisgeben.

Während dieses Codelabs werden wir gemäß dem Prinzip der geringsten Rechte alle Dokumente sperren und schrittweise Zugriff gewähren, bis alle Benutzer den erforderlichen Zugriff haben, aber nicht mehr. Aktualisieren wir die ersten beiden Regeln, um den Zugriff zu verweigern, indem wir die Bedingung auf false setzen:

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. Führen Sie die Emulatoren und Tests aus

Starten Sie die Emulatoren

Stellen Sie in der Befehlszeile sicher, dass Sie sich in emulators-codelab/codelab-initial-state/ befinden. Möglicherweise laufen die Emulatoren aus den vorherigen Schritten noch. Wenn nicht, starten Sie die Emulatoren erneut:

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

Sobald die Emulatoren ausgeführt werden, können Sie lokal Tests mit ihnen durchführen.

Führen Sie die Tests durch

Auf der Kommandozeile in einem neuen Terminal-Tab aus dem Verzeichnis emulators-codelab/codelab-initial-state/

Wechseln Sie zuerst in das Funktionsverzeichnis (wir bleiben für den Rest des Codelabs hier):

$ cd functions

Führen Sie nun die Mocha-Tests im Funktionsverzeichnis aus und scrollen Sie zum Anfang der Ausgabe:

# 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

Im Moment haben wir vier Ausfälle. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie beobachten, wie mehr Tests bestanden werden.

9. Sicherer Warenkorbzugriff

Die ersten beiden Fehler sind die "Einkaufswagen"-Tests, die Folgendes testen:

  • Benutzer können nur ihre eigenen Warenkörbe erstellen und aktualisieren
  • Benutzer können nur ihre eigenen Warenkörbe lesen

Funktionen/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());
  });

Lassen Sie uns diese Tests bestehen. Öffnen Sie im Editor die Sicherheitsregeldatei firestore.rules und aktualisieren Sie die Anweisungen in 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;
    }

    // ...
  }
}

Diese Regeln erlauben jetzt nur noch Lese- und Schreibzugriff durch den Besitzer des Einkaufswagens.

Um eingehende Daten und die Benutzerauthentifizierung zu überprüfen, verwenden wir zwei Objekte, die im Kontext jeder Regel verfügbar sind:

  • Das request enthält Daten und Metadaten über die Operation, die versucht wird.
  • Wenn ein Firebase-Projekt Firebase Authentication verwendet, beschreibt das Objekt request.auth den Benutzer, der die Anfrage stellt.

10. Warenkorbzugriff testen

Die Emulator Suite aktualisiert die Regeln automatisch, wenn firestore.rules gespeichert wird. Sie können bestätigen, dass der Emulator die Regeln aktualisiert hat, indem Sie auf der Registerkarte, auf der der Emulator ausgeführt wird, nach der Meldung Rules updated suchen:

5680da418b420226.png

Führen Sie die Tests erneut aus und prüfen Sie, ob die ersten beiden Tests jetzt erfolgreich sind:

$ 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

Gut gemacht! Sie haben jetzt einen gesicherten Zugriff auf Warenkörbe. Kommen wir zum nächsten fehlgeschlagenen Test.

11. Überprüfen Sie den Ablauf „Zum Warenkorb hinzufügen“ in der Benutzeroberfläche

Im Moment können Warenkorbbesitzer, obwohl sie ihren Warenkorb lesen und schreiben, keine einzelnen Artikel in ihrem Warenkorb lesen oder schreiben. Das liegt daran, dass Eigentümer zwar Zugriff auf das Warenkorbdokument haben, aber keinen Zugriff auf die Untersammlung der Artikel des Warenkorbs .

Dies ist ein defekter Zustand für Benutzer.

Kehren Sie zur Webbenutzeroberfläche zurück, die auf http://localhost:5000, und versuchen Sie, Ihrem Warenkorb etwas hinzuzufügen. Sie erhalten einen Fehler Permission Denied “, der in der Debug-Konsole sichtbar ist, weil wir Benutzern noch keinen Zugriff auf erstellte Dokumente in der Untersammlung items gewährt haben.

12. Erlauben Sie den Zugriff auf Einkaufswagenartikel

Diese beiden Tests bestätigen, dass Benutzer nur Artikel zu ihrem eigenen Warenkorb hinzufügen oder Artikel aus diesem lesen können:

  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
    }));
  });

Wir können also eine Regel schreiben, die den Zugriff erlaubt, wenn der aktuelle Benutzer dieselbe UID wie die OwnerUID auf dem Warenkorbdokument hat. Da keine unterschiedlichen Regeln für create, update, delete angegeben werden müssen, können Sie eine write verwenden, die für alle Anfragen gilt, die Daten ändern.

Aktualisieren Sie die Regel für die Dokumente in der Artikeluntersammlung. Die get -Bedingung liest einen Wert aus Firestore – in diesem Fall die ownerUID auf dem Cart-Dokument.

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. Testen Sie den Zugriff auf Einkaufswagenartikel

Jetzt können wir den Test wiederholen. Scrollen Sie zum Anfang der Ausgabe und überprüfen Sie, ob weitere Tests erfolgreich sind:

$ 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

Hübsch! Jetzt sind alle unsere Tests bestanden. Wir haben einen ausstehenden Test, aber dazu kommen wir in ein paar Schritten.

14. Überprüfen Sie den Ablauf „In den Einkaufswagen“ erneut

Kehren Sie zum Web-Frontend ( http://localhost:5000 ) zurück und fügen Sie dem Warenkorb einen Artikel hinzu. Dies ist ein wichtiger Schritt, um zu bestätigen, dass unsere Tests und Regeln mit der vom Kunden geforderten Funktionalität übereinstimmen. (Denken Sie daran, dass Benutzer beim letzten Testen der Benutzeroberfläche keine Artikel in ihren Warenkorb legen konnten!)

69ad26cee520bf24.png

Der Client lädt die Regeln automatisch neu, wenn firestore.rules gespeichert wird. Versuchen Sie also, etwas in den Einkaufswagen zu legen.

Rekapitulieren

Gute Arbeit! Sie haben gerade die Sicherheit Ihrer App verbessert, ein wesentlicher Schritt, um sie für die Produktion vorzubereiten! Wenn es sich um eine Produktions-App handeln würde, könnten wir diese Tests zu unserer Continuous-Integration-Pipeline hinzufügen. Dies gibt uns die Gewissheit, dass unsere Warenkorbdaten diese Zugriffskontrollen haben werden, selbst wenn andere die Regeln ändern.

ba5440b193e75967.gif

Aber warte, es gibt noch mehr!

wenn du weitermachst, lernst du:

  • So schreiben Sie eine Funktion, die durch ein Firestore-Ereignis ausgelöst wird
  • So erstellen Sie Tests, die über mehrere Emulatoren hinweg funktionieren

15. Richten Sie Cloud Functions-Tests ein

Bisher haben wir uns auf das Frontend unserer Web-App und die Firestore-Sicherheitsregeln konzentriert. Aber diese App verwendet auch Cloud-Funktionen, um den Warenkorb des Benutzers auf dem neuesten Stand zu halten, also wollen wir diesen Code auch testen.

Die Emulator Suite macht es so einfach, Cloud-Funktionen zu testen, sogar Funktionen, die Cloud Firestore und andere Dienste verwenden.

Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/functions/test.js und scrollen Sie zum letzten Test in der Datei. Im Moment ist es als ausstehend markiert:

//  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 () => {
    ...
  });
});

Um den Test zu aktivieren, entfernen Sie .skip , sodass es so aussieht:

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

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

Suchen Sie als Nächstes die Variable REAL_FIREBASE_PROJECT_ID oben in der Datei und ändern Sie sie in Ihre echte Firebase-Projekt-ID:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

Wenn Sie Ihre Projekt-ID vergessen haben, finden Sie Ihre Firebase-Projekt-ID in den Projekteinstellungen in der Firebase-Konsole:

d6d0429b700d2b21.png

16. Führen Sie die Funktionstests durch

Da dieser Test die Interaktion zwischen Cloud Firestore und Cloud Functions validiert, erfordert er mehr Einrichtung als die Tests in den vorherigen Codelabs. Lassen Sie uns diesen Test durchgehen und uns ein Bild davon machen, was er erwartet.

Erstellen Sie einen Warenkorb

Cloud Functions wird in einer vertrauenswürdigen Serverumgebung ausgeführt und kann die vom Admin SDK verwendete Dienstkontoauthentifizierung verwenden. Zunächst initialisieren Sie eine App mit initializeAdminApp anstelle von initializeApp . Dann erstellen Sie eine DocumentReference für den Warenkorb, dem wir Artikel hinzufügen, und initialisieren den Warenkorb:

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 });

    ...
  });

Lösen Sie die Funktion aus

Fügen Sie dann Dokumente zur items Untersammlung unseres Warenkorbdokuments hinzu, um die Funktion auszulösen. Fügen Sie zwei Elemente hinzu, um sicherzustellen, dass Sie die Hinzufügung testen, die in der Funktion erfolgt.

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 });

    ...
    });
  });

Testerwartungen festlegen

Verwenden Sie onSnapshot() , um einen Listener für alle Änderungen am Warenkorbdokument zu registrieren. onSnapshot() gibt eine Funktion zurück, die Sie aufrufen können, um den Listener abzumelden.

Fügen Sie für diesen Test zwei Artikel hinzu, die zusammen 9,98 $ kosten. Überprüfen Sie dann, ob der Warenkorb den erwarteten itemCount und totalPrice hat. Wenn ja, dann hat die Funktion ihren Job gemacht.

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. Führen Sie die Tests durch

Möglicherweise laufen noch die Emulatoren der vorherigen Tests. Wenn nicht, starten Sie die Emulatoren. Führen Sie von der Befehlszeile aus

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

Öffnen Sie einen neuen Terminal-Tab (lassen Sie die Emulatoren laufen) und wechseln Sie in das Funktionsverzeichnis. Möglicherweise haben Sie dies noch von den Sicherheitsregeltests geöffnet.

$ cd functions

Führen Sie nun die Komponententests aus, Sie sollten insgesamt 5 Tests sehen:

$ 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

Wenn Sie sich den spezifischen Fehler ansehen, scheint es sich um einen Zeitüberschreitungsfehler zu handeln. Dies liegt daran, dass der Test darauf wartet, dass die Funktion korrekt aktualisiert wird, dies jedoch nie der Fall ist. Jetzt können wir die Funktion schreiben, um den Test zu bestehen.

18. Schreiben Sie eine Funktion

Um diesen Test zu beheben, müssen Sie die Funktion in functions/index.js aktualisieren. Obwohl ein Teil dieser Funktion geschrieben wurde, ist sie nicht vollständig. So sieht die Funktion aktuell aus:

// 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) {
      }
    });

Die Funktion setzt die Einkaufswagenreferenz korrekt, aber anstatt die Werte von totalPrice und itemCount zu berechnen, aktualisiert sie sie auf fest codierte Werte.

Holen und iterieren Sie durch die

items Untersammlung

Initialisieren Sie eine neue Konstante itemsSnap als Untersammlung items . Durchlaufen Sie dann alle Dokumente in der Sammlung.

// 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) {
      }
    });

Berechnen Sie totalPrice und itemCount

Lassen Sie uns zunächst die Werte von totalPrice und itemCount auf Null initialisieren.

Fügen Sie dann die Logik zu unserem Iterationsblock hinzu. Überprüfen Sie zunächst, ob der Artikel einen Preis hat. Wenn für den Artikel keine Menge angegeben ist, verwenden Sie standardmäßig 1 . Fügen Sie dann die Menge zur laufenden Summe von itemCount hinzu. Fügen Sie schließlich den Preis des Artikels multipliziert mit der Menge zur laufenden Summe von totalPrice hinzu:

// 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) {
      }
    });

Sie können auch eine Protokollierung hinzufügen, um beim Debuggen von Erfolgs- und Fehlerzuständen zu helfen:

// 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. Wiederholen Sie die Tests

Stellen Sie in der Befehlszeile sicher, dass die Emulatoren noch ausgeführt werden, und führen Sie die Tests erneut aus. Sie müssen die Emulatoren nicht neu starten, da sie Änderungen an den Funktionen automatisch übernehmen. Sie sollten sehen, dass alle Tests bestanden sind:

$ 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)

Gut gemacht!

20. Probieren Sie es mit der Storefront-Benutzeroberfläche aus

Kehren Sie für den abschließenden Test zur Web-App ( http://localhost:5000/ ) zurück und fügen Sie einen Artikel zum Warenkorb hinzu.

69ad26cee520bf24.png

Bestätigen Sie, dass der Warenkorb mit der korrekten Gesamtsumme aktualisiert wird. Fantastisch!

Rekapitulieren

Sie haben einen komplexen Testfall zwischen Cloud Functions for Firebase und Cloud Firestore durchlaufen. Sie haben eine Cloud-Funktion geschrieben, um den Test zu bestehen. Sie haben auch bestätigt, dass die neue Funktionalität in der Benutzeroberfläche funktioniert! All dies haben Sie lokal gemacht, indem Sie die Emulatoren auf Ihrem eigenen Rechner ausgeführt haben.

Sie haben auch einen Webclient erstellt, der mit den lokalen Emulatoren ausgeführt wird, Sicherheitsregeln angepasst, um die Daten zu schützen, und die Sicherheitsregeln mit den lokalen Emulatoren getestet.

c6a7aeb91fe97a64.gif