Sviluppo locale con Firebase Emulator Suite

1. Prima di iniziare

Gli strumenti di backend serverless come Cloud Firestore e Cloud Functions sono molto facili da usare, ma possono essere difficili da testare. Firebase Local Emulator Suite ti consente di eseguire versioni locali di questi servizi sul tuo computer di sviluppo in modo da poter sviluppare la tua app in modo rapido e sicuro.

Prerequisiti

  • Un semplice editor come Visual Studio Code, Atom o Sublime Text
  • Node.js 10.0.0 o superiore (per l'installazione Node.js, uso NVM , per controllare la versione, eseguire node --version )
  • Java 7 o superiore (per installare Java utilizzare queste istruzioni , per controllare la versione, eseguire java -version )

cosa farai

In questo codelab, eseguirai ed eseguirai il debug di una semplice app per lo shopping online che è alimentata da più servizi Firebase:

  • Nuvola FireStore: una, senza server, database di livello mondiale scalabile NoSQL con funzionalità in tempo reale.
  • Funzioni Cloud: un codice backend senza server che viene eseguito in risposta ad eventi o richieste HTTP.
  • Firebase autenticazione: un servizio di autenticazione gestito che si integra con gli altri prodotti Firebase.
  • Firebase Hosting: veloce e sicuro di hosting per le applicazioni web.

Collegherai l'app a Emulator Suite per abilitare lo sviluppo locale.

2589e2f95b74fa88.png

Imparerai anche a:

  • Come connettere la tua app alla Emulator Suite e come sono collegati i vari emulatori.
  • Come funzionano le regole di sicurezza di Firebase e come testare le regole di sicurezza di Firestore su un emulatore locale.
  • Come scrivere una funzione Firebase che viene attivata da eventi Firestore e come scrivere test di integrazione che vengono eseguiti su Emulator Suite.

2. Configurazione

Ottieni il codice sorgente

In questo codelab, inizi con una versione dell'esempio di The Fire Store che è quasi completa, quindi la prima cosa che devi fare è clonare il codice sorgente:

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

Quindi spostati nella directory codelab, dove lavorerai per il resto di questo codelab:

$ cd emulators-codelab/codelab-initial-state

Ora installa le dipendenze in modo da poter eseguire il codice. Se utilizzi una connessione Internet più lenta, questa operazione potrebbe richiedere uno o due minuti:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

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

Ottieni la CLI di Firebase

Emulator Suite fa parte della Firebase CLI (interfaccia a riga di comando) che può essere installata sulla tua macchina con il seguente comando:

$ npm install -g firebase-tools

Quindi, conferma di avere l'ultima versione della CLI. Questo codelab dovrebbe funzionare con la versione 9.0.0 o successiva, ma le versioni successive includono più correzioni di bug.

$ firebase --version
9.6.0

Connettiti al tuo progetto Firebase

Se non si dispone di un progetto Firebase, nella console Firebase , creare un nuovo progetto Firebase. Prendi nota dell'ID progetto che scegli, ti servirà in seguito.

Ora dobbiamo collegare questo codice al tuo progetto Firebase. Eseguire prima il comando seguente per accedere alla CLI Firebase:

$ firebase login

Quindi esegui il seguente comando per creare un alias di progetto. Sostituire $YOUR_PROJECT_ID con l'ID del progetto Firebase.

$ firebase use $YOUR_PROJECT_ID

Ora sei pronto per eseguire l'app!

3. Esegui gli emulatori

In questa sezione, eseguirai l'app localmente. Ciò significa che è ora di avviare Emulator Suite.

Avvia gli emulatori

Dall'interno della directory dei sorgenti di codelab, eseguire il comando seguente per avviare gli emulatori:

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

Dovresti vedere un output come questo:

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

Una volta che vedi i tutti gli emulatori iniziate messaggio, l'applicazione è pronta per l'uso.

Collega la web app agli emulatori

In base alla tabella nei log possiamo vedere che la l'emulatore cloud FireStore è in ascolto sulla porta 8080 e l'emulatore di autenticazione è in ascolto sulla porta 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Colleghiamo il tuo codice frontend all'emulatore, piuttosto che alla produzione. Aprire il public/js/homepage.js di file e trovare il onDocumentReady funzioni. Possiamo vedere che il codice accede alle istanze standard di Firestore e Auth:

public/js/homepage.js

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

L'aggiornamento di lasciare che il db e auth oggetti per puntare al emulatori locali:

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

Ora, quando l'app è in esecuzione su localhost (servito dall'emulatore di hosting), anche il client Firestore punta all'emulatore locale anziché a un database di produzione.

Apri l'interfaccia utente dell'emulatore

Nel browser Web, accedere a http: // localhost: 4000 / . Dovresti vedere l'interfaccia utente di Emulator Suite.

Schermata iniziale dell'interfaccia utente degli emulatori

Fare clic per visualizzare l'interfaccia utente dell'emulatore Firestore. items raccolta contiene già dei dati a causa dei dati importati con il --import bandiera.

4ef88d0148405d36.png

4. Esegui l'app

Apri l'app

Nel browser Web, accedere a http: // localhost: 5000 e si dovrebbe vedere il fuoco Conservare in esecuzione in locale sulla propria macchina!

939f87946bac2ee4.png

Usa l'app

Scegliere un elemento sulla homepage e cliccare su Aggiungi al carrello. Sfortunatamente, incontrerai il seguente errore:

a11bd59933a8e885.png

Risolviamo questo bug! Poiché tutto è in esecuzione negli emulatori, possiamo sperimentare e non preoccuparci di influenzare i dati reali.

5. Eseguire il debug dell'app

Trova il bug

Ok, diamo un'occhiata nella console per sviluppatori di Chrome. Premete Control+Shift+J (Windows, Linux, Chrome OS) o Command+Option+J (Mac) per vedere l'errore sulla console:

74c45df55291dab1.png

Sembra che ci fosse qualche errore nel addToCart metodo, diamo uno sguardo a questo. Dove si cerca di accedere a qualcosa chiamato uid in quel metodo e perché sarebbe null ? In questo momento il metodo aspetto come questo in 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! Non abbiamo effettuato l'accesso all'app. Secondo la documentazione Firebase di autenticazione , quando non sei connesso, auth.currentUser è null . Aggiungiamo un controllo per questo:

public/js/homepage.js

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

    // ...
  }

Prova l'app

Ora, aggiornare la pagina e quindi fare clic su Aggiungi al carrello. Dovresti ottenere un errore più carino questa volta:

c65f6c05588133f7.png

Ma se si fa clic su Entra nella barra degli strumenti superiore e quindi fare clic su Aggiungi al carrello ancora una volta, si vedrà che il carro viene aggiornato.

Tuttavia, non sembra affatto che i numeri siano corretti:

239f26f02f959eef.png

Non preoccuparti, risolveremo presto questo bug. Per prima cosa, approfondiamo cosa è successo quando hai aggiunto un articolo al carrello.

6. Trigger di funzioni locali

Facendo clic su Aggiungi al carrello prende il via una catena di eventi che coinvolgono più emulatori. Nei log della CLI di Firebase, dovresti vedere qualcosa come i seguenti messaggi dopo aver aggiunto un articolo al carrello:

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

Ci sono stati quattro eventi chiave che si sono verificati per produrre quei registri e l'aggiornamento dell'interfaccia utente che hai osservato:

68c9323f2ad10f7a.png

1) Scrittura Firestore - Cliente

Un nuovo documento viene aggiunto alla collezione FireStore /carts/{cartId}/items/{itemId}/ . Si può vedere questo codice nel addToCart funzione all'interno 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) Funzione cloud attivata

La nube Funzione calculateCart ascolti per tutti gli eventi di scrittura (creare, aggiornare o eliminare) che capita di carrello oggetti utilizzando la onWrite grilletto, che potete vedere in functions/index.js :

funzioni/indice.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) Scrittura Firestore - Amministratore

La calculateCart funzione legge tutti gli elementi presenti nel carrello e aggiunge la quantità totale e il prezzo, poi aggiorna il "carrello" documento con le nuove totali (vedi cartRef.update(...) di cui sopra).

4) Lettura Firestore - Cliente

Il frontend web è sottoscritto per ricevere aggiornamenti sulle modifiche al carrello. Si ottiene un aggiornamento in tempo reale dopo che la funzione cloud scrive i nuovi totali e aggiorna l'interfaccia utente, come si può vedere in public/js/homepage.js :

public/js/homepage.js

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

Ricapitolare

Bel lavoro! Hai appena configurato un'app completamente locale che utilizza tre diversi emulatori Firebase per i test completamente locali.

db82eef1706c9058.gif

Ma aspetta, c'è di più! Nella prossima sezione imparerai:

  • Come scrivere unit test che utilizzano gli emulatori Firebase.
  • Come utilizzare gli emulatori Firebase per eseguire il debug delle regole di sicurezza.

7. Crea regole di sicurezza su misura per la tua app

La nostra app web legge e scrive dati, ma finora non ci siamo preoccupati affatto della sicurezza. Cloud Firestore utilizza un sistema chiamato "Regole di sicurezza" per dichiarare chi ha accesso per leggere e scrivere i dati. Emulator Suite è un ottimo modo per prototipare queste regole.

Nel l'editor, aprire il file emulators-codelab/codelab-initial-state/firestore.rules . Vedrai che abbiamo tre sezioni principali nelle nostre regole:

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

In questo momento chiunque può leggere e scrivere dati nel nostro database! Vogliamo assicurarci che vengano eseguite solo operazioni valide e che non trapeliamo informazioni sensibili.

Durante questo codelab, seguendo il principio del minimo privilegio, bloccheremo tutti i documenti e aggiungeremo gradualmente l'accesso fino a quando tutti gli utenti avranno tutto l'accesso di cui hanno bisogno, ma non di più. L'aggiornamento di lasciare che i primi due regole per negare l'accesso impostando la condizione di 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. Esegui gli emulatori e i test

Avvia gli emulatori

Sulla riga di comando, assicuratevi di essere in emulators-codelab/codelab-initial-state/ . Potresti avere ancora gli emulatori in esecuzione dai passaggi precedenti. In caso contrario, riavvia gli emulatori:

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

Una volta che gli emulatori sono in esecuzione, puoi eseguire i test localmente su di essi.

Esegui i test

Sulla linea di comando in una nuova scheda terminale dalla directory emulators-codelab/codelab-initial-state/

Prima spostati nella directory delle funzioni (rimarremo qui per il resto del codelab):

$ cd functions

Ora esegui i test mocha nella directory functions e scorri fino all'inizio dell'output:

# 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

In questo momento abbiamo quattro fallimenti. Mentre crei il file delle regole, puoi misurare i progressi osservando il superamento di più test.

9. Accesso sicuro al carrello

I primi due fallimenti sono i test del "carrello della spesa" che verificano che:

  • Gli utenti possono solo creare e aggiornare i propri carrelli
  • Gli utenti possono leggere solo i propri carrelli

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

Facciamo passare questi test. Nel l'editor, aprire il file di regole di sicurezza, firestore.rules , e aggiornare le istruzioni all'interno 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;
    }

    // ...
  }
}

Queste regole ora consentono solo l'accesso in lettura e scrittura al proprietario del carrello.

Per verificare i dati in entrata e l'autenticazione dell'utente, utilizziamo due oggetti disponibili nel contesto di ogni regola:

  • La request oggetto contiene dati e metadati relativi l'operazione che si sta tentando.
  • Se un progetto Firebase sta usando l'autenticazione Firebase , l' request.auth oggetto descrive l'utente che sta effettuando la richiesta.

10. Prova l'accesso al carrello

L'emulatore Suite aggiorna automaticamente le regole ogni volta che firestore.rules viene salvato. È possibile confermare che l'emulatore ha aggiornato le regole, cercando nella scheda in esecuzione l'emulatore per il messaggio Rules updated :

5680da418b420226.png

Rieseguire i test e verificare che i primi due test siano stati superati:

$ 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

Buon lavoro! Ora hai l'accesso sicuro ai carrelli della spesa. Passiamo al prossimo test fallito.

11. Controlla il flusso "Aggiungi al carrello" nell'interfaccia utente

In questo momento, anche se i proprietari del carrello leggono e scrivono nel carrello, non possono leggere o scrivere singoli elementi nel carrello. Questo perché mentre i proprietari hanno accesso al documento della spesa, che non hanno accesso a quello del carrello articoli sottoraccolta.

Questo è uno stato interrotto per gli utenti.

Ritorno al web UI, che è in esecuzione su http://localhost:5000, e cercare di aggiungere qualcosa al carrello. È possibile ottenere Permission Denied errore, visibile dalla console di debug, perché non abbiamo ancora concesso l'accesso agli utenti di documenti creati in items sottoraccolta.

12. Consenti l'accesso agli articoli del carrello

Questi due test confermano che gli utenti possono solo aggiungere articoli o leggere articoli dal proprio carrello:

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

Quindi possiamo scrivere una regola che permetta l'accesso se l'utente corrente ha lo stesso UID del proprietarioUID sul documento del carrello. Dal momento che non c'è bisogno di specificare regole diverse per create, update, delete , è possibile utilizzare una write regola, che si applica a tutte le richieste che i dati di modifica.

Aggiorna la regola per i documenti nella sottoraccolta articoli. La get al condizionale sta leggendo un valore da FireStore, in questo caso, il ownerUID sul documento della spesa.

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. Prova l'accesso agli articoli del carrello

Ora possiamo rieseguire il test. Scorri fino all'inizio dell'output e verifica che vengano superati più test:

$ 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

Simpatico! Ora tutti i nostri test passano. Abbiamo un test in sospeso, ma ci arriveremo in pochi passaggi.

14. Controlla di nuovo il flusso "aggiungi al carrello"

Torna a front-end web ( http: // localhost: 5000 ) e aggiungere un elemento al carrello. Questo è un passaggio importante per confermare che i nostri test e le nostre regole corrispondano alle funzionalità richieste dal cliente. (Ricorda che l'ultima volta che abbiamo provato l'interfaccia utente gli utenti non sono stati in grado di aggiungere articoli al carrello!)

69ad26cee520bf24.png

Il cliente ricarica automaticamente le regole quando i firestore.rules viene salvato. Quindi, prova ad aggiungere qualcosa al carrello.

Ricapitolare

Bel lavoro! Hai appena migliorato la sicurezza della tua app, un passaggio essenziale per prepararla alla produzione! Se questa fosse un'app di produzione, potremmo aggiungere questi test alla nostra pipeline di integrazione continua. In futuro, questo ci darebbe la certezza che i dati del nostro carrello degli acquisti avranno questi controlli di accesso, anche se altri stanno modificando le regole.

ba5440b193e75967.gif

Ma aspetta, c'è di più!

se continui imparerai:

  • Come scrivere una funzione attivata da un evento Firestore
  • Come creare test che funzionano su più emulatori

15. Configura i test di Cloud Functions

Finora ci siamo concentrati sul frontend della nostra app Web e sulle regole di sicurezza di Firestore. Ma questa app utilizza anche le funzioni cloud per mantenere aggiornato il carrello dell'utente, quindi vogliamo testare anche quel codice.

Emulator Suite semplifica il test delle funzioni cloud, anche delle funzioni che utilizzano Cloud Firestore e altri servizi.

Nel l'editor, aprire le emulators-codelab/codelab-initial-state/functions/test.js di file e di scorrimento per l'ultimo test nel file. Al momento, è contrassegnato come in sospeso:

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

Per abilitare il test, rimuovere .skip , in modo che appaia in questo modo:

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

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

Quindi, trovare il REAL_FIREBASE_PROJECT_ID variabile nella parte superiore del file e modificarlo a tuo vero Firebase Progetto ID .:

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

Se hai dimenticato il tuo ID progetto, puoi trovare il tuo ID progetto Firebase nelle Impostazioni progetto nella Console Firebase:

d6d0429b700d2b21.png

16. Esaminare i test di funzioni

Poiché questo test convalida l'interazione tra Cloud Firestore e Cloud Functions, richiede una configurazione maggiore rispetto ai test nei codelab precedenti. Esaminiamo questo test e abbiamo un'idea di cosa si aspetta.

Crea un carrello

Cloud Functions viene eseguito in un ambiente server attendibile e può utilizzare l'autenticazione dell'account di servizio utilizzata da Admin SDK. In primo luogo, si inizializza un'applicazione utilizzando initializeAdminApp invece di initializeApp . Poi, si crea un DocumentReference per il carrello ne aggiungeremo elementi da e inizializzare il carro:

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

    ...
  });

Attiva la funzione

Quindi, aggiungere documenti alla items sottoraccolta del nostro documento della spesa al fine di innescare la funzione. Aggiungi due elementi per assicurarti di testare l'aggiunta che avviene nella funzione.

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

    ...
    });
  });

Imposta le aspettative del test

Usa onSnapshot() per registrare un listener per eventuali modifiche sul documento della spesa. onSnapshot() restituisce una funzione che è possibile chiamare per annullare la registrazione l'ascoltatore.

Per questo test, aggiungi due articoli che insieme costano $ 9,98. Quindi, controllare se il carrello ha l'atteso itemCount e totalPrice . In tal caso, la funzione ha svolto il suo lavoro.

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. Eseguire i test

Potresti avere ancora gli emulatori in esecuzione dai test precedenti. In caso contrario, avvia gli emulatori. Dalla riga di comando, esegui

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

Aprire una nuova scheda terminale (lasciare le emulatori in esecuzione) e spostare nella directory funzioni. Potresti ancora averlo aperto dai test delle regole di sicurezza.

$ cd functions

Ora esegui gli unit test, dovresti vedere 5 test totali:

$ 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

Se guardi l'errore specifico, sembra essere un errore di timeout. Questo perché il test è in attesa che la funzione si aggiorni correttamente, ma non lo fa mai. Ora siamo pronti per scrivere la funzione per soddisfare il test.

18. Scrivi una funzione

Per risolvere questo test, è necessario aggiornare la funzione di functions/index.js . Sebbene alcune di queste funzioni siano scritte, non sono complete. Ecco come appare attualmente la funzione:

// 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 funzione sta impostando correttamente il riferimento carro, ma poi invece di calcolare i valori di totalPrice e itemCount , li aggiorna quelli hardcoded.

Recupera e itera attraverso il

items sottoraccolta

Inizializzare una nuova costante, itemsSnap , di essere items sotto-collezione. Quindi, scorrere tutti i documenti nella raccolta.

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

Calcola il prezzo totale e il numero di articoli

In primo luogo, cerchiamo di inizializzare i valori di totalPrice e itemCount a zero.

Quindi, aggiungi la logica al nostro blocco di iterazione. Innanzitutto, controlla che l'articolo abbia un prezzo. Se l'articolo non dispone di una quantità determinata, lasciate di default a 1 . Quindi, aggiungere la quantità al totale di itemCount . Infine, aggiungere il prezzo dell'oggetto moltiplicato per la quantità al totale di 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) {
      }
    });

Puoi anche aggiungere la registrazione per aiutare a eseguire il debug degli stati di successo ed errore:

// 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. Ripetere i test

Sulla riga di comando, assicurati che gli emulatori siano ancora in esecuzione ed esegui nuovamente i test. Non è necessario riavviare gli emulatori perché rilevano automaticamente le modifiche alle funzioni. Dovresti vedere tutti i test superati:

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

Buon lavoro!

20. Provalo utilizzando l'interfaccia utente di Storefront

Per la prova finale, ritorno al web app ( http: // localhost: 5000 / ) e aggiungere un elemento al carrello.

69ad26cee520bf24.png

Conferma che il carrello si aggiorni con il totale corretto. Fantastico!

Ricapitolare

Hai esaminato un complesso test case tra Cloud Functions for Firebase e Cloud Firestore. Hai scritto una funzione cloud per far passare il test. Hai anche confermato che la nuova funzionalità funziona nell'interfaccia utente! Hai fatto tutto questo localmente, eseguendo gli emulatori sulla tua macchina.

Hai anche creato un client Web in esecuzione sugli emulatori locali, regole di sicurezza personalizzate per proteggere i dati e testato le regole di sicurezza utilizzando gli emulatori locali.

c6a7aeb91fe97a64.gif