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 l'app in modo rapido e sicuro.
Prerequisiti
- Un editor semplice come Visual Studio Code, Atom o Sublime Text
- Node.js 10.0.0 o versioni successive (per installare Node.js, utilizza nvm; per controllare la versione, esegui
node --version
) - Java 7 o versioni successive (per installare Java utilizza queste istruzioni, per controllare la versione, esegui
java -version
)
Attività previste
In questo codelab, eseguirai e debuggherai una semplice app di shopping online basata su più servizi Firebase:
- Cloud Firestore: un database NoSQL scalabile a livello globale con funzionalità in tempo reale.
- Cloud Functions: un codice di backend serverless eseguito in risposta a eventi o richieste HTTP.
- Firebase Authentication: un servizio di autenticazione gestito che si integra con altri prodotti Firebase.
- Firebase Hosting: hosting veloce e sicuro per le app web.
Collegherai l'app a Emulator Suite per abilitare lo sviluppo locale.
Imparerai inoltre a:
- Come connettere la tua app a Emulator Suite e come sono connessi i vari emulatori.
- Come funzionano le Regole di sicurezza Firebase e come testare le Regole di sicurezza Firestore su un emulatore locale.
- Come scrivere una funzione Firebase attivata da eventi Firestore e come scrivere test di integrazione eseguiti su Emulator Suite.
2. Configura
Ottieni il codice sorgente
In questo codelab, inizierai con una versione quasi completa dell'esempio di Fire Store, quindi la prima cosa da fare è clonare il codice sorgente:
$ git clone https://github.com/firebase/emulators-codelab.git
Poi vai alla directory del codelab, dove lavorerai per il resto del lab:
$ cd emulators-codelab/codelab-initial-state
Ora installa le dipendenze in modo da poter eseguire il codice. Se utilizzi una connessione a internet più lenta, l'operazione potrebbe richiedere un minuto o due:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Scarica l'interfaccia a riga di comando di Firebase
Emulator Suite fa parte dell'interfaccia a riga di comando di Firebase CLI, che può essere installata sulla tua macchina con il seguente comando:
$ npm install -g firebase-tools
Successivamente, verifica di avere la versione più recente della CLI. Questo codelab dovrebbe funzionare con la versione 9.0.0 o successive, ma le versioni successive includono ulteriori correzioni di bug.
$ firebase --version 9.6.0
Connettersi al progetto Firebase
Se non hai un progetto Firebase, crea un nuovo progetto Firebase nella Console Firebase. Prendi nota dell'ID progetto che scegli, ti servirà più avanti.
Ora dobbiamo collegare questo codice al tuo progetto Firebase. Per prima cosa, esegui questo comando per accedere all'interfaccia a riga di comando di Firebase:
$ firebase login
Quindi, esegui il comando seguente per creare un alias del progetto. Sostituisci $YOUR_PROJECT_ID
con l'ID del tuo progetto Firebase.
$ firebase use $YOUR_PROJECT_ID
Ora puoi eseguire l'app.
3. Esegui gli emulatori
In questa sezione eseguirai l'app localmente. Questo significa che è il momento di avviare Emulator Suite.
Avvia gli emulatori
Dalla directory di origine del codelab, esegui il seguente comando per avviare gli emulatori:
$ firebase emulators:start --import=./seed
Dovresti vedere un output simile al seguente:
$ 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.
Quando visualizzi il messaggio Tutti gli emulatori iniziati, l'app è pronta per essere usata.
Collegare l'app web agli emulatori
Dalla tabella nei log possiamo vedere che l'emulatore Cloud Firestore è in ascolto sulla porta 8080
e l'emulatore Authentication è in ascolto sulla porta 9099
.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
Collega il codice frontend all'emulatore anziché alla produzione. Apri il file public/js/homepage.js
e trova la funzione onDocumentReady
. Possiamo notare che il codice accede alle istanze Firestore e Auth standard:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Aggiorniamo gli oggetti db
e auth
in modo che puntino agli emulatori locali:
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);
}
Ora, quando l'app è in esecuzione sulla tua macchina locale (fornita dall'emulatore Hosting), il client Firestore punta anche all'emulatore locale anziché a un database di produzione.
Apri EmulatorUI
Nel browser web, vai all'indirizzo http://127.0.0.1:4000/. Dovresti vedere l'interfaccia utente di Emulator Suite.
Fai clic per visualizzare l'interfaccia utente dell'emulatore Firestore. La raccolta items
contiene già dati a causa di dati importati con il flag --import
.
4. Esegui l'app
Apri l'app
Nel browser web, vai all'indirizzo http://127.0.0.1:5000 e dovresti vedere The Fire Store in esecuzione in locale sul tuo computer.
Utilizzare l'app
Scegli un articolo nella home page e fai clic su Aggiungi al carrello. Purtroppo, riceverai il seguente errore:
Risolviamo il bug! Poiché tutto viene eseguito negli emulatori, possiamo fare esperimenti senza preoccuparci di influire sui dati reali.
5. Esegui il debug dell'app
Trovare il bug
Diamo un'occhiata alla console per gli sviluppatori di Chrome. Premi Control+Shift+J
(Windows, Linux, ChromeOS) o Command+Option+J
(Mac) per visualizzare l'errore nella console:
Sembra che si sia verificato un errore nel metodo addToCart
. Diamo un'occhiata. Dove proviamo ad accedere a un elemento chiamato uid
in quel metodo e perché dovrebbe essere null
? Al momento il metodo ha un aspetto simile al seguente 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 ah! Non abbiamo eseguito l'accesso all'app. In base alla documentazione su Firebase Authentication, quando non abbiamo eseguito l'accesso, il valore di auth.currentUser
è null
. Aggiungiamo un controllo:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Testare l'app
Ora aggiorna la pagina e fai clic su Aggiungi al carrello. Questa volta dovresti ricevere un errore più chiaro:
Tuttavia, se fai clic su Accedi nella barra degli strumenti in alto, quindi fai di nuovo clic su Aggiungi al carrello, vedrai che il carrello è aggiornato.
Tuttavia, sembra che i numeri non siano corretti:
Non preoccuparti, correggeremo il bug a breve. Innanzitutto, analizziamo cosa è successo quando hai aggiunto un articolo al carrello.
6. Trigger delle funzioni locali
Se fai clic su Aggiungi al carrello, viene avviata una catena di eventi che coinvolge più emulatori. Dopo aver aggiunto un articolo al carrello, nei log dell'interfaccia a riga di comando di Firebase dovresti vedere un messaggio simile al seguente:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Si sono verificati quattro eventi chiave per produrre questi log e l'aggiornamento dell'interfaccia utente osservato:
1) Scrittura Firestore - Client
Viene aggiunto un nuovo documento alla raccolta Firestore /carts/{cartId}/items/{itemId}/
. Puoi vedere questo codice nella funzione addToCart
all'interno di 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 Functions attivata
La funzione Cloud Functions calculateCart
rimane in ascolto di qualsiasi evento di scrittura (creazione, aggiornamento o eliminazione) degli articoli del carrello utilizzando l'attivatore onWrite
, che puoi vedere in functions/index.js
:
functions/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) Scrittura Firestore - Amministratore
La funzione calculateCart
legge tutti gli articoli nel carrello e somma la quantità totale e il prezzo, quindi aggiorna il documento "cart" con i nuovi totali (vedi cartRef.update(...)
sopra).
4) Lettura Firestore - Client
Il frontend web è iscritto per ricevere aggiornamenti sulle modifiche al carrello. Riceve un aggiornamento in tempo reale dopo che la funzione Cloud scrive i nuovi totali e aggiorna l'interfaccia utente, come puoi vedere in public/js/homepage.js
:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Riepilogo
Ottimo! Devi solo configurare un'app completamente locale che utilizza tre diversi emulatori Firebase per i test completamente locali.
Ma non è tutto! Nella sezione successiva scoprirai:
- Come scrivere test delle unità 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 in lettura e scrittura ai dati. Emulator Suite è un ottimo modo per creare un prototipo di queste regole.
Nell'editor, apri il file emulators-codelab/codelab-initial-state/firestore.rules
. Noterai che le regole includono tre sezioni principali:
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;
}
}
}
Al momento chiunque può leggere e scrivere dati nel nostro database. Vogliamo assicurarci che vengano eseguite solo operazioni valide e che non vengano divulgate informazioni sensibili.
Durante questo codelab, seguendo il Principio del privilegio minimo, bloccheremo tutti i documenti e aggiungeremo gradualmente l'accesso finché tutti gli utenti non avranno tutto l'accesso di cui hanno bisogno, ma non di più. Aggiorna le prime due regole per negare l'accesso impostando la condizione su 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 emulatori e test
Avvia gli emulatori
Nella riga di comando, assicurati di essere in emulators-codelab/codelab-initial-state/
. È possibile che gli emulatori siano ancora in esecuzione dai passaggi precedenti. In caso contrario, riavvia gli emulatori:
$ firebase emulators:start --import=./seed
Una volta avviati gli emulatori, puoi eseguire test locali su di essi.
Esegui i test
Usa la riga di comando in una nuova scheda del terminale dalla directory emulators-codelab/codelab-initial-state/
Per prima cosa, spostati nella directory delle funzioni (resteremo qui per il resto del codelab):
$ cd functions
Ora esegui i test mocha nella directory delle funzioni 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
Al momento abbiamo quattro errori. Man mano che crei il file delle regole, puoi misurare i progressi osservando il superamento di un numero maggiore di test.
9. Accesso sicuro al carrello
I primi due errori sono i test del "carrello degli acquisti", che verificano che:
- Gli utenti possono creare e aggiornare solo i propri carrelli
- Gli utenti possono leggere solo i propri carrelli
functions/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. Nell'editor, apri il file delle regole di sicurezza, firestore.rules
, e aggiorna le istruzioni all'interno di 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;
}
// ...
}
}
Ora queste regole consentono l'accesso in lettura e scrittura solo al proprietario del carrello.
Per verificare i dati in entrata e l'autenticazione dell'utente, utilizziamo due oggetti disponibili nel contesto di ogni regola:
- L'oggetto
request
contiene dati e metadati sull'operazione in corso. - Se un progetto Firebase utilizza l'autenticazione Firebase, l'oggetto
request.auth
descrive l'utente che effettua la richiesta.
10. Testa l'accesso al carrello
Emulator Suite aggiorna automaticamente le regole ogni volta che firestore.rules
viene salvato. Puoi verificare che l'emulatore abbia aggiornato le regole cercando nella scheda che esegue l'emulatore per il messaggio Rules updated
:
Esegui di nuovo i test e verifica che i primi due 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
Ottimo lavoro! Ora hai ottenuto l'accesso ai carrelli degli acquisti. Passiamo al prossimo test non riuscito.
11. Controlla il flusso "Aggiungi al carrello" nell'interfaccia utente
Al momento, anche se i proprietari del carrello possono leggere e scrivere nel carrello, non possono leggere o scrivere i singoli articoli al suo interno. Il motivo è che, sebbene i proprietari abbiano accesso al documento del carrello, non hanno accesso alla sottoraccolta degli articoli del carrello.
Questo è uno stato non valido per gli utenti.
Torna all'interfaccia utente web in esecuzione su http://127.0.0.1:5000,
e prova ad aggiungere un articolo al carrello. Viene visualizzato un errore Permission Denied
, visibile dalla console di debug, perché non abbiamo ancora concesso agli utenti l'accesso ai documenti creati nella sottoraccolta items
.
12. Consenti l'accesso agli articoli del carrello
Questi due test confermano che gli utenti possono aggiungere articoli al carrello o leggere articoli solo 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
}));
});
Possiamo quindi scrivere una regola che consenta l'accesso se l'utente corrente ha lo stesso UID del proprietario nel documento del carrello. Poiché non è necessario specificare regole diverse per create, update, delete
, puoi utilizzare una regola write
, che si applica a tutte le richieste che modificano i dati.
Aggiorna la regola per i documenti nella raccolta secondaria degli elementi. Il get
nella condizione legge un valore da Firestore, in questo caso il ownerUID
nel documento del carrello.
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. Testa l'accesso agli articoli del carrello
Ora possiamo ripetere il test. Scorri fino alla parte superiore dell'output e controlla che altri test abbiano esito positivo:
$ 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
Bene! Ora tutti i nostri test vengono superati. Abbiamo un test in attesa, ma lo faremo in pochi passaggi.
14. Controlla di nuovo il flusso "Aggiungi al carrello"
Torna al front-end web (http://127.0.0.1:5000) e aggiungi un articolo al carrello. Questo è un passaggio importante per verificare che i nostri test e le nostre regole corrispondano alla funzionalità richiesta dal cliente. Ricorda che l'ultima volta che abbiamo provato l'interfaccia utente, gli utenti non sono riusciti ad aggiungere articoli al carrello.
Il client ricarica automaticamente le regole quando viene salvato firestore.rules
. Prova quindi ad aggiungere un articolo al carrello.
Riepilogo
Ottimo! Hai appena migliorato la sicurezza della tua app, un passaggio essenziale per prepararla alla produzione. Se si trattasse di un'app di produzione, potremmo aggiungere questi test alla nostra pipeline di integrazione continua. In questo modo potremo continuare ad avere la certezza che in futuro i dati del nostro carrello degli acquisti avranno questi controlli di accesso, anche se altri utenti stanno modificando le regole.
Ma non è tutto.
Se continui, scoprirai:
- Come scrivere una funzione attivata da un evento Firestore
- Come creare test che funzionino 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 Firestore. Tuttavia, questa app utilizza anche Cloud Functions per mantenere aggiornato il carrello dell'utente, quindi vogliamo testare anche quel codice.
Emulator Suite semplifica il test di Cloud Functions, anche le funzioni che utilizzano Cloud Firestore e altri servizi.
Nell'editor, apri il file emulators-codelab/codelab-initial-state/functions/test.js
e scorri fino all'ultimo test nel file. Al momento è contrassegnato come In attesa:
// 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 attivare il test, rimuovi .skip
, in modo che il codice sia simile al seguente:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Poi, individua la variabile REAL_FIREBASE_PROJECT_ID
nella parte superiore del file e sostituiscila con il tuo ID progetto Firebase reale:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Se dimentichi l'ID progetto, puoi trovarlo nelle impostazioni progetto della Console Firebase:
16. Scopri come eseguire i test di Functions
Poiché questo test convalida l'interazione tra Cloud Firestore e Cloud Functions, richiede più configurazioni rispetto ai test dei codelab precedenti. Vediamo come funziona questo test e facciamoci un'idea di cosa ci si aspetta.
Creare un carrello
Cloud Functions viene eseguito in un ambiente server attendibile e può utilizzare l'autenticazione dell'account di servizio utilizzata dall'SDK Admin. Innanzitutto, inizializza un'app utilizzando initializeAdminApp
anziché initializeApp
. Quindi, crea un DocumentReference per il carrello a cui aggiungeremo gli articoli e inizializzalo:
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, aggiungi documenti alla sottoraccolta items
del documento del carrello per attivare 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 });
...
});
});
Definire le aspettative relative al test
Utilizza onSnapshot()
per registrare un listener per eventuali modifiche al documento del carrello. onSnapshot()
restituisce una funzione che puoi chiamare per annullare la registrazione dell'ascoltatore.
Per questo test, aggiungi due articoli che insieme costano 9,98 $. Poi controlla se il carrello contiene itemCount
e totalPrice
come previsto. In questo caso, la funzione ha svolto il proprio 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. Esegui i test
Gli emulatori potrebbero essere ancora in esecuzione dai test precedenti. In caso contrario, avvia gli emulatori. Dalla riga di comando, esegui
$ firebase emulators:start --import=./seed
Apri una nuova scheda del terminale (lascia in esecuzione gli emulatori) e vai alla directory delle funzioni. Potresti ancora avere questa opzione aperta dai test delle regole di sicurezza.
$ cd functions
Ora esegui i test delle unità, dovresti vedere in totale 5 test:
$ 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 esamini l'errore specifico, sembra che si tratti di un errore di timeout. Questo accade perché il test è in attesa dell'aggiornamento corretto della funzione, che però non avviene mai. Ora è tutto pronto per scrivere la funzione che soddisfi il test.
18. Scrivi una funzione
Per correggere questo test, devi aggiornare la funzione in functions/index.js
. Anche se alcune di questa funzione sono 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 imposta correttamente il riferimento al carrello, ma poi, invece di calcolare i valori di totalPrice
e itemCount
, li aggiorna a quelli impostati come hardcoded.
Recupera e ripeti
items
subcollection
Inizializza una nuova costante, itemsSnap
, che sarà la sottoraccolta items
. Quindi, esegui l'iterazione di tutti i documenti della 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) {
}
});
Calcolo di totalPrice e itemCount
Innanzitutto, inizializziamo i valori di totalPrice
e itemCount
a zero.
Quindi, aggiungi la logica al blocco di iterazione. Innanzitutto, verifica che l'articolo abbia un prezzo. Se per l'articolo non è specificata una quantità, lascia il valore predefinito 1
. Aggiungi poi la quantità al totale parziale di itemCount
. Infine, aggiungi il prezzo dell'articolo moltiplicato per la quantità al totale parziale 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 facilitare 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. Esegui di nuovo i test
Nella riga di comando, assicurati che gli emulatori siano ancora in esecuzione ed esegui di nuovo 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)
Ottimo lavoro!
20. Provare la funzionalità utilizzando l'interfaccia utente della vetrina
Per il test finale, torna all'app web ( http://127.0.0.1:5000/) e aggiungi un articolo al carrello.
Verifica che il carrello venga aggiornato con il totale corretto. Ottimo!
Riepilogo
Hai esaminato un caso di test complesso tra Cloud Functions for Firebase e Cloud Firestore. Hai scritto una funzione Cloud Functions per far superare il test. Hai anche confermato che la nuova funzionalità è attiva nell'interfaccia utente. Hai eseguito tutto in locale, eseguendo gli emulatori sulla tua macchina.
Inoltre, hai creato un client web in esecuzione sugli emulatori locali, hai personalizzato regole di sicurezza per proteggere i dati e hai testato le regole di sicurezza utilizzando gli emulatori locali.