توسعه محلی با مجموعه شبیه ساز Firebase

۱. قبل از شروع

ابزارهای بک‌اند بدون سرور مانند Cloud Firestore و Cloud Functions بسیار آسان برای استفاده هستند، اما آزمایش آنها می‌تواند دشوار باشد. Firebase Local Emulator Suite به شما امکان می‌دهد نسخه‌های محلی این سرویس‌ها را روی دستگاه توسعه خود اجرا کنید تا بتوانید برنامه خود را به سرعت و با خیال راحت توسعه دهید.

پیش‌نیازها

کاری که انجام خواهید داد

در این آزمایشگاه کد، شما یک برنامه خرید آنلاین ساده را که توسط چندین سرویس Firebase پشتیبانی می‌شود، اجرا و اشکال‌زدایی خواهید کرد:

  • Cloud Firestore: یک پایگاه داده NoSQL، بدون سرور و مقیاس‌پذیر در سطح جهانی با قابلیت‌های بلادرنگ.
  • توابع ابری : یک کد بک‌اند بدون سرور که در پاسخ به رویدادها یا درخواست‌های HTTP اجرا می‌شود.
  • احراز هویت فایربیس : یک سرویس احراز هویت مدیریت‌شده که با سایر محصولات فایربیس ادغام می‌شود.
  • میزبانی فایربیس : میزبانی سریع و امن برای برنامه‌های وب.

شما برنامه را به Emulator Suite متصل خواهید کرد تا توسعه محلی را فعال کنید.

2589e2f95b74fa88.png

همچنین یاد خواهید گرفت که چگونه:

  • نحوه اتصال برنامه خود به مجموعه شبیه‌سازها و نحوه اتصال شبیه‌سازهای مختلف.
  • نحوه عملکرد قوانین امنیتی Firebase و نحوه آزمایش قوانین امنیتی Firestore در برابر یک شبیه‌ساز محلی.
  • چگونه یک تابع Firebase بنویسیم که توسط رویدادهای Firestore فعال شود و چگونه تست‌های یکپارچه‌سازی بنویسیم که در Emulator Suite اجرا شوند.

۲. راه‌اندازی

دریافت کد منبع

در این آزمایشگاه کد، شما با نسخه‌ای از نمونه 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 ../

دریافت رابط خط فرمان فایربیس

مجموعه شبیه‌ساز بخشی از رابط خط فرمان (CLI) فایربیس است که می‌تواند با دستور زیر روی دستگاه شما نصب شود:

$ npm install -g firebase-tools

در مرحله بعد، تأیید کنید که آخرین نسخه CLI را دارید. این آزمایشگاه کد باید با نسخه 9.0.0 یا بالاتر کار کند، اما نسخه‌های بعدی شامل رفع اشکالات بیشتری هستند.

$ firebase --version
9.6.0

به پروژه Firebase خود متصل شوید

ایجاد یک پروژه فایربیس

  1. با استفاده از حساب گوگل خود وارد کنسول فایربیس شوید.
  2. برای ایجاد یک پروژه جدید، روی دکمه کلیک کنید و سپس نام پروژه را وارد کنید (برای مثال، Emulators Codelab ).
  3. روی ادامه کلیک کنید.
  4. در صورت درخواست، شرایط Firebase را مرور و قبول کنید و سپس روی ادامه کلیک کنید.
  5. (اختیاری) دستیار هوش مصنوعی را در کنسول Firebase (با نام "Gemini در Firebase") فعال کنید.
  6. برای این codelab، به گوگل آنالیتیکس نیاز ندارید ، بنابراین گزینه گوگل آنالیتیکس را غیرفعال کنید .
  7. روی ایجاد پروژه کلیک کنید، منتظر بمانید تا پروژه شما آماده شود و سپس روی ادامه کلیک کنید.

کد خود را به پروژه Firebase خود متصل کنید

حالا باید این کد را به پروژه Firebase خود متصل کنیم. ابتدا دستور زیر را برای ورود به Firebase CLI اجرا کنید:

$ firebase login

سپس دستور زیر را برای ایجاد یک نام مستعار پروژه اجرا کنید. به جای $YOUR_PROJECT_ID ، شناسه پروژه Firebase خود را قرار دهید.

$ firebase use $YOUR_PROJECT_ID

حالا آماده‌ی اجرای برنامه هستید!

۳. شبیه‌سازها را اجرا کنید

در این بخش، برنامه را به صورت محلی اجرا خواهید کرد. این به این معنی است که زمان راه‌اندازی مجموعه شبیه‌ساز فرا رسیده است.

شروع شبیه‌سازها

از داخل دایرکتوری منبع 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 و شبیه‌ساز Authentication روی پورت 9099 در حال گوش دادن هستند.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

بیایید کد frontend شما را به جای محیط عملیاتی، به شبیه‌ساز متصل کنیم. فایل public/js/homepage.js را باز کنید و تابع onDocumentReady را پیدا کنید. می‌توانیم ببینیم که کد به نمونه‌های استاندارد Firestore و Auth دسترسی دارد:

عمومی/js/homepage.js

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

بیایید اشیاء db و auth را به‌روزرسانی کنیم تا به شبیه‌سازهای محلی اشاره کنند:

عمومی/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);
  }

اکنون وقتی برنامه روی دستگاه محلی شما (که توسط شبیه‌ساز Hosting ارائه می‌شود) اجرا می‌شود، کلاینت Firestore به جای پایگاه داده‌ی عملیاتی، به شبیه‌ساز محلی اشاره می‌کند.

رابط کاربری شبیه‌ساز را باز کنید

در مرورگر وب خود، به آدرس http://127.0.0.1:4000/ بروید. باید رابط کاربری Emulator Suite را ببینید.

صفحه اصلی رابط کاربری شبیه‌سازها

برای مشاهده رابط کاربری شبیه‌ساز Firestore کلیک کنید. مجموعه items به دلیل داده‌های وارد شده با پرچم --import از قبل حاوی داده‌ها هستند.

4ef88d0148405d36.png

۴. برنامه را اجرا کنید

برنامه را باز کنید

در مرورگر وب خود، به آدرس http://127.0.0.1:5000 بروید و باید ببینید که فروشگاه آتش نشانی به صورت محلی روی دستگاه شما اجرا می‌شود!

939f87946bac2ee4.png

از برنامه استفاده کنید

یک کالا را در صفحه اصلی انتخاب کنید و روی افزودن به سبد خرید کلیک کنید. متأسفانه، با خطای زیر مواجه خواهید شد:

a11bd59933a8e885.png

بیایید آن اشکال را برطرف کنیم! از آنجا که همه چیز در شبیه‌سازها اجرا می‌شود، می‌توانیم آزمایش کنیم و نگران تأثیرگذاری بر داده‌های واقعی نباشیم.

۵. اشکال‌زدایی برنامه

اشکال را پیدا کنید

خب، بیایید نگاهی به کنسول توسعه‌دهندگان کروم بیندازیم. برای دیدن خطا در کنسول Control+Shift+J (ویندوز، لینوکس، سیستم عامل کروم) یا Command+Option+J (مک) را فشار دهید:

74c45df55291dab1.png

به نظر می‌رسد خطایی در متد addToCart رخ داده است، بیایید نگاهی به آن بیندازیم. در کجا باید به چیزی به نام uid در آن متد دسترسی پیدا کنیم و چرا باید null باشد؟ در حال حاضر، متد در public/js/homepage.js به این شکل است:

عمومی/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 است. بیایید یک بررسی برای این موضوع اضافه کنیم:

عمومی/js/homepage.js

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

    // ...
  }

برنامه را تست کنید

حالا صفحه را رفرش کنید و سپس روی افزودن به سبد خرید کلیک کنید. این بار باید خطای بهتری دریافت کنید:

c65f6c05588133f7.png

اما اگر در نوار ابزار بالا روی ورود کلیک کنید و سپس دوباره روی افزودن به سبد خرید کلیک کنید، خواهید دید که سبد خرید به‌روزرسانی شده است.

با این حال، به نظر نمی‌رسد که اعداد اصلاً درست باشند:

۲۳۹f۲۶f۰۲f۹۵۹eef.png

نگران نباشید، به زودی این اشکال را برطرف خواهیم کرد. ابتدا، بیایید عمیقاً بررسی کنیم که وقتی یک کالا را به سبد خرید خود اضافه کردید، واقعاً چه اتفاقی افتاد.

۶. تریگرهای توابع محلی

کلیک روی «افزودن به سبد خرید» زنجیره‌ای از رویدادها را آغاز می‌کند که شامل چندین شبیه‌ساز می‌شوند. در گزارش‌های Firebase CLI، پس از افزودن یک کالا به سبد خرید، باید چیزی شبیه به پیام‌های زیر را مشاهده کنید:

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

چهار رویداد کلیدی برای تولید آن لاگ‌ها و به‌روزرسانی رابط کاربری که مشاهده کردید، رخ داده است:

68c9323f2ad10f7a.png

۱) نوشتن در فایراستور - کلاینت

یک سند جدید به مجموعه Firestore /carts/{cartId}/items/{itemId}/ اضافه شده است. می‌توانید این کد را در تابع addToCart در داخل public/js/homepage.js مشاهده کنید:

عمومی/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);
  }

۲) عملکرد ابری فعال شد

تابع ابری calculateCart با استفاده از تریگر onWrite که می‌توانید در functions/index.js مشاهده کنید، به هر رویداد نوشتن (ایجاد، به‌روزرسانی یا حذف) که برای اقلام سبد خرید اتفاق می‌افتد، گوش می‌دهد:

توابع/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) {
      }
    }
);

۳) نوشتن در فایراستور - بخش مدیریت

تابع calculateCart تمام اقلام موجود در سبد خرید را می‌خواند و تعداد کل و قیمت را با هم جمع می‌کند، سپس سند "سبد خرید" را با مجموع جدید به‌روزرسانی می‌کند (به cartRef.update(...) در بالا مراجعه کنید).

۴) Firestore Read - کلاینت

رابط کاربری وب برای دریافت به‌روزرسانی‌های مربوط به تغییرات سبد خرید مشترک می‌شود. پس از اینکه تابع ابری مجموع جدید را می‌نویسد و رابط کاربری را به‌روزرسانی می‌کند، به‌روزرسانی بلادرنگ دریافت می‌کند، همانطور که می‌توانید در public/js/homepage.js مشاهده کنید:

عمومی/js/homepage.js

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

خلاصه

آفرین! شما همین الان یک برنامه کاملاً محلی راه‌اندازی کردید که از سه شبیه‌ساز Firebase مختلف برای آزمایش کاملاً محلی استفاده می‌کند.

db82eef1706c9058.gif

اما صبر کنید، چیزهای بیشتری هم هست! در بخش بعدی یاد خواهید گرفت:

  • نحوه نوشتن تست‌های واحد که از شبیه‌سازهای Firebase استفاده می‌کنند.
  • نحوه استفاده از شبیه‌سازهای Firebase برای اشکال‌زدایی قوانین امنیتی شما.

۷. قوانین امنیتی متناسب با برنامه خود ایجاد کنید

برنامه وب ما داده‌ها را می‌خواند و می‌نویسد، اما تاکنون واقعاً نگران امنیت نبوده‌ایم. 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;
    }
  }
}

در حال حاضر هر کسی می‌تواند داده‌ها را در پایگاه داده ما بخواند و بنویسد! ما می‌خواهیم مطمئن شویم که فقط عملیات معتبر عبور می‌کنند و هیچ اطلاعات حساسی فاش نمی‌شود.

در طول این کدنویسی، با پیروی از اصل حداقل امتیاز، تمام اسناد را قفل می‌کنیم و به تدریج دسترسی را اضافه می‌کنیم تا زمانی که همه کاربران تمام دسترسی مورد نیاز خود را داشته باشند، اما نه بیشتر. بیایید دو قانون اول را برای رد دسترسی با تنظیم شرط روی 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;
    }
  }
}

۸. شبیه‌سازها و تست‌ها را اجرا کنید

شبیه‌سازها را شروع کنید

در خط فرمان، مطمئن شوید که در emulators-codelab/codelab-initial-state/ هستید. ممکن است هنوز شبیه‌سازهای مراحل قبلی در حال اجرا باشند. در غیر این صورت، دوباره شبیه‌سازها را اجرا کنید:

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

پس از اجرای شبیه‌سازها، می‌توانید تست‌ها را به صورت محلی روی آنها اجرا کنید.

تست‌ها را اجرا کنید

در خط فرمان ، در یک تب ترمینال جدید از دایرکتوری emulators-codelab/codelab-initial-state/

ابتدا به دایرکتوری functions بروید (ما در ادامه‌ی کدنویسی اینجا خواهیم ماند):

$ cd functions

حالا تست‌های mocha را در دایرکتوری 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

در حال حاضر ما چهار شکست داریم. همزمان با ساخت فایل قوانین، می‌توانید با مشاهده‌ی موفقیت آزمایش‌های بیشتر، پیشرفت را اندازه‌گیری کنید.

۹. دسترسی امن به سبد خرید

دو شکست اول، تست‌های «سبد خرید» هستند که موارد زیر را بررسی می‌کنند:

  • کاربران فقط می‌توانند سبد خرید خود را ایجاد و به‌روزرسانی کنند
  • کاربران فقط می‌توانند سبد خرید خودشان را بخوانند

توابع/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} را به‌روزرسانی کنید:

قوانین فروشگاه آتش‌نشانی

rules_version = '2';
service cloud.firestore {
    // UPDATE THESE LINES
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }

    // ...
  }
}

این قوانین اکنون فقط به صاحب سبد خرید اجازه دسترسی خواندن و نوشتن می‌دهند.

برای تأیید داده‌های ورودی و احراز هویت کاربر، از دو شیء که در متن هر قانون موجود هستند استفاده می‌کنیم:

  • شیء request (request object) شامل داده‌ها و فراداده‌هایی (metadata) در مورد عملیاتی است که انجام می‌شود.
  • اگر یک پروژه Firebase از Firebase Authentication استفاده می‌کند، شیء request.auth کاربری را که درخواست را انجام می‌دهد توصیف می‌کند.

۱۰. دسترسی به سبد خرید را آزمایش کنید

مجموعه شبیه‌ساز به طور خودکار هر زمان که firestore.rules ذخیره می‌شود، قوانین را به‌روزرسانی می‌کند. می‌توانید با نگاه کردن به تب اجرای شبیه‌ساز و یافتن پیام Rules updated از به‌روزرسانی قوانین توسط شبیه‌ساز اطمینان حاصل کنید:

5680da418b420226.png

تست‌ها را دوباره اجرا کنید و بررسی کنید که دو تست اول اکنون با موفقیت پشت سر گذاشته شده‌اند:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    1) can be read only by the cart owner
    2) can be added only by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  2 passing (482ms)
  1 pending
  2 failing

آفرین! حالا شما دسترسی به سبدهای خرید را تضمین کرده‌اید. بیایید به سراغ آزمون ناموفق بعدی برویم.

۱۱. جریان «افزودن به سبد خرید» را در رابط کاربری بررسی کنید

در حال حاضر، اگرچه صاحبان سبد خرید می‌توانند سبد خرید خود را بخوانند و بنویسند، اما نمی‌توانند اقلام تکی موجود در سبد خرید خود را بخوانند یا بنویسند. دلیل این امر این است که اگرچه صاحبان سبد خرید به سند سبد خرید دسترسی دارند، اما به زیرمجموعه اقلام سبد خرید دسترسی ندارند.

این یک وضعیت خراب برای کاربران است.

به رابط کاربری وب که روی http://127.0.0.1:5000, برگردید و سعی کنید چیزی به سبد خرید خود اضافه کنید. با خطای Permission Denied مواجه می‌شوید که از کنسول اشکال‌زدایی قابل مشاهده است، زیرا ما هنوز به کاربران اجازه دسترسی به اسناد ایجاد شده در زیرمجموعه items را نداده‌ایم.

۱۲. اجازه دسترسی به اقلام سبد خرید

این دو آزمایش تأیید می‌کنند که کاربران فقط می‌توانند اقلامی را به سبد خرید خود اضافه کنند یا اقلامی را از آن بخوانند:

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

    // ...
  }
}

۱۳. دسترسی به اقلام سبد خرید را آزمایش کنید

حالا می‌توانیم تست را دوباره اجرا کنیم. به بالای خروجی بروید و بررسی کنید که تست‌های بیشتری با موفقیت انجام شوند:

$ 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

عالی! حالا همه تست‌های ما با موفقیت انجام شدند. یک تست در حال بررسی داریم، اما در چند مرحله به آن خواهیم پرداخت.

۱۴. دوباره روند «افزودن به سبد خرید» را بررسی کنید

به بخش کاربری وب ( http://127.0.0.1:5000 ) برگردید و یک کالا به سبد خرید اضافه کنید. این یک گام مهم برای تأیید مطابقت تست‌ها و قوانین ما با عملکرد مورد نیاز مشتری است. (به یاد داشته باشید که آخرین باری که رابط کاربری را امتحان کردیم، کاربران قادر به اضافه کردن کالا به سبد خرید خود نبودند!)

69ad26cee520bf24.png

کلاینت به طور خودکار وقتی firestore.rules ذخیره می‌شود، قوانین را دوباره بارگذاری می‌کند. بنابراین، سعی کنید چیزی به سبد خرید اضافه کنید.

خلاصه

آفرین! شما امنیت برنامه‌تان را بهبود بخشیدید، گامی اساسی برای آماده‌سازی آن برای تولید انبوه! اگر این یک برنامه‌ی تولیدی بود، می‌توانستیم این تست‌ها را به خط تولید یکپارچه‌سازی مداوم خود اضافه کنیم. این به ما اطمینان می‌دهد که در آینده، حتی اگر دیگران قوانین را تغییر دهند، داده‌های سبد خرید ما این کنترل‌های دسترسی را خواهند داشت.

ba5440b193e75967.gif

اما صبر کنید، چیزهای بیشتری هم هست!

اگر ادامه بدهی، یاد خواهی گرفت:

  • نحوه نوشتن تابعی که توسط یک رویداد Firestore فعال می‌شود
  • نحوه ایجاد تست‌هایی که در چندین شبیه‌ساز کار می‌کنند

۱۵. تست‌های توابع ابری را تنظیم کنید

تاکنون ما روی ظاهر برنامه وب خود و قوانین امنیتی Firestore تمرکز کرده‌ایم. اما این برنامه همچنین از توابع ابری برای به‌روز نگه داشتن سبد خرید کاربر استفاده می‌کند، بنابراین می‌خواهیم آن کد را نیز آزمایش کنیم.

مجموعه شبیه‌ساز، آزمایش توابع ابری، حتی توابعی که از 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";

اگر شناسه پروژه خود را فراموش کرده‌اید، می‌توانید شناسه پروژه فایربیس خود را در تنظیمات پروژه در کنسول فایربیس پیدا کنید:

d6d0429b700d2b21.png

۱۶. تست‌های توابع را مرور کنید

از آنجا که این آزمایش، تعامل بین Cloud Firestore و Cloud Functions را تأیید می‌کند، شامل تنظیمات بیشتری نسبت به آزمایش‌های موجود در آزمایشگاه‌های کد قبلی است. بیایید این آزمایش را بررسی کنیم و ایده‌ای از آنچه انتظار می‌رود، به دست آوریم.

ایجاد سبد خرید

توابع ابری در یک محیط سرور قابل اعتماد اجرا می‌شوند و می‌توانند از احراز هویت حساب سرویس که توسط Admin SDK استفاده می‌شود، استفاده کنند. ابتدا، شما یک برنامه را با استفاده از initializeAdminApp به جای initializeApp مقداردهی اولیه می‌کنید. سپس، یک DocumentReference برای سبد خریدی که قرار است به آن اقلام اضافه کنیم ایجاد می‌کنید و سبد خرید را مقداردهی اولیه می‌کنید:

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    ...
  });

تابع را فعال کنید

سپس، اسناد را به زیرمجموعه items سند سبد خرید خود اضافه کنید تا تابع فعال شود. دو مورد اضافه کنید تا مطمئن شوید که عملیات جمع در تابع را آزمایش می‌کنید.

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });

    ...
    });
  });

انتظارات آزمون را تعیین کنید

onSnapshot() برای ثبت یک شنونده (listener) برای هرگونه تغییر در سند سبد خرید استفاده کنید. onSnapshot() تابعی را برمی‌گرداند که می‌توانید برای لغو ثبت شنونده (listener) فراخوانی کنید.

برای این تست، دو کالا که روی هم رفته ۹.۹۸ دلار قیمت دارند را جمع کنید. سپس بررسی کنید که آیا سبد خرید 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();
        };
      });
    });
   });
 });

۱۷. تست‌ها را اجرا کنید

ممکن است هنوز شبیه‌سازهای تست‌های قبلی را در حال اجرا داشته باشید. در غیر این صورت، شبیه‌سازها را اجرا کنید. از خط فرمان، دستور زیر را اجرا کنید:

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

یک تب ترمینال جدید باز کنید (شبیه‌سازها را در حال اجرا بگذارید) و به دایرکتوری functions بروید. ممکن است هنوز این پوشه را از تست‌های قوانین امنیتی باز داشته باشید.

$ cd functions

حالا تست‌های واحد را اجرا کنید، باید در مجموع ۵ تست ببینید:

$ 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

اگر به خطای خاص نگاه کنید، به نظر می‌رسد که یک خطای timeout است. دلیل این امر این است که تست منتظر است تا تابع به درستی به‌روزرسانی شود، اما هرگز این کار را نمی‌کند. اکنون، ما آماده‌ایم تا تابعی بنویسیم که تست را برآورده کند.

۱۸. یک تابع بنویسید

برای رفع این مشکل، باید تابع را در functions/index.js به‌روزرسانی کنید. اگرچه مقداری از این تابع نوشته شده است، اما کامل نیست. در حال حاضر، تابع به این شکل است:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 125.98;
      let itemCount = 8;
      try {
        
        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

این تابع به درستی مرجع سبد خرید را تنظیم می‌کند، اما سپس به جای محاسبه مقادیر totalPrice و itemCount ، آنها را به مقادیر ثابت به‌روزرسانی می‌کند.

واکشی و تکرار از طریق

زیرمجموعه items

یک ثابت جدید itemsSnap به عنوان زیرمجموعه items مقداردهی اولیه کنید. سپس، روی تمام اسناد موجود در مجموعه پیمایش کنید.

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }


      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        // ADD LINES FROM HERE
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
        })
        // TO HERE
       
        return cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

محاسبه‌ی totalPrice و itemCount

ابتدا، مقادیر totalPrice و itemCount را با صفر مقداردهی اولیه می‌کنیم.

سپس، منطق را به بلوک تکرار خود اضافه کنید. ابتدا، بررسی کنید که کالا قیمت داشته باشد. اگر کالا تعداد مشخصی ندارد، مقدار پیش‌فرض را 1 قرار دهید. سپس، تعداد را به جمع کل جاری itemCount اضافه کنید. در نهایت، قیمت کالا ضربدر تعداد را به جمع کل جاری totalPrice اضافه کنید:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      try {
        // CHANGE THESE LINES
        let totalPrice = 0;
        let itemCount = 0;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          // ADD LINES FROM HERE
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = itemData.quantity ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
          // TO HERE
        })

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

همچنین می‌توانید برای کمک به اشکال‌زدایی حالت‌های موفقیت و خطا، گزارش‌گیری را اضافه کنید:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 0;
      let itemCount = 0;
      try {
        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = (itemData.quantity) ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
        });

        await cartRef.update({
          totalPrice,
          itemCount
        });

        // OPTIONAL LOGGING HERE
        console.log("Cart total successfully recalculated: ", totalPrice);
      } catch(err) {
        // OPTIONAL LOGGING HERE
        console.warn("update error", err);
      }
    });

۱۹. آزمایش‌های مجدد

در خط فرمان، مطمئن شوید که شبیه‌سازها هنوز در حال اجرا هستند و تست‌ها را دوباره اجرا کنید. نیازی به راه‌اندازی مجدد شبیه‌سازها نیست زیرا آنها به طور خودکار تغییرات در توابع را دریافت می‌کنند. باید ببینید که همه تست‌ها با موفقیت انجام می‌شوند:

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

آفرین! آفرین!

۲۰. با استفاده از رابط کاربری Storefront آن را امتحان کنید

برای آزمایش نهایی، به برنامه وب ( http://127.0.0.1:5000/ ) برگردید و یک کالا را به سبد خرید اضافه کنید.

69ad26cee520bf24.png

تأیید کنید که سبد خرید با مجموع صحیح به‌روزرسانی می‌شود. فوق‌العاده است!

خلاصه

شما یک مورد آزمایشی پیچیده بین توابع ابری برای Firebase و Cloud Firestore را پشت سر گذاشته‌اید. شما یک تابع ابری نوشته‌اید تا تست با موفقیت انجام شود. همچنین تأیید کرده‌اید که قابلیت‌های جدید در رابط کاربری کار می‌کنند! شما همه این کارها را به صورت محلی انجام داده‌اید و شبیه‌سازها را روی دستگاه خود اجرا کرده‌اید.

شما همچنین یک کلاینت وب ایجاد کرده‌اید که در برابر شبیه‌سازهای محلی اجرا می‌شود، قوانین امنیتی را برای محافظت از داده‌ها تنظیم کرده‌اید و قوانین امنیتی را با استفاده از شبیه‌سازهای محلی آزمایش کرده‌اید.

c6a7aeb91fe97a64.gif