1. לפני שמתחילים
קל מאוד להשתמש בכלים של בקאנד ללא שרת (serverless) כמו Cloud Firestore ו-Cloud Functions, אבל יכול להיות קשה לבדוק אותם. הכלים לאמולטור מקומי ב-Firebase מאפשרים להריץ גרסאות מקומיות של השירותים האלה במחשב הפיתוח, כדי שתוכלו לפתח את האפליקציה במהירות ובבטחה.
דרישות מוקדמות
- כלי עריכה פשוט כמו Visual Studio Code, Atom או Sublime Text
- Node.js בגרסה 10.0.0 ואילך (כדי להתקין את Node.js, משתמשים ב-nvm, וכדי לבדוק את הגרסה, מריצים את
node --version) - Java 7 ואילך (כדי להתקין את Java, פועלים לפי ההוראות האלה. כדי לבדוק את הגרסה, מריצים את הפקודה
java -version)
הפעולות שתבצעו
ב-Codelab הזה, תפעילו ותנפו שגיאות באפליקציה פשוטה לקניות אונליין שמופעלת על ידי כמה שירותי Firebase:
- Cloud Firestore: מסד נתונים NoSQL בלי שרת (serverless) שניתן להתאים לעומס גלובלי, עם יכולות בזמן אמת.
- Cloud Functions: קוד קצה עורפי ללא שרת (serverless) שפועל בתגובה לאירועים או לבקשות HTTP.
- אימות ב-Firebase: שירות אימות מנוהל שמשתלב עם מוצרים אחרים של Firebase.
- אירוח ב-Firebase: אירוח מהיר ומאובטח של אפליקציות אינטרנט.
תחברו את האפליקציה ל-Emulator Suite כדי לאפשר פיתוח מקומי.

בנוסף, תלמדו איך:
- איך לקשר את האפליקציה לכלים לאמולטור ואיך האמולטורים השונים מקושרים.
- איך פועלים כללי האבטחה של Firebase ואיך בודקים את כללי אבטחה ב-Firestore באמצעות אמולטור מקומי.
- איך לכתוב פונקציית Firebase שמופעלת על ידי אירועים ב-Firestore ואיך לכתוב בדיקות שילוב שפועלות מול Emulator Suite.
2. הגדרה
קבלת קוד המקור
ב-Codelab הזה, תתחילו עם גרסה כמעט מלאה של האפליקציה לדוגמה The Fire Store, אז הדבר הראשון שצריך לעשות הוא לשכפל את קוד המקור:
$ git clone https://github.com/firebase/emulators-codelab.git
לאחר מכן עוברים לספריית ה-Codelab, שבה תעבדו בשאר הזמן של ה-Codelab הזה:
$ cd emulators-codelab/codelab-initial-state
עכשיו מתקינים את יחסי התלות כדי להריץ את הקוד. אם החיבור לאינטרנט איטי, יכול להיות שהתהליך יימשך דקה או שתיים:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
הורדת Firebase CLI
כלים לאמולטור הם חלק מ-Firebase CLI (ממשק שורת הפקודה) שאפשר להתקין במחשב באמצעות הפקודה הבאה:
$ npm install -g firebase-tools
בשלב הבא, מוודאים שמותקנת הגרסה העדכנית של ה-CLI. ה-Codelab הזה אמור לפעול עם גרסה 9.0.0 ואילך, אבל גרסאות מאוחרות יותר כוללות יותר תיקוני באגים.
$ firebase --version 9.6.0
קישור לפרויקט ב-Firebase
יצירת פרויקט ב-Firebase
- נכנסים למסוף Firebase באמצעות חשבון Google.
- לוחצים על הכפתור כדי ליצור פרויקט חדש, ואז מזינים שם לפרויקט (לדוגמה,
Emulators Codelab).
- לוחצים על המשך.
- אם מוצגת בקשה, קוראים ומאשרים את התנאים של Firebase ולוחצים על המשך.
- (אופציונלי) מפעילים את העזרה מבוססת-AI במסוף Firebase (נקראת "Gemini ב-Firebase").
- ב-Codelab הזה לא צריך את Google Analytics, אז משביתים את האפשרות של Google Analytics.
- לוחצים על Create project, ממתינים עד שהפרויקט יוקצה ואז לוחצים על Continue.
קישור הקוד לפרויקט ב-Firebase
עכשיו צריך לחבר את הקוד הזה לפרויקט Firebase. קודם כל, מריצים את הפקודה הבאה כדי להתחבר ל-Firebase CLI:
$ firebase login
לאחר מכן, מריצים את הפקודה הבאה כדי ליצור כינוי לפרויקט. מחליפים את $YOUR_PROJECT_ID במזהה של הפרויקט ב-Firebase.
$ firebase use $YOUR_PROJECT_ID
עכשיו אתם מוכנים להפעיל את האפליקציה.
3. הפעלת האמולטורים
בקטע הזה תפעילו את האפליקציה באופן מקומי. זה אומר שהגיע הזמן להפעיל את חבילת הכלים לאמולטור.
הפעלת האמולטורים
מתוך ספריית קובצי המקור של ה-Codelab, מריצים את הפקודה הבאה כדי להפעיל את האמולטורים:
$ firebase emulators:start --import=./seed
הפלט אמור להיראות כך:
$ 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.
כשההודעה All emulators started מופיעה, האפליקציה מוכנה לשימוש.
חיבור אפליקציית האינטרנט לאמולטורים
לפי הטבלה ביומנים, אפשר לראות שהאמולטור של Cloud Firestore מאזין ליציאה 8080 והאמולטור של Authentication מאזין ליציאה 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 │ └────────────────┴────────────────┴─────────────────────────────────┘
בואו נחבר את קוד החזית לאמולטור, במקום לסביבת הייצור. פותחים את הקובץ public/js/homepage.js ומוצאים את הפונקציה onDocumentReady. אפשר לראות שהקוד ניגש למכונות הרגילות של Firestore ו-Auth:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
עכשיו נעדכן את האובייקטים db ו-auth כך שיצביעו על האמולטורים המקומיים:
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);
}
עכשיו, כשהאפליקציה פועלת במכונה המקומית (מוצגת על ידי אמולטור האירוח), לקוח Firestore מצביע גם על האמולטור המקומי ולא על מסד נתונים בסביבת הייצור.
פתיחת EmulatorUI
בדפדפן האינטרנט, עוברים לכתובת http://127.0.0.1:4000/. ממשק המשתמש של חבילת הכלים לאמולטור אמור להופיע.

לוחצים כדי לראות את ממשק המשתמש של Firestore Emulator. האוסף items כבר מכיל נתונים בגלל ייבוא הנתונים עם הדגל --import.

4. הפעלת האפליקציה
פתיחת האפליקציה
בדפדפן האינטרנט, עוברים אל http://127.0.0.1:5000. עכשיו אתם אמורים לראות את Fire Store פועל באופן מקומי במכונה שלכם.

שימוש באפליקציה
בוחרים פריט בדף הבית ולוחצים על הוספה לעגלת הקניות. לצערנו, תופיע השגיאה הבאה:

בואו נתקן את הבאג הזה! מכיוון שהכול פועל באמולטורים, אנחנו יכולים להתנסות בלי לדאוג לגבי ההשפעה על נתונים אמיתיים.
5. ניפוי באגים באפליקציה
איך מוצאים את הבאג
אוקיי, בואו נסתכל בקונסולת המפתחים של Chrome. מקישים על Control+Shift+J (ב-Windows, ב-Linux וב-Chrome OS) או על Command+Option+J (ב-Mac) כדי לראות את השגיאה במסוף:

נראה שהייתה שגיאה ב-method addToCart, בואו נבדוק את זה. איפה אנחנו מנסים לגשת למשהו שנקרא uid בשיטה הזו, ולמה הוא יהיה null? כרגע, השיטה נראית כך ב-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);
}
Aha! אנחנו לא מחוברים לאפליקציה. לפי המסמכים של אימות ב-Firebase, כשאנחנו לא מחוברים, הערך של auth.currentUser הוא null. נוסיף בדיקה לכך:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
בדיקת האפליקציה
עכשיו מרעננים את הדף ולוחצים על הוספה לעגלת הקניות. הפעם אמורה להופיע שגיאה ברורה יותר:

אבל אם תלחצו על כניסה בסרגל הכלים העליון ואז תלחצו שוב על הוספה לעגלת הקניות, תראו שעגלת הקניות מתעדכנת.
אבל נראה שהמספרים לא נכונים בכלל:

אל דאגה, נתקן את הבאג הזה בקרוב. קודם כול, בואו נבין מה קורה בפועל כשמוסיפים פריט לעגלת הקניות.
6. טריגרים של פונקציות מקומיות
לחיצה על הוספה לעגלת הקניות מפעילה שרשרת אירועים שכוללת כמה אמולטורים. אחרי שתוסיפו פריט לעגלת הקניות, ביומנים של Firebase CLI אמורות להופיע הודעות כמו אלה:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
היו ארבעה אירועים מרכזיים שהתרחשו כדי ליצור את היומנים האלה ואת העדכון בממשק המשתמש שראיתם:

1) כתיבה ב-Firestore – לקוח
מסמך חדש נוסף לאוסף /carts/{cartId}/items/{itemId}/ ב-Firestore. אפשר לראות את הקוד הזה בפונקציה addToCart בתוך 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) הפעלת פונקציה של Cloud Functions
הפונקציה של Cloud Functions calculateCart מאזינה לכל אירועי הכתיבה (יצירה, עדכון או מחיקה) שמתרחשים בפריטים בעגלת הקניות באמצעות הטריגר onWrite, שאפשר לראות ב-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) Firestore Write – Admin
הפונקציה calculateCart קוראת את כל הפריטים בעגלת הקניות, מחשבת את הכמות והמחיר הכוללים ומעדכנת את המסמך "עגלת קניות" בסכומים הכוללים החדשים (ראו cartRef.update(...) למעלה).
4) Firestore Read – Client
ממשק האינטרנט נרשם לקבלת עדכונים על שינויים בעגלת הקניות. הוא מקבל עדכון בזמן אמת אחרי שהפונקציה של Cloud Functions כותבת את הסכומים הכוללים החדשים ומעדכנת את ממשק המשתמש, כמו שאפשר לראות ב-public/js/homepage.js:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
סיכום
כל הכבוד! עכשיו הגדרתם אפליקציה מקומית לחלוטין שמשתמשת בשלושה אמולטורים שונים של Firebase לבדיקה מקומית לחלוטין.

רק רגע, יש עוד! בקטע הבא נסביר:
- איך כותבים בדיקות יחידה (unit testing) שמשתמשות באמולטורים של Firebase.
- איך משתמשים באמולטורים של Firebase כדי לנפות באגים בכללי האבטחה.
7. יצירת כללי אבטחה שמותאמים לאפליקציה
אפליקציית האינטרנט שלנו קוראת וכותבת נתונים, אבל עד עכשיו לא ממש דאגנו לאבטחה. ב-Cloud Firestore יש מערכת שנקראת "כללי אבטחה" שמשמשת כדי להצהיר למי יש גישה לקרוא ולכתוב נתונים. חבילת הכלים לאמולטור היא דרך מצוינת ליצור אב טיפוס של הכללים האלה.
בכלי לעריכת טקסט, פותחים את הקובץ emulators-codelab/codelab-initial-state/firestore.rules. הכללים שלנו מחולקים לשלושה חלקים עיקריים:
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;
}
}
}
כרגע כל אחד יכול לקרוא נתונים ממסד הנתונים שלנו ולכתוב אליו נתונים. אנחנו רוצים לוודא שרק פעולות תקינות עוברות ושאנחנו לא מדליפים מידע רגיש.
ב-Codelab הזה, בהתאם לעיקרון של הרשאות מינימליות, ננעל את כל המסמכים ונוסיף גישה בהדרגה עד שלכל המשתמשים תהיה את כל הגישה שהם צריכים, אבל לא יותר מזה. בואו נעדכן את שני הכללים הראשונים כדי לדחות את הגישה, על ידי הגדרת התנאי ל-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. הפעלת האמולטורים והבדיקות
הפעלת האמולטורים
בשורת הפקודה, מוודאים שאתם נמצאים ב-emulators-codelab/codelab-initial-state/. יכול להיות שהאמולטורים מהשלבים הקודמים עדיין פועלים. אם לא, מפעילים שוב את הסימולטורים:
$ firebase emulators:start --import=./seed
אחרי שהאמולטורים פועלים, אפשר להריץ בדיקות באופן מקומי.
הרצת הבדיקות
בשורת הפקודה בכרטיסיית מסוף חדשה מהספרייה emulators-codelab/codelab-initial-state/
קודם עוברים לספריית הפונקציות (נישאר כאן עד סוף ה-Codelab):
$ cd functions
עכשיו מריצים את בדיקות Mocha בספריית הפונקציות וגוללים לחלק העליון של הפלט:
# 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
כרגע יש לנו ארבעה כשלים. בזמן שאתם יוצרים את קובץ הכללים, אתם יכולים למדוד את ההתקדמות שלכם לפי מספר הבדיקות שעוברות בהצלחה.
9. גישה מאובטחת לעגלת הקניות
שני הכשלים הראשונים הם בדיקות של "עגלת הקניות", שבודקות את הדברים הבאים:
- המשתמשים יכולים רק ליצור ולעדכן את עגלות הקניות שלהם
- המשתמשים יכולים לקרוא רק את עגלות הקניות שלהם
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());
});
בואו נגרום לבדיקות האלה לעבור. בעורך, פותחים את קובץ כללי האבטחה, firestore.rules, ומעדכנים את ההצהרות בתוך 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;
}
// ...
}
}
הכללים האלה מאפשרים עכשיו רק לבעלי העגלה גישה לקריאה ולכתיבה.
כדי לאמת את הנתונים הנכנסים ואת אימות המשתמש, אנחנו משתמשים בשני אובייקטים שזמינים בהקשר של כל כלל:
- האובייקט
requestמכיל נתונים ומטא-נתונים על הפעולה שמתבצע ניסיון לבצע. - אם בפרויקט Firebase משתמשים באימות ב-Firebase, האובייקט
request.authמתאר את המשתמש ששולח את הבקשה.
10. בדיקת הגישה לעגלת הקניות
הכלים לאמולטור מעדכנים את הכללים באופן אוטומטי בכל פעם ש-firestore.rules נשמר. כדי לוודא שהאמולטור עדכן את הכללים, מחפשים את ההודעה Rules updated בכרטיסייה שבה פועל האמולטור:

מריצים מחדש את הבדיקות ומוודאים ששתי הבדיקות הראשונות עוברות:
$ 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
יפה מאוד! עכשיו יש לכם גישה לעגלות הקניות. נעבור לבדיקה הבאה שנכשלה.
11. בודקים את התהליך "הוספה לעגלת הקניות" בממשק המשתמש
כרגע, למרות שבעלי העגלה יכולים לקרוא ולכתוב בעגלה שלהם, הם לא יכולים לקרוא או לכתוב פריטים בודדים בעגלה. הסיבה לכך היא שלבעלים יש גישה למסמך העגלה, אבל אין להם גישה לאוסף המשנה של הפריטים בעגלה.
זה מצב לא תקין מבחינת המשתמשים.
חוזרים לממשק המשתמש של האתר שפועל ב-http://127.0.0.1:5000, ומנסים להוסיף משהו לעגלת הקניות. תופיע השגיאה Permission Denied במסוף ניפוי הבאגים, כי עדיין לא נתנו למשתמשים גישה למסמכים שנוצרו באוסף המשנה items.
12. מתן גישה לפריטים בעגלת הקניות
שתי הבדיקות האלה מוודאות שמשתמשים יכולים להוסיף פריטים רק לעגלת הקניות שלהם או לקרוא פריטים רק ממנה:
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
}));
});
לכן, אנחנו יכולים לכתוב כלל שמאפשר גישה אם למשתמש הנוכחי יש את אותו UID כמו ה-ownerUID במסמך של עגלת הקניות. אין צורך לציין כללים שונים ל-create, update, delete, לכן אפשר להשתמש בכלל write, שחל על כל הבקשות שמשנות נתונים.
מעדכנים את הכלל של המסמכים באוסף המשני items. המשתנה get בתנאי קורא ערך מ-Firestore – במקרה הזה, ownerUID במסמך של עגלת הקניות.
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. בדיקת הגישה לפריטים בעגלת הקניות
עכשיו אפשר להריץ את הבדיקה שוב. גוללים לחלק העליון של הפלט ובודקים שיותר בדיקות עברו בהצלחה:
$ 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
יפה מאוד. עכשיו כל הבדיקות שלנו עוברות. יש לנו בדיקה אחת בהמתנה, אבל נגיע אליה בעוד כמה שלבים.
14. בודקים שוב את תהליך ההוספה לעגלת הקניות
חוזרים לממשק האינטרנט (http://127.0.0.1:5000) ומוסיפים פריט לעגלת הקניות. זהו שלב חשוב שמאפשר לנו לוודא שהבדיקות והכללים שלנו תואמים לפונקציונליות שהלקוח דורש. (זוכרים שבפעם האחרונה שניסינו את ממשק המשתמש, המשתמשים לא הצליחו להוסיף פריטים לעגלת הקניות?)

הלקוח טוען מחדש באופן אוטומטי את הכללים כששומרים את firestore.rules. כדאי לנסות להוסיף משהו לעגלת הקניות.
סיכום
כל הכבוד! שיפרת את האבטחה של האפליקציה, וזה שלב חיוני בהכנה שלה לסביבת הייצור. אם זו הייתה אפליקציה בסביבת ייצור, היינו יכולים להוסיף את הבדיקות האלה לפייפליין של האינטגרציה הרציפה. כך נוכל להיות בטוחים שגם אם מישהו אחר ישנה את הכללים, נתוני עגלת הקניות יהיו מוגנים באמצעות אמצעי בקרת הגישה האלה.

אבל רגע, יש עוד!
אם תמשיכו לקרוא, תלמדו:
- איך כותבים פונקציה שמופעלת על ידי אירוע ב-Firestore
- איך יוצרים בדיקות שפועלות בכמה אמולטורים
15. הגדרת בדיקות של Cloud Functions
עד עכשיו התמקדנו בקצה הקדמי של אפליקציית האינטרנט ובכללי האבטחה ב-Firestore. אבל האפליקציה הזו משתמשת גם ב-Cloud Functions כדי לעדכן את עגלת הקניות של המשתמש, אז אנחנו רוצים לבדוק גם את הקוד הזה.
בעזרת חבילת הכלים לאמולטור קל מאוד לבדוק פונקציות של Cloud Functions, אפילו פונקציות שמשתמשות ב-Cloud Firestore ובשירותים אחרים.
בכלי לעריכת קוד, פותחים את הקובץ emulators-codelab/codelab-initial-state/functions/test.js וגוללים לבדיקה האחרונה בקובץ. כרגע, הוא מסומן כ"בהמתנה":
// 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 () => {
...
});
});
כדי להפעיל את הבדיקה, מסירים את .skip, כך שהקוד ייראה כך:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
עכשיו, מוצאים את המשתנה REAL_FIREBASE_PROJECT_ID בחלק העליון של הקובץ ומשנים אותו למזהה הפרויקט האמיתי ב-Firebase:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
אם שכחתם את מזהה הפרויקט, אתם יכולים למצוא את מזהה הפרויקט ב-Firebase בהגדרות הפרויקט במסוף Firebase:

16. הדרכה על בדיקות פונקציונליות
הבדיקה הזו מאמתת את האינטראקציה בין Cloud Firestore לבין Cloud Functions, ולכן היא דורשת יותר הגדרות מהבדיקות ב-Codelabs הקודמים. בואו נעבור על הבדיקה הזו ונבין מה היא מצפה לקבל.
יצירת עגלת קניות
פונקציות Cloud Functions פועלות בסביבת שרת מהימנה ויכולות להשתמש באימות של חשבון השירות שמשמש את SDK לאדמינים . קודם כול, מפעילים אפליקציה באמצעות initializeAdminApp במקום initializeApp. אחר כך יוצרים DocumentReference לעגלת הקניות שאליה נוסיף פריטים, ומאתחלים את עגלת הקניות:
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 });
...
});
הפעלת הפונקציה
לאחר מכן, מוסיפים מסמכים לאוסף המשנה items של מסמך עגלת הקניות כדי להפעיל את הפונקציה. מוסיפים שני פריטים כדי לוודא שאתם בודקים את ההוספה שמתרחשת בפונקציה.
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 });
...
});
});
הגדרת ציפיות מהבדיקה
משתמשים ב-onSnapshot() כדי לרשום מאזין לכל שינוי במסמך של עגלת הקניות. הפונקציה onSnapshot() מחזירה פונקציה שאפשר להפעיל כדי לבטל את הרישום של ה-listener.
בבדיקה הזו, מוסיפים שני פריטים שהעלות הכוללת שלהם היא 9.98$. לאחר מכן, בודקים אם בעגלת הקניות יש את itemCount ו-totalPrice הצפויים. אם כן, הפונקציה עשתה את העבודה שלה.
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. הרצת הבדיקות
יכול להיות שהאמולטורים עדיין פועלים מהבדיקות הקודמות. אם לא, מפעילים את האמולטורים. מריצים את הפקודה הבאה משורת הפקודה:
$ firebase emulators:start --import=./seed
פותחים כרטיסיית טרמינל חדשה (משאירים את האמולטורים פועלים) ועוברים לספריית הפונקציות. יכול להיות שהכרטיסייה הזו עדיין פתוחה מהבדיקות של כללי האבטחה.
$ cd functions
עכשיו מריצים את בדיקות היחידה. אמורות להופיע 5 בדיקות בסך הכול:
$ 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
אם תסתכלו על הכישלון הספציפי, נראה שמדובר בשגיאת זמן קצוב לתפוגה. הסיבה לכך היא שהבדיקה ממתינה שהפונקציה תתעדכן בצורה נכונה, אבל זה לא קורה. עכשיו אנחנו מוכנים לכתוב את הפונקציה שתעבור את הבדיקה.
18. כתיבת פונקציה
כדי לתקן את הבדיקה הזו, צריך לעדכן את הפונקציה ב-functions/index.js. למרות שחלק מהפונקציה הזו כתוב, היא לא שלמה. כך הפונקציה נראית כרגע:
// 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) {
}
});
הפונקציה מגדירה בצורה נכונה את ההפניה לעגלת הקניות, אבל במקום לחשב את הערכים של totalPrice ו-itemCount, היא מעדכנת אותם לערכים בתוך הקוד.
אחזור ואיטרציה של
items subcollection
מאתחלים קבוע חדש, itemsSnap, שיהיה אוסף המשנה items. לאחר מכן, עוברים על כל המסמכים באוסף.
// 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) {
}
});
חישוב של totalPrice ו-itemCount
קודם כל, נאתחל את הערכים של totalPrice ו-itemCount לאפס.
אחר כך מוסיפים את הלוגיקה לבלוק החזרה. קודם כל, בודקים אם לפריט יש מחיר. אם לא צוינה כמות לפריט, ברירת המחדל היא 1. לאחר מכן, מוסיפים את הכמות לסכום המצטבר של itemCount. לבסוף, מוסיפים את מחיר הפריט כפול הכמות לסכום המצטבר של 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) {
}
});
אפשר גם להוסיף רישום ביומן כדי לנפות באגים במצבי הצלחה ומצב שגיאה:
// 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. הפעלה מחדש של הבדיקות
בשורת הפקודה, מוודאים שהאמולטורים עדיין פועלים ומריצים מחדש את הבדיקות. אין צורך להפעיל מחדש את הסימולטורים כי הם מזהים את השינויים בפונקציות באופן אוטומטי. כל הבדיקות אמורות לעבור:
$ 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)
יפה מאוד!
20. איך מנסים את זה בממשק המשתמש של Storefront
לבדיקה האחרונה, חוזרים לאפליקציית האינטרנט (http://127.0.0.1:5000/) ומוסיפים פריט לעגלת הקניות.

מוודאים שהסכום הכולל בעגלת הקניות מתעדכן בצורה נכונה. נהדר!
סיכום
עברתם על מקרה בדיקה מורכב בין Cloud Functions for Firebase לבין Cloud Firestore. כתבתם פונקציה של Cloud Functions כדי שהבדיקה תעבור. וגם וידאתם שהפונקציונליות החדשה פועלת בממשק המשתמש. עשיתם את כל זה באופן מקומי, והפעלתם את האמולטורים במחשב שלכם.
בנוסף, יצרתם לקוח אינטרנט שפועל מול האמולטורים המקומיים, התאמתם כללי אבטחה כדי להגן על הנתונים ובדקתם את כללי האבטחה באמצעות האמולטורים המקומיים.
