Lokale Entwicklung mit der Firebase Emulator Suite

1. Bevor Sie beginnen

Serverlose Backend-Tools wie Cloud Firestore und Cloud Functions sind sehr einfach zu verwenden, können jedoch schwierig zu testen sein. Mit der Firebase Local Emulator Suite können Sie lokale Versionen dieser Dienste auf Ihrem Entwicklungscomputer ausführen, sodass 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 führen Sie eine einfache Online-Shopping-App aus und debuggen sie, die auf mehreren Firebase-Diensten basiert:

  • Cloud Firestore: eine global skalierbare, serverlose NoSQL-Datenbank mit Echtzeitfähigkeiten.
  • Cloud Functions : ein serverloser Backend-Code, der als Reaktion auf Ereignisse oder HTTP-Anfragen ausgeführt wird.
  • Firebase-Authentifizierung : ein verwalteter Authentifizierungsdienst, der in andere Firebase-Produkte integriert werden kann.
  • 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

Sie erfahren außerdem, wie Sie:

  • So verbinden Sie Ihre App mit der Emulator Suite und wie die verschiedenen Emulatoren verbunden sind.
  • Wie Firebase-Sicherheitsregeln funktionieren und wie man Firestore-Sicherheitsregeln mit einem lokalen Emulator testet.
  • So schreiben Sie eine Firebase-Funktion, die durch Firestore-Ereignisse ausgelöst wird, und wie schreiben Sie Integrationstests, die für 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 Fire Store-Beispiels. Als Erstes müssen Sie also 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:

$ cd emulators-codelab/codelab-initial-state

Installieren Sie nun die Abhängigkeiten, damit Sie den Code ausführen können. Wenn Sie eine langsamere Internetverbindung haben, 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, spätere Versionen enthalten jedoch weitere Fehlerkorrekturen.

$ 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 werden sie später benötigen.

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 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://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.

Sobald Sie die Meldung „Alle Emulatoren gestartet“ sehen, ist die App einsatzbereit.

Verbinden Sie die Web-App mit den Emulatoren

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

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

Verbinden wir Ihren Frontend-Code mit dem Emulator und nicht mit der Produktion. Öffnen Sie die Datei public/js/homepage.js und suchen Sie die Funktion onDocumentReady . Wir können sehen, dass der Code auf die Standard-Firestore- und Auth-Instanzen zugreift:

public/js/homepage.js

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

Aktualisieren wir die 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 === "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);
  }

Wenn die App nun auf Ihrem lokalen Computer ausgeführt wird (der vom Hosting-Emulator bereitgestellt wird), verweist der Firestore-Client auch auf den lokalen Emulator und nicht auf eine Produktionsdatenbank.

Öffnen Sie die EmulatorUI

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

Startbildschirm der Emulatoren-Benutzeroberfläche

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

4ef88d0148405d36.png

4. Führen Sie die App aus

Öffnen Sie die App

Navigieren Sie in Ihrem Webbrowser zu http://127.0.0.1:5000 und Sie sollten sehen, dass The Fire Store lokal auf Ihrem Computer ausgeführt wird!

939f87946bac2ee4.png

Nutzen Sie die App

Wählen Sie auf der Startseite einen Artikel aus und klicken Sie auf „In den Warenkorb“ . Leider wird der folgende Fehler auftreten:

a11bd59933a8e885.png

Beheben wir diesen Fehler! Da alles in den Emulatoren läuft, können wir experimentieren und müssen uns keine Sorgen ü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, dass in der Methode addToCart ein Fehler aufgetreten ist. Sehen wir uns das einmal an. Wo versuchen wir, in dieser Methode auf etwas namens uid 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. Laut der Dokumentation zur Firebase-Authentifizierung ist auth.currentUser null , wenn wir nicht angemeldet sind. Fügen wir dafür einen Scheck hinzu:

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 App

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

c65f6c05588133f7.png

Wenn Sie jedoch in der oberen Symbolleiste auf „Anmelden“ klicken und dann erneut auf „Zum Warenkorb hinzufügen“ klicken, werden Sie sehen, dass der Warenkorb aktualisiert wurde.

Es sieht jedoch nicht so aus, als ob die Zahlen überhaupt korrekt wären:

239f26f02f959eef.png

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

6. Lokale Funktionsauslöser

Wenn Sie auf „Zum Warenkorb hinzufügen“ klicken, wird eine Kette von Ereignissen ausgelöst, an denen mehrere Emulatoren beteiligt sind. In den Firebase-CLI-Protokollen sollten etwa die folgenden Meldungen angezeigt werden, nachdem Sie einen Artikel zu Ihrem Warenkorb hinzugefügt haben:

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

Zur Erstellung dieser Protokolle und der von Ihnen beobachteten Aktualisierung der Benutzeroberfläche sind vier wichtige Ereignisse eingetreten:

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 mithilfe des onWrite Triggers auf alle Schreibereignisse (Erstellen, Aktualisieren oder Löschen), die bei Warenkorbartikeln auftreten, die 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 Preis. Anschließend aktualisiert sie das Dokument „Warenkorb“ mit den neuen Gesamtbeträgen (siehe cartRef.update(...) “ oben).

4) Firestore Read – Client

Das Web-Frontend ist abonniert, um Updates über Änderungen am Warenkorb zu erhalten. Es erhält ein Echtzeit-Update, nachdem die Cloud-Funktion die neuen Gesamtzahlen schreibt und die Benutzeroberfläche aktualisiert, 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 warten Sie, es gibt noch mehr! Im nächsten Abschnitt erfahren Sie:

  • So schreiben Sie Unit-Tests, 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 um die Sicherheit haben wir uns bisher überhaupt keine Sorgen gemacht. Cloud Firestore verwendet ein System namens „Sicherheitsregeln“, um zu deklarieren, wer Zugriff auf Lese- und Schreibdaten 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 wir in unseren 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;
    }
  }
}

Derzeit kann jeder Daten in unserer Datenbank lesen und schreiben! Wir möchten sicherstellen, dass nur gültige Vorgänge durchkommen und keine vertraulichen Informationen preisgegeben werden.

Während dieses Codelabs werden wir gemäß dem Prinzip der geringsten Rechte alle Dokumente sperren und nach und nach Zugriff hinzufügen, bis alle Benutzer den gesamten Zugriff haben, den sie benötigen, 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 der vorherigen Schritte 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 ausführen.

Führen Sie die Tests durch

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

Gehen Sie zunächst 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 weitere Tests durchgeführt werden.

9. Sicherer Warenkorbzugriff

Die ersten beiden Fehler sind die „Warenkorb“-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 Warenkorbbesitzer.

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

10. Testen Sie den Warenkorbzugriff

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 nun 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

Gute Arbeit! Sie haben sich nun den Zugang zu den Warenkörben gesichert. Fahren wir mit dem nächsten fehlgeschlagenen Test fort.

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

Derzeit können Warenkorbbesitzer zwar ihren Warenkorb lesen und schreiben, einzelne Artikel in ihrem Warenkorb jedoch nicht lesen oder schreiben. Das liegt daran, dass Eigentümer zwar Zugriff auf das Warenkorbdokument, aber keinen Zugriff auf die Artikeluntersammlung des Warenkorbs haben.

Dies ist ein fehlerhafter Zustand für Benutzer.

Kehren Sie zur Web-Benutzeroberfläche zurück, die unter http://127.0.0.1:5000, und versuchen Sie, etwas zu Ihrem Warenkorb hinzuzufügen. Sie erhalten die Fehlermeldung Permission Denied , die in der Debug-Konsole sichtbar ist, da wir Benutzern noch keinen Zugriff auf erstellte Dokumente in der Untersammlung „ items gewährt haben.

12. Erlauben Sie den Zugriff auf Warenkorbartikel

Diese beiden Tests bestätigen, dass Benutzer nur Artikel zu ihrem eigenen Warenkorb hinzufügen oder Artikel daraus 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 ermöglicht, wenn der aktuelle Benutzer dieselbe UID hat wie die Eigentümer-UID im Warenkorbdokument. Da keine unterschiedlichen Regeln für create, update, delete angegeben werden müssen, können Sie eine write verwenden, die für alle Anforderungen gilt, die Daten ändern.

Aktualisieren Sie die Regel für die Dokumente in der Artikeluntersammlung. Der get in der Bedingung liest einen Wert aus Firestore – in diesem Fall die ownerUID im Warenkorbdokument.

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 Warenkorbartikel

Jetzt können wir den Test erneut durchführen. Scrollen Sie zum Anfang der Ausgabe und prüfen Sie, ob weitere Tests bestanden wurden:

$ 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 bestehen alle unsere Tests. Ein Test steht noch aus, aber dazu kommen wir in ein paar Schritten.

14. Überprüfen Sie den Ablauf „In den Warenkorb“ noch einmal

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

69ad26cee520bf24.png

Der Client lädt die Regeln automatisch neu, wenn die firestore.rules gespeichert wird. Versuchen Sie also, etwas in den Warenkorb 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 Produktionsanwendung handeln würde, könnten wir diese Tests zu unserer kontinuierlichen Integrationspipeline hinzufügen. Dies würde uns in Zukunft die Gewissheit geben, dass unsere Warenkorbdaten über diese Zugriffskontrollen verfügen, selbst wenn andere die Regeln ändern.

ba5440b193e75967.gif

Aber warten Sie, es gibt noch mehr!

Wenn Sie weitermachen, erfahren Sie:

  • So schreiben Sie eine Funktion, die durch ein Firestore-Ereignis ausgelöst wird
  • So erstellen Sie Tests, die auf mehreren Emulatoren funktionieren

15. Richten Sie Cloud Functions-Tests ein

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

Mit der Emulator Suite ist es ganz einfach, Cloud-Funktionen zu testen, sogar Funktionen, die Cloud Firestore und andere Dienste nutzen.

Ö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 wie folgt 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. Gehen 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 eine Vorstellung davon bekommen, was er erwartet.

Erstellen Sie einen Warenkorb

Cloud Functions werden in einer vertrauenswürdigen Serverumgebung ausgeführt und können die vom Admin SDK verwendete Dienstkontoauthentifizierung verwenden. Zuerst initialisieren Sie eine App mit initializeAdminApp anstelle von initializeApp . Anschließend 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 Untersammlung items 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 });

    ...
    });
  });

Legen Sie Testerwartungen fest

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 die Registrierung des Listeners aufzuheben.

Fügen Sie für diesen Test zwei Artikel hinzu, die zusammen 9,98 $ kosten. Überprüfen Sie dann, ob der Warenkorb die erwartete itemCount und totalPrice aufweist. Wenn ja, dann hat die Funktion ihre Aufgabe erfüllt.

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 über die Befehlszeile Folgendes 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 ist dies aufgrund der Sicherheitsregeltests noch geöffnet.

$ cd functions

Führen Sie nun die Unit-Tests 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 konkreten Fehler ansehen, scheint es sich um einen Timeout-Fehler 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 ist, ist sie noch nicht vollständig. So sieht die Funktion derzeit 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 Warenkorbreferenz korrekt, aber statt die Werte von totalPrice und itemCount zu berechnen, aktualisiert sie sie auf fest codierte Werte.

Rufen Sie die Datei ab und durchlaufen Sie sie

Untersammlung items

Initialisieren Sie eine neue Konstante, itemsSnap , um die Untersammlung items zu sein. Anschließend durchlaufen Sie 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 für den Artikel ein Preis angegeben ist. Wenn für den Artikel keine Menge angegeben ist, verwenden Sie den Standardwert 1 . Fügen Sie dann die Menge zur laufenden Summe von itemCount hinzu. Addieren Sie abschließend den Preis des Artikels multipliziert mit der Menge zur laufenden Summe von 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) {
      }
    });

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

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

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 wurden:

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

Gute Arbeit!

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

Kehren Sie für den letzten Test zur Web-App ( http://127.0.0.1:5000/ ) zurück und legen Sie einen Artikel in den Warenkorb.

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! Sie haben dies alles lokal durchgeführt und die Emulatoren auf Ihrem eigenen Computer ausgeführt.

Sie haben außerdem einen Web-Client erstellt, der mit den lokalen Emulatoren ausgeführt wird, Sicherheitsregeln zum Schutz der Daten angepasst und die Sicherheitsregeln mithilfe der lokalen Emulatoren getestet.

c6a7aeb91fe97a64.gif