Phát triển cục bộ bằng Bộ mô phỏng Firebase

1. Trước khi bắt đầu

Các công cụ phụ trợ không máy chủ như Cloud Firestore và Cloud Functions rất dễ sử dụng, nhưng cũng có thể khó thử nghiệm. Bộ công cụ mô phỏng cục bộ Firebase cho phép bạn chạy các phiên bản cục bộ của những dịch vụ này trên máy phát triển để bạn có thể phát triển ứng dụng nhanh chóng và an toàn.

Điều kiện tiên quyết

  • Một trình chỉnh sửa đơn giản như Visual Studio Code, Atom hoặc Sublime Text
  • Node.js 10.0.0 trở lên (để cài đặt Node.js, hãy sử dụng nvm để kiểm tra phiên bản của bạn, hãy chạy node --version)
  • Java 7 trở lên (để cài đặt Java, hãy sử dụng các hướng dẫn này để kiểm tra phiên bản của bạn, hãy chạy java -version)

Bạn sẽ thực hiện

Trong lớp học lập trình này, bạn sẽ chạy và gỡ lỗi một ứng dụng mua sắm trực tuyến đơn giản sử dụng nhiều dịch vụ Firebase:

  • Cloud Firestore: cơ sở dữ liệu NoSQL, không máy chủ, có thể mở rộng trên toàn cầu với các chức năng theo thời gian thực.
  • Chức năng đám mây: mã phụ trợ không máy chủ chạy để phản hồi các sự kiện hoặc yêu cầu HTTP.
  • Xác thực Firebase: dịch vụ xác thực được quản lý, có tích hợp với các sản phẩm khác của Firebase.
  • Lưu trữ Firebase: lưu trữ nhanh và an toàn cho các ứng dụng web.

Bạn sẽ kết nối ứng dụng với Bộ mô phỏng để cho phép phát triển cục bộ.

2589e2f95b74fa88.pngS

Bạn cũng sẽ tìm hiểu cách:

  • Cách kết nối ứng dụng với Trình mô phỏng và cách kết nối nhiều trình mô phỏng.
  • Cách hoạt động của Quy tắc bảo mật của Firebase và cách kiểm thử Quy tắc bảo mật của Firestore so với một trình mô phỏng cục bộ.
  • Cách viết Hàm Firebase được kích hoạt bởi các sự kiện Firestore và cách viết chương trình kiểm thử tích hợp chạy trên Bộ mô phỏng.

2. Thiết lập

Lấy mã nguồn

Trong lớp học lập trình này, bạn bắt đầu với một phiên bản mẫu của The Fire Store đã gần hoàn tất, vì vậy, việc đầu tiên bạn cần làm là sao chép mã nguồn:

$ git clone https://github.com/firebase/emulators-codelab.git

Sau đó, hãy chuyển đến thư mục của lớp học lập trình để xử lý phần còn lại của lớp học lập trình này:

$ cd emulators-codelab/codelab-initial-state

Bây giờ, hãy cài đặt các phần phụ thuộc để có thể chạy mã. Nếu bạn đang sử dụng kết nối Internet chậm hơn, quá trình này có thể mất một hoặc hai phút:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

Tải giao diện dòng lệnh (CLI) của Firebase

Bộ mô phỏng là một phần của Firebase CLI (giao diện dòng lệnh) có thể được cài đặt trên máy của bạn bằng lệnh sau:

$ npm install -g firebase-tools

Tiếp theo, hãy xác nhận rằng bạn có phiên bản CLI mới nhất. Lớp học lập trình này hoạt động với phiên bản 9.0.0 trở lên, nhưng các phiên bản sau này sửa nhiều lỗi hơn.

$ firebase --version
9.6.0

Kết nối với dự án Firebase của bạn

Nếu bạn chưa có dự án Firebase, hãy tạo một dự án Firebase mới trong bảng điều khiển của Firebase. Ghi lại Mã dự án mà bạn chọn, bạn sẽ cần đến mã này sau này.

Bây giờ, chúng ta cần kết nối mã này với dự án Firebase của bạn. Trước tiên, hãy chạy lệnh sau để đăng nhập vào Firebase CLI:

$ firebase login

Tiếp theo, hãy chạy lệnh sau để tạo một email đại diện cho dự án. Thay thế $YOUR_PROJECT_ID bằng mã dự án Firebase của bạn.

$ firebase use $YOUR_PROJECT_ID

Bây giờ, bạn đã sẵn sàng chạy ứng dụng!

3. Chạy trình mô phỏng

Trong phần này, bạn sẽ chạy ứng dụng trên thiết bị. Điều này có nghĩa là đã đến lúc khởi động Bộ mô phỏng.

Khởi động trình mô phỏng

Từ bên trong thư mục nguồn của lớp học lập trình, hãy chạy lệnh sau để khởi động trình mô phỏng:

$ firebase emulators:start --import=./seed

Bạn sẽ thấy một số kết quả như sau:

$ 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.

Sau khi bạn thấy thông báo All Emulators started (Đã khởi động tất cả trình mô phỏng), tức là ứng dụng đã sẵn sàng để sử dụng.

Kết nối ứng dụng web với trình mô phỏng

Dựa vào bảng trong nhật ký, chúng ta có thể thấy rằng trình mô phỏng Cloud Firestore đang theo dõi trên cổng 8080 còn Trình mô phỏng xác thực đang theo dõi trên cổng 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Hãy kết nối mã giao diện người dùng của bạn với trình mô phỏng chứ không phải với phiên bản chính thức. Mở tệp public/js/homepage.js rồi tìm hàm onDocumentReady. Chúng ta có thể thấy rằng mã này truy cập vào các thực thể Firestore và Xác thực tiêu chuẩn:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

Hãy cập nhật các đối tượng dbauth để trỏ đến trình mô phỏng cục bộ:

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

Giờ đây, khi ứng dụng đang chạy trên máy cục bộ của bạn (được trình mô phỏng lưu trữ phân phát), ứng dụng Firestore cũng trỏ đến trình mô phỏng cục bộ thay vì vào cơ sở dữ liệu chính thức.

Mở EmulatorUI

Trong trình duyệt web, hãy chuyển đến http://127.0.0.1:4000/. Bạn sẽ thấy giao diện người dùng của Bộ mô phỏng.

Màn hình chính của giao diện người dùng trình mô phỏng

Nhấp để xem giao diện người dùng cho Trình mô phỏng Firestore. Tập hợp items đã chứa dữ liệu do dữ liệu được nhập bằng cờ --import.

4ef88d0148405d36.pngS

4. Chạy ứng dụng

Mở ứng dụng

Trong trình duyệt web, truy cập http://127.0.0.1:5000 và bạn sẽ thấy The Fire Store chạy trên máy của mình!

939f87946bac2ee4.pngs

Sử dụng ứng dụng

Chọn một mặt hàng trên trang chủ rồi nhấp vào Thêm vào giỏ hàng. Rất tiếc, bạn sẽ gặp lỗi sau:

a11bd59933a8e885.pngS

Hãy khắc phục lỗi đó! Vì mọi thứ đều đang chạy trong trình mô phỏng, nên chúng ta có thể thử nghiệm mà không phải lo lắng về việc ảnh hưởng đến dữ liệu thực tế.

5. Gỡ lỗi ứng dụng

Tìm lỗi

Hãy xem xét trong bảng điều khiển dành cho nhà phát triển Chrome. Nhấn Control+Shift+J (Windows, Linux, Chrome OS) hoặc Command+Option+J (Mac) để xem lỗi trên bảng điều khiển:

74c45df55291dab1.png.

Có vẻ như đã xảy ra lỗi trong phương thức addToCart. Hãy cùng xem xét lỗi đó. Chúng ta cố gắng truy cập uid ở đâu trong phương thức đó và tại sao nó lại là null? Hiện tại, phương thức này có dạng như sau trong 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);
  }

Ha! Chúng tôi không đăng nhập vào ứng dụng. Theo tài liệu về Xác thực Firebase, khi chúng ta không đăng nhập, auth.currentUsernull. Hãy kiểm tra thêm:

public/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

Kiểm thử ứng dụng

Bây giờ, hãy làm mới trang này rồi nhấp vào Thêm vào giỏ hàng. Lần này, bạn sẽ thấy lỗi dễ hơn:

c65f6c05588133f7.pngS

Tuy nhiên, nếu bạn nhấp vào Đăng nhập trong thanh công cụ phía trên rồi nhấp lại vào Thêm vào giỏ hàng, thì bạn sẽ thấy giỏ hàng đã được cập nhật.

Tuy nhiên, có vẻ như các con số này không chính xác:

239f26f02f959eef.png.

Đừng lo, chúng tôi sẽ sớm khắc phục lỗi đó. Trước tiên, hãy tìm hiểu sâu hơn về những gì thực sự xảy ra khi bạn thêm một mặt hàng vào giỏ hàng.

6. Điều kiện kích hoạt hàm cục bộ

Thao tác nhấp vào Thêm vào giỏ hàng sẽ khởi động một chuỗi sự kiện liên quan đến nhiều trình mô phỏng. Trong nhật ký Giao diện dòng lệnh (CLI) của Firebase, bạn sẽ thấy những thông báo tương tự như sau sau khi thêm một mặt hàng vào giỏ hàng:

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

Có 4 sự kiện chính đã xảy ra để tạo các nhật ký đó và bản cập nhật giao diện người dùng mà bạn quan sát được:

68c9323f2ad10f7a.pngS

1) Firestore Write – Ứng dụng

Một tài liệu mới được thêm vào tập hợp /carts/{cartId}/items/{itemId}/ trên Firestore. Bạn có thể thấy mã này ở hàm addToCart bên trong 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) Chức năng đám mây được kích hoạt

Hàm đám mây calculateCart theo dõi mọi sự kiện ghi (tạo, cập nhật hoặc xoá) xảy ra với các mặt hàng trong giỏ hàng bằng cách sử dụng điều kiện kích hoạt onWrite. Bạn có thể xem điều này trong 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) Viết trên Firestore – Quản trị viên

Hàm calculateCart đọc tất cả các mặt hàng trong giỏ hàng, cộng tổng số lượng và giá, sau đó cập nhật "giỏ hàng" có các tổng số mới (xem cartRef.update(...) ở trên).

4) Đọc trên Firestore – Ứng dụng

Giao diện người dùng web được đăng ký nhận thông tin cập nhật về các thay đổi đối với giỏ hàng. Hàm này sẽ cập nhật theo thời gian thực sau khi Hàm đám mây ghi các giá trị tổng số mới và cập nhật giao diện người dùng, như bạn có thể thấy trong public/js/homepage.js:

public/js/homepage.js

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

Nội dung tóm tắt

Bạn làm tốt lắm! Bạn vừa thiết lập một ứng dụng cục bộ hoàn toàn sử dụng 3 trình mô phỏng Firebase khác nhau để kiểm thử cục bộ hoàn toàn.

db82eef1706c9058.gif

Tuy nhiên, hãy đợi vì còn nhiều thứ khác nữa! Trong phần tiếp theo, bạn sẽ tìm hiểu:

  • Cách viết chương trình kiểm thử đơn vị bằng Trình mô phỏng Firebase.
  • Cách sử dụng Trình mô phỏng Firebase để gỡ lỗi Quy tắc bảo mật.

7. Tạo quy tắc bảo mật phù hợp với ứng dụng

Ứng dụng web của chúng tôi đọc và ghi dữ liệu nhưng cho đến nay chúng tôi chưa thực sự lo lắng về tính bảo mật. Cloud Firestore sử dụng một hệ thống có tên là "Quy tắc bảo mật" để khai báo ai có quyền đọc và ghi dữ liệu. Bộ mô phỏng là một cách tuyệt vời để tạo nguyên mẫu cho các quy tắc này.

Trong trình chỉnh sửa, hãy mở tệp emulators-codelab/codelab-initial-state/firestore.rules. Bạn sẽ thấy chúng tôi có ba phần chính trong các quy tắc của mình:

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

Hiện tại, ai cũng có thể đọc và ghi dữ liệu vào cơ sở dữ liệu của chúng ta! Chúng tôi muốn đảm bảo rằng chỉ những hoạt động hợp lệ mới được truy cập và không làm rò rỉ thông tin nhạy cảm nào.

Trong lớp học lập trình này, tuân theo Nguyên tắc đặc quyền thấp nhất, chúng tôi sẽ khoá tất cả tài liệu và dần dần thêm quyền truy cập cho đến khi tất cả người dùng có tất cả quyền truy cập họ cần, nhưng không còn nhiều hơn nữa. Hãy cập nhật hai quy tắc đầu tiên để từ chối quyền truy cập bằng cách đặt điều kiện thành 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. Chạy trình mô phỏng và kiểm thử

Khởi động trình mô phỏng

Trên dòng lệnh, hãy đảm bảo bạn đang ở emulators-codelab/codelab-initial-state/. Có thể bạn vẫn chạy trình mô phỏng từ các bước trước. Nếu không, hãy khởi động lại trình mô phỏng:

$ firebase emulators:start --import=./seed

Khi trình mô phỏng đang chạy, bạn có thể chạy kiểm thử cục bộ trên trình mô phỏng.

Chạy kiểm thử

Trên dòng lệnh trong thẻ dòng lệnh mới trong thư mục emulators-codelab/codelab-initial-state/

Trước tiên, hãy chuyển vào thư mục hàm (chúng ta sẽ ở đây trong phần còn lại của lớp học lập trình):

$ cd functions

Bây giờ, hãy chạy quy trình kiểm thử mocha trong thư mục hàm rồi di chuyển lên đầu kết quả:

# 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

Hiện tại, chúng tôi có 4 thất bại. Khi tạo tệp quy tắc, bạn có thể đo lường tiến trình bằng cách xem có nhiều lượt kiểm thử đạt hơn.

9. Truy cập an toàn vào giỏ hàng

Hai lỗi đầu tiên là "giỏ hàng" để kiểm tra xem:

  • Người dùng chỉ có thể tạo và cập nhật giỏ hàng của riêng mình
  • Người dùng chỉ có thể đọc giỏ hàng của chính họ

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

Hãy thực hiện các kiểm thử này thành công. Trong trình chỉnh sửa, hãy mở tệp quy tắc bảo mật firestore.rules rồi cập nhật câu lệnh trong 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;
    }

    // ...
  }
}

Các quy tắc này hiện chỉ cho phép chủ sở hữu giỏ hàng có quyền đọc và ghi.

Để xác minh dữ liệu đến và xác thực người dùng, chúng ta sử dụng hai đối tượng có sẵn trong ngữ cảnh của mỗi quy tắc:

  • Đối tượng request chứa dữ liệu và siêu dữ liệu về thao tác đang được thực hiện.
  • Nếu dự án Firebase đang sử dụng tính năng Xác thực Firebase, thì đối tượng request.auth sẽ mô tả người dùng đang đưa ra yêu cầu.

10. Thử nghiệm quyền truy cập vào giỏ hàng

Bộ mô phỏng sẽ tự động cập nhật các quy tắc bất cứ khi nào firestore.rules được lưu. Bạn có thể xác nhận rằng trình mô phỏng đã cập nhật quy tắc bằng cách xem thông báo Rules updated trong thẻ chạy trình mô phỏng:

5680da418b420226.pngS

Chạy lại các chương trình kiểm thử và kiểm tra để đảm bảo 2 chương trình kiểm thử đầu tiên hiện đã đạt:

$ 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

Bạn thật xuất sắc! Giờ đây, bạn đã có quyền truy cập an toàn vào giỏ hàng. Hãy chuyển sang kiểm thử không thành công tiếp theo.

11. Chọn nút "Thêm vào giỏ hàng" luồng trong giao diện người dùng

Hiện tại, mặc dù chủ sở hữu giỏ hàng đọc và ghi vào giỏ hàng của họ nhưng họ không thể đọc hoặc viết các mặt hàng riêng lẻ trong giỏ hàng. Đó là vì mặc dù chủ sở hữu có quyền truy cập vào tài liệu giỏ hàng, nhưng họ không có quyền truy cập vào tập hợp con mặt hàng của giỏ hàng.

Đây là trạng thái bị hỏng đối với người dùng.

Quay lại giao diện người dùng web đang chạy trên http://127.0.0.1:5000, rồi thử thêm mặt hàng vào giỏ hàng của bạn. Bạn gặp phải lỗi Permission Denied (xuất hiện trên bảng điều khiển gỡ lỗi) vì chúng tôi chưa cấp cho người dùng quyền truy cập vào các tài liệu đã tạo trong tập hợp con items.

12. Cho phép truy cập vào các mục trong giỏ hàng

Hai bài kiểm tra này xác nhận rằng người dùng chỉ có thể thêm mặt hàng vào hoặc đọc các mặt hàng trong giỏ hàng của họ:

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

Vì vậy, chúng ta có thể viết một quy tắc cho phép truy cập nếu người dùng hiện tại có cùng UID với ownerUID trên tài liệu giỏ hàng. Vì không cần chỉ định các quy tắc khác nhau cho create, update, delete, nên bạn có thể sử dụng quy tắc write để áp dụng cho tất cả các yêu cầu sửa đổi dữ liệu.

Cập nhật quy tắc cho tài liệu trong tập hợp con các mục. get trong điều kiện đang đọc một giá trị từ Firestore – trong trường hợp này là ownerUID trên tài liệu giỏ hàng.

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. Thử nghiệm quyền truy cập vào mặt hàng trong giỏ hàng

Bây giờ, chúng ta có thể chạy lại bài kiểm thử. Di chuyển lên đầu kết quả để kiểm tra xem có nhiều lượt kiểm thử đạt hơn không:

$ 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

Hay quá! Hiện tại, tất cả các kiểm thử của chúng ta đều thành công. Chúng tôi có một thử nghiệm đang chờ xử lý, nhưng chúng tôi sẽ thử nghiệm đó trong vài bước.

14. Chọn nút "thêm vào giỏ hàng" tiếp tục tập luyện

Quay lại giao diện người dùng web ( http://127.0.0.1:5000) rồi thêm một mặt hàng vào giỏ hàng. Đây là một bước quan trọng để xác nhận rằng các bài kiểm thử và quy tắc của chúng ta phù hợp với chức năng mà khách hàng yêu cầu. (Hãy nhớ rằng lần cuối chúng tôi dùng thử giao diện người dùng, người dùng đã không thể thêm sản phẩm vào giỏ hàng!)

69ad26cee520bf24.png.

Ứng dụng sẽ tự động tải lại các quy tắc khi firestore.rules được lưu. Vì vậy, hãy thử thêm mặt hàng nào đó vào giỏ hàng.

Nội dung tóm tắt

Bạn làm tốt lắm! Bạn vừa cải thiện độ bảo mật của ứng dụng. Đây là một bước thiết yếu để chuẩn bị sẵn sàng cho việc phát hành công khai! Nếu đây là ứng dụng phát hành công khai, chúng ta có thể thêm các chương trình kiểm thử này vào quy trình tích hợp liên tục. Điều này sẽ giúp chúng tôi yên tâm rằng dữ liệu giỏ hàng của mình sẽ có các chế độ kiểm soát quyền truy cập này, ngay cả khi người khác đang sửa đổi quy tắc.

ba5440b193e75967.gif

Nhưng chờ đã, vẫn còn nhiều thứ khác nữa!

nếu tiếp tục, bạn sẽ tìm hiểu:

  • Cách viết hàm được kích hoạt bởi sự kiện Firestore
  • Cách tạo chương trình kiểm thử hoạt động trên nhiều trình mô phỏng

15. Thiết lập kiểm thử Cloud Functions

Cho đến nay, chúng ta đã tập trung vào giao diện người dùng của ứng dụng web và Quy tắc bảo mật của Firestore. Tuy nhiên, ứng dụng này cũng sử dụng Cloud Functions để luôn cập nhật thông tin mới nhất cho giỏ hàng của người dùng, vì vậy, chúng ta cũng muốn kiểm thử mã đó.

Bộ mô phỏng giúp bạn dễ dàng kiểm thử Cloud Functions, ngay cả những hàm sử dụng Cloud Firestore và các dịch vụ khác.

Trong trình chỉnh sửa, hãy mở tệp emulators-codelab/codelab-initial-state/functions/test.js rồi di chuyển đến chương trình kiểm thử gần đây nhất trong tệp. Hiện tại, thư này được đánh dấu là đang chờ xử lý:

//  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 () => {
    ...
  });
});

Để bật kiểm thử, hãy xoá .skip để có dạng như sau:

describe("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

Tiếp theo, hãy tìm biến REAL_FIREBASE_PROJECT_ID ở đầu tệp và thay đổi biến đó thành Mã dự án Firebase thực của bạn.:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

Nếu quên mã dự án, bạn có thể tìm mã dự án Firebase trong mục Cài đặt dự án trên Bảng điều khiển của Firebase:

d6d0429b700d2b21.pngs

16. Tìm hiểu về các bài kiểm thử Hàm

Vì chương trình kiểm thử này xác thực sự tương tác giữa Cloud Firestore và Cloud Functions nên sẽ có nhiều bước thiết lập hơn so với các chương trình kiểm thử trong các lớp học lập trình trước đây. Hãy cùng tìm hiểu quy trình kiểm thử này để nắm được kết quả của quy trình này.

Tạo giỏ hàng

Chức năng đám mây hoạt động trong môi trường máy chủ đáng tin cậy và có thể sử dụng phương thức xác thực tài khoản dịch vụ mà SDK dành cho quản trị viên sử dụng . Trước tiên, bạn khởi chạy ứng dụng bằng initializeAdminApp thay vì initializeApp. Sau đó, bạn tạo một DocumentReference cho giỏ hàng mà chúng tôi sẽ thêm mặt hàng vào và khởi tạo giỏ hàng:

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

    ...
  });

Kích hoạt hàm

Sau đó, hãy thêm tài liệu vào tập hợp con items của tài liệu giỏ hàng để kích hoạt hàm này. Thêm 2 mục để đảm bảo bạn đang kiểm thử việc thêm diễn ra trong hàm.

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

    ...
    });
  });

Đặt kỳ vọng kiểm thử

Sử dụng onSnapshot() để đăng ký trình nghe cho mọi thay đổi trên tài liệu giỏ hàng. onSnapshot() trả về một hàm mà bạn có thể gọi để huỷ đăng ký trình nghe.

Đối với thử nghiệm này, hãy thêm hai mặt hàng có tổng giá là 9,98 đô la Mỹ. Sau đó, hãy kiểm tra xem giỏ hàng có itemCounttotalPrice như dự kiến hay không. Nếu có, thì hàm đã thực hiện công việc của mình.

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. Chạy kiểm thử

Có thể bạn vẫn chạy trình mô phỏng từ các lượt kiểm thử trước. Nếu chưa, hãy khởi động trình mô phỏng. Trên dòng lệnh, hãy chạy

$ firebase emulators:start --import=./seed

Mở một thẻ thiết bị đầu cuối mới (để trình mô phỏng chạy) rồi di chuyển vào thư mục hàm. Có thể bạn vẫn có thể mở tệp này từ quá trình kiểm thử quy tắc bảo mật.

$ cd functions

Bây giờ, hãy chạy kiểm thử đơn vị, bạn sẽ thấy tổng cộng 5 bài kiểm thử:

$ 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

Nếu bạn nhìn vào lỗi cụ thể, có vẻ như đó là lỗi hết thời gian chờ. Điều này là do chương trình kiểm thử đang chờ hàm cập nhật chính xác, nhưng hoạt động kiểm thử không bao giờ thực hiện việc này. Bây giờ, chúng ta đã sẵn sàng viết hàm để đáp ứng chương trình kiểm thử.

18. Viết hàm

Để khắc phục kiểm thử này, bạn cần cập nhật hàm này trong functions/index.js. Mặc dù một số hàm trong hàm này đã được viết nhưng chưa hoàn chỉnh. Đây là giao diện hiện tại của hàm:

// 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) {
      }
    });

Hàm này đang cài đặt chính xác tham chiếu giỏ hàng, nhưng sau đó thay vì tính toán các giá trị của totalPriceitemCount, hàm này sẽ cập nhật các giá trị đó thành giá trị được cố định giá trị trong mã.

Tìm nạp và lặp lại thông qua

items bộ sưu tập con

Khởi tạo một hằng số mới, itemsSnap, làm bộ sưu tập con items. Sau đó, lặp lại qua tất cả các tài liệu trong bộ sưu tập.

// 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) {
      }
    });

Tính tổng giá và số lượng mặt hàng

Trước tiên, hãy khởi tạo các giá trị của totalPriceitemCount về 0.

Sau đó, hãy thêm logic vào khối lặp lại. Trước tiên, hãy kiểm tra để đảm bảo mặt hàng đó có giá. Nếu mặt hàng chưa được chỉ định số lượng, hãy để giá trị mặc định là 1. Sau đó, hãy cộng số lượng vào tổng số đang chạy là itemCount. Cuối cùng, hãy cộng giá của mặt hàng nhân với số lượng vào tổng giá trị đang chạy là 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) {
      }
    });

Bạn cũng có thể thêm tính năng ghi nhật ký để giúp gỡ lỗi thành công và trạng thái lỗi:

// 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. Chạy lại kiểm thử

Trên dòng lệnh, hãy đảm bảo trình mô phỏng vẫn đang chạy và chạy lại chương trình kiểm thử. Bạn không cần khởi động lại trình mô phỏng vì trình mô phỏng này tự động nhận các thay đổi đối với hàm. Bạn sẽ thấy tất cả các bài kiểm thử thành công:

$ 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)

Bạn thật xuất sắc!

20. Hãy dùng thử trên giao diện người dùng của Cửa hàng

Để kiểm tra lần cuối, hãy quay lại ứng dụng web ( http://127.0.0.1:5000/) rồi thêm một mặt hàng vào giỏ hàng.

69ad26cee520bf24.png.

Kiểm tra để đảm bảo giỏ hàng cập nhật đúng tổng số tiền. Quá tuyệt!

Nội dung tóm tắt

Bạn đã tìm hiểu một trường hợp kiểm thử phức tạp giữa Cloud Functions cho Firebase và Cloud Firestore. Bạn đã viết một Hàm đám mây để vượt qua bài kiểm thử. Bạn cũng xác nhận rằng chức năng mới đang hoạt động trong giao diện người dùng! Bạn đã làm tất cả những việc này trên máy tính bằng cách chạy trình mô phỏng trên máy của riêng mình.

Bạn cũng đã tạo một ứng dụng web chạy trên trình mô phỏng cục bộ, các quy tắc bảo mật phù hợp để bảo vệ dữ liệu, đồng thời kiểm thử quy tắc bảo mật bằng trình mô phỏng cục bộ.

c6a7aeb91fe97a64.gif