การพัฒนาในเครื่องด้วยชุดโปรแกรมจำลอง Firebase

1. ก่อนเริ่มต้น

เครื่องมือแบ็กเอนด์แบบ Serverless เช่น Cloud Firestore และ Cloud Functions ใช้งานได้ง่ายมากแต่ทดสอบได้ยาก Firebase Local Emulator Suite ทำให้คุณสามารถเรียกใช้บริการเหล่านี้ในเวอร์ชันท้องถิ่นในเครื่องการพัฒนาได้ เพื่อให้คุณพัฒนาแอปได้อย่างรวดเร็วและปลอดภัย

ข้อกำหนดเบื้องต้น

  • โปรแกรมแก้ไขแบบง่าย เช่น โค้ด Visual Studio, 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: การโฮสต์ที่รวดเร็วและปลอดภัยสำหรับเว็บแอป

คุณจะต้องเชื่อมต่อแอปกับชุดโปรแกรมจำลองเพื่อเปิดใช้การพัฒนาในเครื่อง

2589e2f95b74fa88.png

และจะได้เรียนรู้วิธีต่อไปนี้

  • วิธีเชื่อมต่อแอปกับชุดโปรแกรมจำลองและวิธีการเชื่อมต่อโปรแกรมจำลองต่างๆ
  • วิธีการทำงานของกฎการรักษาความปลอดภัยของ Firebase และวิธีทดสอบกฎความปลอดภัยของ Firestore กับโปรแกรมจำลองในเครื่อง
  • วิธีเขียนฟังก์ชัน Firebase ที่ทริกเกอร์โดยเหตุการณ์ Firestore และวิธีเขียนการทดสอบการผสานรวมที่ทำงานกับชุดโปรแกรมจำลอง

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 ใหม่ในคอนโซล 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.

เมื่อคุณเห็นข้อความโปรแกรมจำลองทั้งหมดเริ่มแล้ว แสดงว่าแอปพร้อมใช้งานแล้ว

เชื่อมต่อเว็บแอปกับโปรแกรมจำลอง

จากตารางในบันทึก เราพบว่าโปรแกรมจำลอง 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 และการตรวจสอบสิทธิ์มาตรฐาน ดังนี้

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/ ในเว็บเบราว์เซอร์ คุณควรเห็น UI ของชุดโปรแกรมจำลอง

หน้าจอหลักของ UI โปรแกรมจำลอง

คลิกเพื่อดู UI สำหรับโปรแกรมจำลอง Firestore คอลเล็กชัน items มีข้อมูลอยู่แล้วเนื่องจากข้อมูลที่นำเข้าด้วยแฟล็ก --import

4ef88d0148405d36.png

4. เรียกใช้แอป

เปิดแอป

ในเว็บเบราว์เซอร์ ให้ไปที่ http://127.0.0.1:5000 คุณจะเห็น Fire Store ทำงานอยู่ในเครื่องของคุณ

939f87946bac2ee4.png

ใช้แอป

เลือกสินค้าในหน้าแรก แล้วคลิกเพิ่มลงในรถเข็น ขออภัย คุณจะพบข้อผิดพลาดต่อไปนี้

a11bd59933a8e885.png

มาแก้ไขข้อบกพร่องกัน เนื่องจากทุกอย่างทำงานในโปรแกรมจำลอง เราจึงสามารถทดลองและไม่ต้องกังวลว่าจะกระทบกับข้อมูลจริง

5. แก้ไขข้อบกพร่องแอป

ค้นหาข้อบกพร่อง

มาดูใน Developer Console ของ 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

มีเหตุการณ์สำคัญ 4 อย่างที่เกิดขึ้นในการสร้างบันทึกเหล่านั้นและการอัปเดต UI ที่คุณสังเกตเห็น

68c9323f2ad10f7a.png

1) การเขียน Firestore - ไคลเอ็นต์

มีการเพิ่มเอกสารใหม่ในคอลเล็กชัน Firestore /carts/{cartId}/items/{itemId}/ คุณดูโค้ดนี้ได้ในฟังก์ชัน 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 Function

calculateCart ของ Cloud Function จะเฝ้าติดตามเหตุการณ์การเขียน (สร้าง อัปเดต หรือลบ) ที่เกิดขึ้นกับรายการในรถเข็นโดยใช้ทริกเกอร์ 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 Function เขียนผลรวมใหม่และอัปเดต UI ดังที่แสดงใน public/js/homepage.js

public/js/homepage.js

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

สรุป

เก่งมาก! คุณเพิ่งตั้งค่าแอปในเครื่องที่ใช้ตัวจำลอง Firebase ที่แตกต่างกัน 3 ตัวสำหรับการทดสอบในเครื่องโดยสมบูรณ์

db82eef1706c9058.gif

แต่ยังไม่หมดเท่านี้! ในส่วนถัดไป คุณจะได้เรียนรู้เกี่ยวกับสิ่งต่อไปนี้

  • วิธีเขียนการทดสอบ 1 หน่วยที่ใช้โปรแกรมจำลอง 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 นี้ เราจะล็อกเอกสารทั้งหมดและค่อยๆ เพิ่มการเข้าถึงจนกว่าผู้ใช้ทุกคนจะมีสิทธิ์เข้าถึงในแบบที่ต้องการ ตามหลักการของสิทธิ์ขั้นต่ำที่สุด แต่จะเพิ่มการเข้าถึงไม่ได้ ลองอัปเดตกฎ 2 ข้อแรกเพื่อปฏิเสธการเข้าถึงโดยตั้งค่าเงื่อนไขเป็น 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

จากนั้นทำการทดสอบมอคค่าในไดเรกทอรีฟังก์ชัน และเลื่อนไปที่ด้านบนของเอาต์พุต ดังนี้

# 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

ตอนนี้เราก็มีความล้มเหลว 4 รายการ เมื่อสร้างไฟล์กฎ คุณจะวัดความคืบหน้าได้ด้วยการดูการผ่านการทดสอบมากขึ้น

9. รักษาความปลอดภัยในการเข้าถึงรถเข็น

ข้อผิดพลาด 2 อย่างแรกคือ "รถเข็นช็อปปิ้ง" ว่าการทดสอบใดที่

  • ผู้ใช้สามารถสร้างและอัปเดตรถเข็นของตนเองเท่านั้น
  • ผู้ใช้จะอ่านได้เฉพาะรถเข็นของตนเอง

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

    // ...
  }
}

ปัจจุบันกฎเหล่านี้อนุญาตเฉพาะเจ้าของรถเข็นที่มีสิทธิ์อ่านและเขียนเท่านั้น

เราใช้ออบเจ็กต์ 2 รายการที่มีอยู่ในบริบทของทุกกฎเพื่อยืนยันข้อมูลที่เข้ามาและการตรวจสอบสิทธิ์ของผู้ใช้ ดังนี้

  • ออบเจ็กต์ request มีข้อมูลและข้อมูลเมตาเกี่ยวกับการดำเนินการที่ทำอยู่
  • หากโปรเจ็กต์ Firebase ใช้การตรวจสอบสิทธิ์ของ Firebase ออบเจ็กต์ request.auth จะอธิบายผู้ใช้ที่ส่งคำขอ

10. ทดสอบการเข้าถึงรถเข็น

ชุดโปรแกรมจำลองจะอัปเดตกฎโดยอัตโนมัติเมื่อใดก็ตามที่บันทึก firestore.rules คุณสามารถยืนยันว่าโปรแกรมจำลองได้อัปเดตกฎแล้ว โดยดูในแท็บที่เรียกใช้โปรแกรมจำลองเพื่อหาข้อความ Rules updated:

5680da418b420226.png

ทำการทดสอบอีกครั้งและตรวจสอบว่าการทดสอบ 2 รายการแรกผ่านแล้ว

$ 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. ทำเครื่องหมายที่ช่อง "เพิ่มลงในรถเข็น" ใน UI

ในขณะนี้ แม้ว่าเจ้าของรถเข็นจะอ่านและเขียนลงในรถเข็นของตน แต่จะไม่สามารถอ่านหรือเขียนรายการแต่ละรายการในรถเข็นได้ เพราะแม้เจ้าของมีสิทธิ์เข้าถึงเอกสารรถเข็น แต่ก็ไม่มีสิทธิ์เข้าถึงคอลเล็กชันย่อยของสินค้าของรถเข็น

นี่คือสถานะที่ใช้งานไม่ได้สำหรับผู้ใช้

กลับไปที่ UI ของเว็บซึ่งกำลังทำงานอยู่บน http://127.0.0.1:5000, แล้วลองเพิ่มสินค้าลงในรถเข็นของคุณ คุณได้รับข้อผิดพลาด Permission Denied ซึ่งดูได้จากคอนโซลแก้ไขข้อบกพร่อง เนื่องจากเรายังไม่ได้ให้สิทธิ์ผู้ใช้ในการเข้าถึงเอกสารที่สร้างในคอลเล็กชันย่อย items

12. อนุญาตให้เข้าถึงรายการในรถเข็น

การทดสอบ 2 รายการนี้จะยืนยันว่าผู้ใช้เพิ่มสินค้าลงในหรืออ่านสินค้าจากรถเข็นของตนเองได้เท่านั้น

  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 ซึ่งจะมีผลกับคำขอทั้งหมดที่แก้ไขข้อมูล

อัปเดตกฎสำหรับเอกสารในคอลเล็กชันย่อยของรายการ 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

เยี่ยม ตอนนี้การทดสอบทั้งหมดของเราผ่านแล้ว เรามีการทดสอบที่รอดำเนินการ 1 รายการ แต่เราจะดำเนินการใน 2-3 ขั้นตอน

14. ทำเครื่องหมายในช่อง "เพิ่มในรถเข็น" ไหลอีกครั้ง

กลับไปที่ส่วนหน้าของเว็บ ( http://127.0.0.1:5000) และเพิ่มรายการลงในรถเข็น ซึ่งเป็นขั้นตอนที่สำคัญในการยืนยันว่าการทดสอบและกฎของเราตรงกับฟังก์ชันการทำงานที่ลูกค้าต้องการ (อย่าลืมว่าครั้งสุดท้ายที่เราลองใช้ UI นั้นผู้ใช้เพิ่มสินค้าลงในรถเข็นไม่ได้)

69ad26cee520bf24.png

ไคลเอ็นต์จะโหลดกฎซ้ำโดยอัตโนมัติเมื่อบันทึก firestore.rules ดังนั้น ให้ลองเพิ่มสินค้าลงในรถเข็นดู

สรุป

เก่งมาก! คุณเพิ่งปรับปรุงความปลอดภัยของแอป ซึ่งเป็นขั้นตอนสำคัญในการเตรียมแอปให้พร้อมสำหรับเวอร์ชันที่ใช้งานจริง หากนี่เป็นแอปเวอร์ชันที่ใช้งานจริง เราสามารถเพิ่มการทดสอบเหล่านี้ลงในไปป์ไลน์การผสานรวมอย่างต่อเนื่องของเรา ซึ่งจะทำให้เรามั่นใจว่าข้อมูลรถเข็นช็อปปิ้งของเราจะมีการควบคุมการเข้าถึงเหล่านี้ แม้ว่าผู้อื่นจะปรับเปลี่ยนกฎก็ตาม

ba5440b193e75967.gif

แต่เดี๋ยวก่อน ยังไม่หมดแค่นั้น

หากคุณดำเนินการต่อ คุณจะได้เรียนรู้เกี่ยวกับ

  • วิธีเขียนฟังก์ชันที่ทริกเกอร์โดยเหตุการณ์ Firestore
  • วิธีสร้างการทดสอบที่ใช้ได้กับโปรแกรมจำลองหลายรายการ

15. ตั้งค่าการทดสอบ Cloud Functions

ที่ผ่านมาเราได้ให้ความสำคัญกับฟรอนท์เอนด์ของเว็บแอปและกฎความปลอดภัยของ Firestore ไปแล้ว แต่แอปนี้ก็ใช้ฟังก์ชันระบบคลาวด์เพื่ออัปเดตรถเข็นของผู้ใช้ด้วย เราจึงต้องการทดสอบโค้ดดังกล่าวด้วย

ชุดโปรแกรมจำลองช่วยให้ทดสอบ 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. แนะนำการทดสอบฟังก์ชัน

เนื่องจากการทดสอบนี้จะตรวจสอบการโต้ตอบระหว่าง Cloud Firestore และ Cloud Functions จึงต้องมีการตั้งค่ามากกว่าการทดสอบใน Codelab ก่อนหน้านี้ เราจะมาเรียนรู้เกี่ยวกับการทดสอบนี้และดูสิ่งที่จะเกิดขึ้น

สร้างรถเข็น

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 ของเอกสารรถเข็นช็อปปิ้งของเราเพื่อทริกเกอร์ฟังก์ชัน เพิ่ม 2 รายการเพื่อให้แน่ใจว่าคุณกำลังทดสอบการบวกที่เกิดขึ้นในฟังก์ชัน

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() เพื่อลงทะเบียน Listener สำหรับการเปลี่ยนแปลงในเอกสารรถเข็นช็อปปิ้ง onSnapshot() จะแสดงผลฟังก์ชันที่คุณสามารถเรียกใช้เพื่อยกเลิกการลงทะเบียน Listener

สำหรับการทดสอบนี้ ให้เพิ่มสินค้า 2 รายการที่มีราคา $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

ถึงตอนนี้ ให้เรียกใช้การทดสอบ 1 หน่วย คุณควรเห็นการทดสอบทั้งหมด 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 เป็น 0 ก่อน

จากนั้นเพิ่มตรรกะลงในบล็อกการทำซ้ำ ก่อนอื่น ให้ตรวจสอบว่าสินค้าดังกล่าวมีราคา หากสินค้าไม่ได้ระบุจำนวนไว้ ให้ใช้ 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. ลองใช้งานโดยใช้ UI หน้าร้าน

สำหรับการทดสอบครั้งสุดท้าย ให้กลับไปที่เว็บแอป ( http://127.0.0.1:5000/) แล้วเพิ่มรายการลงในรถเข็น

69ad26cee520bf24.png

ตรวจสอบว่ารถเข็นอัปเดตด้วยยอดรวมที่ถูกต้อง เยี่ยมเลย

สรุป

คุณได้อธิบายถึงกรอบการทดสอบที่ซับซ้อนระหว่าง Cloud Functions for Firebase และ Cloud Firestore คุณได้เขียน Cloud Function เพื่อทำการทดสอบผ่าน และคุณยังตรวจสอบด้วยว่าฟังก์ชันใหม่ทำงานอยู่ใน UI คุณทำทั้งหมดนี้ได้ในเครื่อง โดยเรียกใช้โปรแกรมจำลองในเครื่องของคุณเอง

และคุณยังได้สร้างเว็บไคลเอ็นต์ที่ทำงานกับโปรแกรมจำลองในเครื่อง กฎความปลอดภัยที่ปรับแต่งเพื่อปกป้องข้อมูล และทดสอบกฎความปลอดภัยโดยใช้โปรแกรมจำลองในเครื่อง

c6a7aeb91fe97a64.gif