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 versione successiva (per installare Node.js, utilizzare nvm , per verificare la versione, eseguire
node --version
) - Java 7 o successivo (per installare Java usa queste istruzioni , per verificare la tua versione, esegui
java -version
)
Cosa farai
In questo codelab eseguirai ed eseguirai il debug di una semplice app per lo shopping online basata su più servizi Firebase:
- Cloud Firestore: un database NoSQL serverless, scalabile a livello globale con funzionalità in tempo reale.
- Cloud Functions : un codice backend serverless che viene eseguito in risposta a eventi o richieste HTTP.
- Autenticazione Firebase : un servizio di autenticazione gestito che si integra con altri prodotti Firebase.
- Hosting Firebase : hosting veloce e sicuro per app web.
Collegherai l'app all'Emulator Suite per abilitare lo sviluppo locale.
Imparerai anche come:
- Come collegare la tua app all'Emulator Suite e come sono collegati i vari emulatori.
- Come funzionano le regole di sicurezza Firebase e come testare le regole di sicurezza Firestore rispetto a un emulatore locale.
- Come scrivere una funzione Firebase che viene attivata dagli eventi Firestore e come scrivere test di integrazione che vengono eseguiti con Emulator Suite.
2. Configurazione
Ottieni il codice sorgente
In questo codelab inizi con una versione dell'esempio di The Fire Store 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 la tua connessione 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 ../
Ottieni la CLI Firebase
Emulator Suite fa parte della CLI Firebase (interfaccia della riga di comando) che può essere installata sul tuo computer con il seguente comando:
$ npm install -g firebase-tools
Successivamente, conferma di avere la versione più recente 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 disponi di un progetto Firebase, nella console Firebase crea un nuovo progetto Firebase. Prendi nota dell'ID progetto che scegli, ti servirà in seguito.
Ora dobbiamo collegare questo codice al tuo progetto Firebase. Per prima cosa esegui il comando seguente per accedere alla CLI Firebase:
$ firebase login
Quindi esegui il comando seguente per creare un alias di progetto. Sostituisci $YOUR_PROJECT_ID
con l'ID del tuo progetto Firebase.
$ firebase use $YOUR_PROJECT_ID
Ora sei pronto per eseguire l'app!
3. Eseguire 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 codelab, esegui 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://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.
Una volta visualizzato il messaggio Tutti gli emulatori avviati , l'app è pronta per l'uso.
Collega l'app Web agli emulatori
In base alla tabella nei log possiamo vedere che 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 │ 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 │ └────────────────┴────────────────┴─────────────────────────────────┘
Colleghiamo il tuo codice frontend all'emulatore, anziché alla produzione. Apri il file public/js/homepage.js
e trova la funzione onDocumentReady
. Possiamo vedere che il codice accede alle istanze standard Firestore e Auth:
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 sul tuo computer locale (servito dall'emulatore di hosting), il client Firestore punta anche all'emulatore locale anziché a un database di produzione.
Apri l'EmulatoreUI
Nel browser Web, accedere a http://127.0.0.1:4000/ . Dovresti vedere l'interfaccia utente di Emulator Suite.
Fare clic per visualizzare l'interfaccia utente dell'emulatore Firestore. La raccolta items
contiene già dati a causa dei dati importati con il flag --import
.
4. Esegui l'app
Apri l'applicazione
Nel tuo browser web, vai a http://127.0.0.1:5000 e dovresti vedere The Fire Store in esecuzione localmente sul tuo computer!
Usa l'app
Scegli un articolo sulla home page e fai clic su Aggiungi al carrello . Sfortunatamente, ti imbatterai nel seguente errore:
Risolviamo quel bug! Poiché tutto è in esecuzione negli emulatori, possiamo sperimentare senza preoccuparci di influenzare i dati reali.
5. Eseguire il debug dell'app
Trova l'insetto
Ok, diamo un'occhiata alla console per sviluppatori di Chrome. Premi Control+Shift+J
(Windows, Linux, Chrome OS) o Command+Option+J
(Mac) per visualizzare l'errore sulla console:
Sembra che ci sia stato qualche errore nel metodo addToCart
, diamo un'occhiata a quello. Dove proviamo ad accedere a qualcosa chiamato uid
in quel metodo e perché dovrebbe essere null
? In questo momento il metodo è simile a 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 i documenti di autenticazione di Firebase , quando non abbiamo effettuato l'accesso, 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 aggiorna la pagina e fai clic su Aggiungi al carrello . Dovresti ottenere un errore più carino questa volta:
Ma se fai clic su Accedi nella barra degli strumenti superiore e poi fai nuovamente clic su Aggiungi al carrello , vedrai che il carrello è aggiornato.
Tuttavia, non sembra affatto che i numeri siano corretti:
Non preoccuparti, risolveremo presto il bug. Innanzitutto, approfondiamo ciò che è realmente accaduto quando hai aggiunto un articolo al carrello.
6. Trigger di funzioni locali
Facendo clic su Aggiungi al carrello si avvia una catena di eventi che coinvolgono più emulatori. Nei log della CLI di Firebase, dovresti vedere qualcosa di simile ai seguenti messaggi dopo aver aggiunto un articolo al carrello:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Si sono verificati quattro eventi chiave che hanno portato alla produzione di tali log e dell'aggiornamento dell'interfaccia utente osservato:
1) Scrittura Firestore - Client
Un nuovo documento viene aggiunto 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 attivata
La funzione Cloud Function calculateCart
ascolta eventuali eventi di scrittura (creazione, aggiornamento o eliminazione) che si verificano sugli articoli del carrello utilizzando il trigger onWrite
, che puoi vedere in functions/index.js
:
funzioni/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 - Amministrazione
La funzione calculateCart
legge tutti gli articoli nel carrello e somma la quantità totale e il prezzo, quindi aggiorna il documento "carrello" con i nuovi totali (vedere 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 Cloud Function ha scritto i nuovi totali e aggiornato 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
// ...
});
Ricapitolare
Bel lavoro! Devi semplicemente configurare un'app completamente locale che utilizza tre diversi emulatori Firebase per test completamente locali.
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 in lettura e scrittura ai dati. L'Emulator Suite è un ottimo modo per prototipare queste regole.
Nell'editor, apri 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 le 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ù. Aggiorniamo 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. Eseguire gli emulatori e i test
Avvia gli emulatori
Sulla riga di comando, assicurati di essere in emulators-codelab/codelab-initial-state/
. Potresti avere ancora gli emulatori in esecuzione dai passaggi precedenti. In caso contrario, avvia nuovamente gli emulatori:
$ firebase emulators:start --import=./seed
Una volta che gli emulatori sono in esecuzione, puoi eseguire test localmente su di essi.
Esegui i test
Sulla riga di comando in una nuova scheda del terminale dalla directory emulators-codelab/codelab-initial-state/
Per prima cosa spostati nella directory delle funzioni (rimarremo 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
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 queste prove. 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;
}
// ...
}
}
Queste regole ora consentono solo l'accesso in lettura e scrittura da parte del 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 che si sta tentando. - Se un progetto Firebase utilizza Firebase Authentication , l'oggetto
request.auth
descrive l'utente che sta effettuando la richiesta.
10. Testare l'accesso al carrello
Emulator Suite aggiorna automaticamente le regole ogni volta che firestore.rules
viene salvato. Puoi confermare che l'emulatore ha aggiornato le regole cercando nella scheda che esegue l'emulatore il messaggio Rules updated
:
Eseguire nuovamente i test e verificare che i primi due test vengano 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
Al momento, anche se i proprietari del carrello leggono e scrivono nel carrello, non possono leggere o scrivere i singoli articoli nel carrello. Questo perché, sebbene i proprietari abbiano accesso al documento del carrello, non hanno accesso alla sottoraccolta degli articoli del carrello.
Questo è uno stato interrotto per gli utenti.
Torna all'interfaccia utente web, che è in esecuzione su http://127.0.0.1:5000,
e prova ad aggiungere qualcosa al carrello. Ricevi 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 solo aggiungere 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 consenta l'accesso se l'utente corrente ha lo stesso UID dell'ownerUID sul documento del carrello. Poiché non è necessario specificare regole diverse per create, update, delete
, è possibile utilizzare una regola write
, che si applica a tutte le richieste che modificano i dati.
Aggiorna la regola per i documenti nella sottoraccolta elementi. Il condizionale get
sta leggendo un valore da Firestore, in questo caso, l' ownerUID
sul 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. Testare l'accesso agli articoli del carrello
Ora possiamo ripetere il test. Scorri fino all'inizio dell'output e controlla 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
Carino! Adesso tutti i nostri test sono superati. Abbiamo un test in sospeso, ma ci arriveremo tra pochi passaggi.
14. Controlla nuovamente il flusso "aggiungi al carrello".
Ritorna al front-end web ( http://127.0.0.1:5000 ) e aggiungi un articolo al carrello. Questo è un passo importante per confermare che i nostri test e le nostre regole corrispondono alla funzionalità richiesta dal cliente. (Ricorda che l'ultima volta che abbiamo provato l'interfaccia utente, gli utenti non erano in grado di aggiungere articoli al carrello!)
Il client ricarica automaticamente le regole quando 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 per la produzione! Se si trattasse di un'app di produzione, potremmo aggiungere questi test alla nostra pipeline di integrazione continua. Ciò ci darebbe fiducia in futuro sul fatto che i dati del nostro carrello degli acquisti avranno questi controlli di accesso, anche se altri stanno modificando le regole.
Ma aspetta, c'è di più!
se continui imparerai:
- 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 di Firestore. Ma questa app utilizza anche Cloud Functions 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.
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 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, rimuovi .skip
, quindi apparirà così:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Successivamente, trova la variabile REAL_FIREBASE_PROJECT_ID
nella parte superiore del file e modificala con il tuo vero ID progetto Firebase:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Se hai dimenticato l'ID progetto, puoi trovare l'ID progetto Firebase nelle Impostazioni progetto nella Console Firebase:
16. Esplora i test delle funzioni
Poiché questo test convalida l'interazione tra Cloud Firestore e Cloud Functions, richiede una maggiore configurazione rispetto ai test dei codelab precedenti. Esaminiamo questo test e facciamo 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. Innanzitutto, inizializzi un'app utilizzando initializeAdminApp
invece di initializeApp
. Quindi, crei un DocumentReference per il carrello a cui aggiungeremo gli articoli e inizializzeremo il carrello:
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 nostro 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 });
...
});
});
Stabilisci le aspettative del test
Utilizza onSnapshot()
per registrare un ascoltatore per eventuali modifiche sul documento del carrello. onSnapshot()
restituisce una funzione che puoi chiamare per annullare la registrazione del listener.
Per questo test, aggiungi due articoli che insieme costano $ 9,98. Quindi, controlla se il carrello ha gli itemCount
e totalPrice
previsti. Se è così, la funzione ha fatto 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. Esegui 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
Apri una nuova scheda del terminale (lascia gli emulatori in esecuzione) e spostati nella directory delle funzioni. Potresti averlo ancora aperto dai test delle regole di sicurezza.
$ cd functions
Ora esegui i test unitari, 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 attende 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 functions/index.js
. Sebbene parte di questa funzione sia scritta, non è completa. 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 invece di calcolare i valori di totalPrice
e itemCount
, li aggiorna a quelli hardcoded.
Recupera e scorri il file
sottoraccolta items
Inizializza una nuova costante, itemsSnap
, in modo che sia la sottoraccolta items
. 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 conteggio degli articoli
Innanzitutto, inizializziamo i valori di totalPrice
e itemCount
su zero.
Quindi, aggiungi la logica al nostro blocco di iterazione. Per prima cosa controlla che l'articolo abbia un prezzo. Se per l'articolo non è specificata una quantità, lascia che sia predefinita su 1
. Quindi, aggiungi 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 e di 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. Eseguire nuovamente 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 il test finale, torna all'app Web ( http://127.0.0.1:5000/ ) e aggiungi un articolo al carrello.
Conferma che il carrello si aggiorni con il totale corretto. Fantastico!
Ricapitolare
Hai seguito un complesso caso di test tra Cloud Functions for Firebase e Cloud Firestore. Hai scritto una funzione Cloud per superare il test. Hai anche confermato che la nuova funzionalità funziona nell'interfaccia utente! Hai fatto tutto questo localmente, eseguendo gli emulatori sul tuo computer.
Hai anche creato un client Web che viene eseguito contro gli emulatori locali, regole di sicurezza su misura per proteggere i dati e testato le regole di sicurezza utilizzando gli emulatori locali.