פיתוח מקומי באמצעות חבילה של אמולטור ב-Firebase

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)

מה תעשו

בקודלאב הזה תלמדו איך להפעיל ולפתור באגים באפליקציית שופינג פשוטה באינטרנט שמבוססת על כמה שירותי Firebase:

  • Cloud Firestore: מסד נתונים מסוג NoSQL ללא שרת (serverless) עם יכולות בזמן אמת, שניתן להתאים אותו בקנה מידה גלובלי.
  • Cloud Functions: קוד ללא שרת (serverless) שרץ בתגובה לאירועים או לבקשות HTTP.
  • אימות ב-Firebase: שירות מנוהל לאימות שמשתלב עם מוצרי Firebase אחרים.
  • אירוח ב-Firebase: אירוח מהיר ומאובטח לאפליקציות אינטרנט.

כדי לאפשר פיתוח מקומי, תצטרכו לחבר את האפליקציה ל-Emulator Suite.

2589e2f95b74fa88.png

בנוסף, תלמדו איך:

  • איך לחבר את האפליקציה לחבילת האמולטור ואיך מחוברים האמולטורים השונים.
  • הסבר על כללי האבטחה של Firebase ועל בדיקת כללי האבטחה של Firestore באמולטור מקומי.
  • איך כותבים פונקציית Firebase שמופעלת על ידי אירועי Firestore, ואיך כותבים בדיקות שילוב שפועלות מול Emulator Suite.

2. הגדרה

קבלת קוד המקור

ב-Codelab הזה, מתחילים עם גרסה של דוגמת 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 ../

הורדת ה-CLI של Firebase

חבילת האמולטור היא חלק מה-CLI של Firebase (ממשק שורת הפקודה) שניתן להתקין במחשב באמצעות הפקודה הבאה:

$ npm install -g firebase-tools

בשלב הבא, מוודאים שמשתמשים בגרסה האחרונה של CLI. סדנת הקוד הזו אמורה לפעול בגרסה 9.0.0 ואילך, אבל גרסאות מאוחרות יותר כוללות תיקוני באגים נוספים.

$ firebase --version
9.6.0

קישור לפרויקט Firebase

אם אין לכם פרויקט ב-Firebase, אתם יכולים ליצור פרויקט חדש ב-Firebase במסוף Firebase. חשוב לזכור את מזהה הפרויקט שבחרתם, כי תצטרכו אותו בהמשך.

עכשיו אנחנו צריכים לחבר את הקוד הזה לפרויקט Firebase שלכם. קודם כול מריצים את הפקודה הבאה כדי להתחבר ל-CLI של Firebase:

$ firebase login

לאחר מכן, מריצים את הפקודה הבאה כדי ליצור כינוי לפרויקט. מחליפים את $YOUR_PROJECT_ID במזהה הפרויקט ב-Firebase.

$ firebase use $YOUR_PROJECT_ID

עכשיו אפשר להריץ את האפליקציה.

3. הפעלת האמולטורים

בקטע הזה, האפליקציה תופעל באופן מקומי. זה אומר שהגיע הזמן להפעיל את Emulator Suite.

הפעלת האמולטורים

מתוך ספריית המקור של 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.

כשמופיעה ההודעה כל האמולטורים הופעלו, האפליקציה מוכנה לשימוש.

חיבור אפליקציית האינטרנט לאמולטורים

לפי הטבלה ביומנים, אפשר לראות שאמולטור Cloud Firestore מאזין ביציאה 8080 ושהאמולטור האימות מאזין ביציאה 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. האוסף items כבר מכיל נתונים בגלל הנתונים שיובאו עם הדגל --import.

4ef88d0148405d36.png

4. הפעלת האפליקציה

פתיחת האפליקציה

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

939f87946bac2ee4.png

שימוש באפליקציה

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

a11bd59933a8e885.png

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

5. ניפוי באגים באפליקציה

איתור הבאג

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

74c45df55291dab1.png

נראה שאירעה שגיאה כלשהי בשיטה 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);
  }

אהה! אנחנו לא מחוברים לאפליקציה. לפי מסמכי האימות של 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;
    }

    // ...
  }

בדיקת האפליקציה

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

c65f6c05588133f7.png

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

עם זאת, נראה שהמספרים לא נכונים בכלל:

239f26f02f959eef.png

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

6. טריגרים של פונקציות מקומיות

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

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

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

68c9323f2ad10f7a.png

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 – אדמין

הפונקציה calculateCart קוראת את כל הפריטים בעגלת הקניות ומוסיפה את הסכום הכולל ואת המחיר הכולל, ולאחר מכן היא מעדכנת את המסמך 'עגלת קניות' בסכומים החדשים (ראו cartRef.update(...) למעלה).

4) קריאה ב-Firestore – לקוח

ממשק הקצה הקדמי באינטרנט רשום לקבלת עדכונים על שינויים בעגלת הקניות. היא מקבלת עדכון בזמן אמת אחרי שהפונקציה של Cloud Functions כותבת את הסכומים החדשים ומעדכנת את ממשק המשתמש, כפי שניתן לראות ב-public/js/homepage.js:

public/js/homepage.js

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

סיכום

כל הכבוד! עכשיו הגדרתם אפליקציה מקומית מלאה שמשתמשת בשלושה אמולטורים שונים של Firebase לבדיקה מקומית מלאה.

db82eef1706c9058.gif

רק רגע, יש עוד! בקטע הבא תלמדו:

  • איך כותבים בדיקות יחידה שמשתמשות במהדמנים של 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/

קודם עוברים לספריית הפונקציות (נשאר כאן עד סוף הקודלאב):

$ 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. בדיקת הגישה לעגלת הקניות

ערכת ה-Emulator Suite מעדכנת את הכללים באופן אוטומטי בכל פעם ש-firestore.rules נשמר. כדי לוודא שהכללים עודכנו במהלך ההרצה של הסימולטור, מחפשים את ההודעה Rules updated בכרטיסייה שבה פועל הסימולטור:

5680da418b420226.png

מריצים מחדש את הבדיקות ובודקים ששני הבדיקות הראשונות עוברות עכשיו:

$ 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 כמו מזהה הבעלים במסמך עגלת הקניות. מאחר שאין צורך לציין כללים שונים עבור create, update, delete, אפשר להשתמש בכלל write שיחול על כל הבקשות שמשנות נתונים.

עדכון הכלל למסמכים באוסף המשנה של הפריטים. ה-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) ומוסיפים פריט לעגלת הקניות. זהו שלב חשוב שצריך לבצע כדי לוודא שהבדיקות והכללים שלנו תואמים לפונקציונליות שנדרשת על ידי הלקוח. (חשוב לזכור שבפעם האחרונה שניסינו את ממשק המשתמש, המשתמשים לא הצליחו להוסיף פריטים לעגלת הקניות שלהם).

69ad26cee520bf24.png

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

סיכום

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

ba5440b193e75967.gif

אבל רגע, יש עוד!

בהמשך המאמר תלמדו:

  • איך כותבים פונקציה שמופעל על ידי אירוע ב-Firestore
  • איך יוצרים בדיקות שפועלות במספר מכונות וירטואליות

15. הגדרת בדיקות של Cloud Functions

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

בעזרת Emulator Suite קל מאוד לבדוק את 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:

d6d0429b700d2b21.png

16. סקירה כללית של בדיקות Functions

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

יצירת עגלת קניות

Cloud Functions פועל בסביבת שרת מהימנה ויכול להשתמש באימות של חשבון השירות שמשמש את Admin 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() מחזירה פונקציה שאפשר לקרוא אליה כדי לבטל את הרישום של ה-listen.

לצורך הבדיקה הזו, מוסיפים שני פריטים שעלותם הכוללת היא 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 אוסף משנה

מאתחלים קבוע חדש, 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. איך בודקים את התכונה באמצעות ממשק המשתמש של חזית החנות

לבדיקה הסופית, חוזרים לאפליקציית האינטרנט (http://127.0.0.1:5000/‎) ומוסיפים פריט לעגלת הקניות.

69ad26cee520bf24.png

מוודאים שעגלת הקניות מתעדכנת עם הסכום הנכון. נהדר!

סיכום

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

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

c6a7aeb91fe97a64.gif