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 cần máy chủ như Cloud Firestore và Cloud Functions rất dễ sử dụng, nhưng có thể khó kiểm thử. Bộ công cụ mô phỏng trên thiết bị của 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 để có thể phát triển ứng dụng một cách 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, hãy chạy node --version)
  • Java 7 trở lên (để cài đặt Java, hãy làm theo các hướng dẫn này, để kiểm tra phiên bản, hãy chạy java -version)

Những việc bạn sẽ làm

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 được hỗ trợ bởi nhiều dịch vụ của Firebase:

  • Cloud Firestore: một cơ sở dữ liệu NoSQL không máy chủ, có khả năng mở rộng trên toàn cầu và có các chức năng theo thời gian thực.
  • Cloud Functions: 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: một dịch vụ xác thực được quản lý, tích hợp với các sản phẩm khác của Firebase.
  • Lưu trữ Firebase: dịch vụ lưu trữ nhanh chóng và an toàn cho các ứng dụng web.

Bạn sẽ kết nối ứng dụng với Emulator Suite để bật tính năng phát triển cục bộ.

2589e2f95b74fa88.png

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

  • Cách kết nối ứng dụng của bạn với Emulator Suite và cách kết nối các trình mô phỏng.
  • Cách hoạt động của Các quy tắc bảo mật của Firebase và cách kiểm thử Các quy tắc bảo mật của Firestore trên trình mô phỏng cục bộ.
  • Cách viết một Hàm Firebase được kích hoạt bởi các sự kiện Firestore và cách viết các bài kiểm thử tích hợp chạy trên Bộ công cụ 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 sẽ bắt đầu với một phiên bản gần như hoàn chỉnh của mẫu The Fire Store. 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 đó, chuyển đến thư mục lớp học lập trình, nơi bạn sẽ làm việc trong 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 để bạn có thể chạy mã. Nếu bạn đang sử dụng kết nối Internet chậm, quá trình này có thể mất một vài 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ộ công cụ mô phỏng là một phần của Giao diện dòng lệnh (CLI) của Firebase. Bạn có thể cài đặt bộ công cụ này trên máy của mình 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 mới nhất của CLI. Lớp học lập trình này sẽ 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ẽ có nhiều bản sửa lỗi hơn.

$ firebase --version
9.6.0

Kết nối với dự án Firebase

Tạo một dự án Firebase

  1. Đăng nhập vào bảng điều khiển của Firebase bằng Tài khoản Google của bạn.
  2. Nhấp vào nút này để tạo một dự án mới, rồi nhập tên dự án (ví dụ: Emulators Codelab).
  3. Nhấp vào Tiếp tục.
  4. Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase, rồi nhấp vào Tiếp tục.
  5. (Không bắt buộc) Bật tính năng hỗ trợ của AI trong bảng điều khiển của Firebase (còn gọi là "Gemini trong Firebase").
  6. Đối với lớp học lập trình này, bạn không cần Google Analytics, vì vậy hãy tắt lựa chọn Google Analytics.
  7. Nhấp vào Tạo dự án, đợi dự án được cấp phép rồi nhấp vào Tiếp tục.

Kết nối mã với dự án Firebase

Giờ đây, 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 bí danh dự án. Thay $YOUR_PROJECT_ID bằng mã dự án Firebase của bạn.

$ firebase use $YOUR_PROJECT_ID

Giờ thì 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 cục bộ. Điều này có nghĩa là đã đến lúc khởi động Emulator Suite.

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

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.

Khi bạn thấy thông báo Đã 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 xử lý trên cổng 8080 và trình mô phỏng Xác thực đang xử lý 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 thay vì với bản phát hành 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 phiên bản Firestore và Auth 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ộ (do trình mô phỏng Lưu trữ cung cấp), ứng dụng Firestore cũng trỏ đến trình mô phỏng cục bộ thay vì đến cơ sở dữ liệu sản xuất.

Mở EmulatorUI

Trong trình duyệt web, hãy truy cập vào http://127.0.0.1:4000/. Bạn sẽ thấy giao diện người dùng Emulator Suite.

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 của 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.png

4. Chạy ứng dụng

Mở ứng dụng

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

939f87946bac2ee4.png

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 phải lỗi sau:

a11bd59933a8e885.png

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

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

Tìm lỗi

Được rồi, hãy xem 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 xem xét lỗi đó. Chúng ta cố gắng truy cập vào một thứ gọi là uid trong phương thức đó và tại sao 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);
  }

Aha! Chúng ta chưa đăng nhập vào ứng dụng. Theo tài liệu Xác thực Firebase, khi chúng ta chưa đăng nhập, auth.currentUser sẽ là null. Hãy thêm một bước kiểm tra cho việc đó:

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 rồi nhấp vào Thêm vào giỏ hàng. Lần này, bạn sẽ nhận được một thông báo lỗi rõ ràng hơn:

c65f6c05588133f7.png

Tuy nhiên, nếu 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, 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 hoàn toàn không chính xác:

239f26f02f959eef.png

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

6. Trình kích hoạt hàm cục bộ

Khi nhấp vào Thêm vào giỏ hàng, một chuỗi sự kiện sẽ bắt đầu, bao gồm nhiều trình mô phỏng. Trong nhật ký của Firebase CLI, bạn sẽ thấy nội dung tương tự như các thông báo 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 ra những nhật ký đó và bản cập nhật giao diện người dùng mà bạn thấy:

68c9323f2ad10f7a.png

1) Firestore Write – Client

Một tài liệu mới được thêm vào bộ sưu tập Firestore /carts/{cartId}/items/{itemId}/. Bạn có thể thấy mã này trong 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) Được kích hoạt bằng Cloud Functions

Cloud Function calculateCart sẽ 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ục trong giỏ hàng bằng cách sử dụng trình kích hoạt onWrite. Bạn có thể thấy trình kích hoạt 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) Firestore Write – Admin

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

4) Firestore Read – Client

Giao diện người dùng web đã đăng ký nhận thông tin cập nhật về những thay đổi đối với giỏ hàng. Ứng dụng sẽ nhận được thông tin cập nhật theo thời gian thực sau khi Cloud Functions ghi 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
   // ...
});

Tóm tắt

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

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 các bài kiểm thử đơn vị sử dụ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 các quy tắc bảo mật phù hợp với ứng dụng của bạn

Ứng dụng web của chúng ta đọc và ghi dữ liệu nhưng cho đến nay, chúng ta chưa thực sự lo lắng về vấn đề 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 những người có quyền đọc và ghi dữ liệu. Emulator Suite là một cách hiệu quả để tạo 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 rằng chúng tôi có 3 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, bất kỳ 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 thao tác hợp lệ mới được thực hiện và chúng tôi không để lộ bất kỳ thông tin nhạy cảm nào.

Trong lớp học lập trình này, theo Nguyên tắc về đặc quyền tối thiểu, chúng ta sẽ khoá tất cả cá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ó mọi quyền truy cập mà họ cần, nhưng không nhiều hơn. Hãy cập nhật 2 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 ở trong emulators-codelab/codelab-initial-state/. Có thể bạn vẫn đang 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

Sau khi các trình mô phỏng đang chạy, bạn có thể chạy các kiểm thử cục bộ đối với các trình mô phỏng đó.

Chạy các bài kiểm thử

Trên dòng lệnh trong một thẻ thiết bị đầu cuối mới từ thư mục emulators-codelab/codelab-initial-state/

Trước tiên, hãy chuyển đến thư mục functions (chúng ta sẽ ở lại đâ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 các kiểm thử mocha trong thư mục functions và 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 ta có 4 lỗi. Khi tạo tệp quy tắc, bạn có thể đo lường tiến trình bằng cách theo dõi thêm các bài kiểm thử đã vượt qua.

9. Truy cập giỏ hàng một cách an toàn

Hai lỗi đầu tiên là các kiểm thử "giỏ hàng" để kiểm thử rằng:

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

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 cùng vượt qua các bài kiểm thử này. 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ác 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;
    }

    // ...
  }
}

Giờ đây, các quy tắc này 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à hoạt động xác thực của người dùng, chúng ta sử dụng 2 đối tượng có trong bối 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ử.
  • Nếu một dự án Firebase đang sử dụ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. Quyền truy cập vào giỏ hàng kiểm thử

Emulator Suite 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 các quy tắc bằng cách xem trong thẻ đang chạy trình mô phỏng để tìm thông báo Rules updated:

5680da418b420226.png

Chạy lại các kiểm thử và kiểm tra để đảm bảo 2 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 đã bảo mật được quyền truy cập vào giỏ hàng. Hãy chuyển sang bài kiểm thử tiếp theo không thành công.

11. Kiểm tra quy trình "Thêm vào giỏ hàng" trong giao diện người dùng

Hiện tại, mặc dù chủ sở hữu giỏ hàng có thể đọc và ghi vào giỏ hàng của họ, nhưng họ không thể đọc hoặc ghi các mặt hàng riêng lẻ trong giỏ hàng. Điều này là do 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 các mục của giỏ hàng.

Đây là trạng thái không hoạt độ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, và thử thêm một mặt hàng vào giỏ hàng. Bạn gặp lỗi Permission Denied (có thể thấy trong 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ặt hàng trong giỏ hàng

Hai kiểm thử này xác nhận rằng người dùng chỉ có thể thêm hoặc đọc các mặt hàng trong giỏ hàng của riêng 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, bạn có thể sử dụng quy tắc write. Quy tắc này á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 các tài liệu trong bộ sưu tập con items. 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. Kiểm tra quyền truy cập vào các mặt hàng trong giỏ hàng

Bây giờ, chúng ta có thể chạy lại kiểm thử. Di chuyển lên đầu đầu ra và kiểm tra để đảm bảo có nhiều bài kiểm thử đạt hơn:

$ 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

Tuyệt vời! Giờ đây, tất cả các kiểm thử của chúng ta đều đạt. Chúng ta có một kiểm thử đang chờ xử lý, nhưng chúng ta sẽ chuyển sang kiểm thử đó trong vài bước nữa.

14. Kiểm tra lại quy trình "thêm vào giỏ hàng"

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 quy tắc và kiểm thử của chúng tôi phù hợp với chức năng mà ứng dụng khách yêu cầu. (Xin lưu ý rằng lần gần đây nhất chúng tôi thử nghiệm giao diện người dùng, người dùng không thể thêm mặt hàng 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 mặt hàng vào giỏ hàng.

Tóm tắt

Xuất sắc! Bạn vừa cải thiện tính bảo mật của ứng dụng. Đây là một bước thiết yếu để chuẩn bị cho ứng dụng phát hành công khai! Nếu đây là một ứng dụng phát hành công khai, chúng ta có thể thêm các 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 chúng tôi sẽ có các chế độ kiểm soát quyền truy cập này, ngay cả khi những người khác đang sửa đổi các quy tắc.

ba5440b193e75967.gif

Nhưng chưa hết!

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

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

15. Thiết lập các 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. Nhưng ứng dụng này cũng sử dụng Cloud Functions để cập nhật giỏ hàng của người dùng, vì vậy, chúng ta cũng muốn kiểm thử mã đó.

Emulator Suite 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 bài kiểm thử cuối cùng trong tệp. Hiện tại, yêu cầu 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 để kiểm thử 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 rồi thay đổi 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 phần Cài đặt dự án của Bảng điều khiển Firebase:

d6d0429b700d2b21.png

16. Xem qua các kiểm thử Hàm

Vì kiểm thử này xác thực hoạt động tương tác giữa Cloud Firestore và Cloud Functions, nên kiểm thử này cần nhiều bước thiết lập hơn so với các kiểm thử trong lớp học lập trình trước. Hãy cùng xem qua bài kiểm tra này để biết những gì bạn cần chuẩn bị.

Tạo giỏ hàng

Cloud Functions chạy trong một 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à Admin SDK sử dụng . Trước tiên, bạn khởi chạy một ứng dụng bằng initializeAdminApp thay vì initializeApp. Sau đó, bạn tạo một DocumentReference cho giỏ hàng mà chúng ta sẽ thêm các 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 các tài liệu vào bộ sưu tập con items của tài liệu giỏ hàng để kích hoạt hàm. Thêm hai mục để đảm bảo bạn đang kiểm thử việc bổ sung xảy 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 về kiểm thử

Sử dụng onSnapshot() để đăng ký một 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 quy trình kiểm thử này, hãy thêm 2 mặt hàng có tổng giá là 9,98 USD. Sau đó, hãy kiểm tra xem giỏ hàng có itemCounttotalPrice như mong đợi hay không. Nếu có, thì hàm đã hoàn thành nhiệm vụ.

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ử

Bạn có thể vẫn đang chạy các trình mô phỏng từ các thử nghiệm trước đó. Nếu không, 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ẻ dòng lệnh mới (để các trình mô phỏng tiếp tục chạy) rồi chuyển vào thư mục functions. Bạn có thể vẫn mở cửa sổ này từ các bài kiểm thử quy tắc bảo mật.

$ cd functions

Bây giờ, hãy chạy các kiểm thử đơn vị, bạn sẽ thấy tổng cộng 5 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 xem xét lỗi cụ thể này, thì có vẻ như đây là lỗi hết thời gian chờ. Điều này là do kiểm thử đang chờ hàm cập nhật chính xác, nhưng hàm không bao giờ cập nhật. Bây giờ, chúng ta đã sẵn sàng viết hàm để đáp ứng kiểm thử.

18. Viết một hàm

Để khắc phục vấn đề này trong quá trình kiểm thử, bạn cần cập nhật hàm trong functions/index.js. Mặc dù một phần của hàm này đã được viết, nhưng chưa hoàn chỉnh. Hàm hiện có dạng như sau:

// 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 đặt đúng giá trị 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 các giá trị được mã hoá cứng.

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

items subcollection

Khởi chạy một hằng số mới, itemsSnap, để trở thành tập hợp con items. Sau đó, hãy lặp lại thao tác cho 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 thành 0.

Sau đó, hãy thêm logic vào khối lặp lại của chúng ta. Trước tiên, hãy kiểm tra để đảm bảo rằng mặt hàng có giá. Nếu mặt hàng không có số lượng được chỉ định, hãy để mặt hàng đó có giá trị mặc định là 1. Sau đó, hãy thêm số lượng vào tổng số tiền đang chạy của itemCount. Cuối cùng, hãy thêm giá của mặt hàng nhân với số lượng vào tổng số tiền đang chạy của 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 nhật ký để gỡ lỗi các trạng thái thành công và 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 các kiểm thử

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

$ 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 thử tính năng này bằng giao diện người dùng của cửa hàng

Đối với thử nghiệm cuối cùng, 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

Xác nhận rằng giỏ hàng cập nhật đúng tổng số tiền. Quá tuyệt!

Tóm tắt

Bạn đã xem qua 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 Cloud Function để 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 đã thực hiện tất cả những việc này trên thiết bị của mình, chạy trình mô phỏng trên máy riêng.

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

c6a7aeb91fe97a64.gif