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

1. قبل از شروع

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

پیش نیازها

کاری که خواهی کرد

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

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

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

2589e2f95b74fa88.png

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

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

2. راه اندازی کنید

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

در این لبه کد، شما با نسخه ای از نمونه فروشگاه آتش شروع می کنید که تقریباً کامل شده است، بنابراین اولین کاری که باید انجام دهید این است که کد منبع را شبیه سازی کنید:

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

سپس به دایرکتوری Codelab بروید، جایی که برای باقیمانده این Codelab کار خواهید کرد:

$ cd emulators-codelab/codelab-initial-state

حالا وابستگی ها را نصب کنید تا بتوانید کد را اجرا کنید. اگر اتصال اینترنت شما کندتر است، ممکن است یک یا دو دقیقه طول بکشد:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

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

Firebase CLI را دریافت کنید

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

$ npm install -g firebase-tools

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

$ firebase --version
9.6.0

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

اگر پروژه Firebase ندارید، در کنسول Firebase ، یک پروژه Firebase جدید ایجاد کنید. شناسه پروژه ای را که انتخاب می کنید یادداشت کنید، بعداً به آن نیاز خواهید داشت.

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

$ firebase login

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

$ firebase use $YOUR_PROJECT_ID

اکنون شما آماده اجرای برنامه هستید!

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

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

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

از داخل پوشه منبع کد لبه، دستور زیر را برای راه اندازی شبیه سازها اجرا کنید:

$ 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

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

public/js/homepage.js

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

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

public/js/homepage.js

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

  // ADD THESE LINES
  if (location.hostname === "127.0.0.1") {
    console.log("127.0.0.1 detected!");
    auth.useEmulator("http://127.0.0.1:9099");
    db.useEmulator("127.0.0.1", 8080);
  }

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

EmulatorUI را باز کنید

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

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

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

4ef88d0148405d36.png

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

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

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

939f87946bac2ee4.png

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

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

a11bd59933a8e885.png

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

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

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

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

74c45df55291dab1.png

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

public/js/homepage.js

  addToCart(id, itemData) {
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

آها! ما وارد برنامه نشده ایم. با توجه به اسناد احراز هویت Firebase ، زمانی که ما وارد سیستم نشده‌ایم، auth.currentUser null است. بیایید یک چک برای آن اضافه کنیم:

public/js/homepage.js

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

    // ...
  }

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

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

c65f6c05588133f7.png

اما اگر در نوار ابزار بالا گزینه Sign In را بزنید و سپس دوباره Add to Cart را بزنید، می بینید که سبد خرید به روز شده است.

با این حال، به نظر می رسد که اعداد به هیچ وجه صحیح نیستند:

239f26f02f959eef.png

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

6. توابع محلی باعث می شود

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

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

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

68c9323f2ad10f7a.png

1) Firestore Write - Client

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

public/js/homepage.js

  addToCart(id, itemData) {
    // ...
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

2) عملکرد ابر فعال شد

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

functions/index.js

exports.calculateCart = functions.firestore
    .document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);

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

3) Firestore Write - Admin

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

4) Firestore Read - Client

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

public/js/homepage.js

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

خلاصه

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

db82eef1706c9058.gif

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

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

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

برنامه وب ما داده‌ها را می‌خواند و می‌نویسد، اما تاکنون واقعاً نگران امنیت نبوده‌ایم. Cloud Firestore از سیستمی به نام "قوانین امنیتی" برای اعلام اینکه چه کسی به خواندن و نوشتن داده ها دسترسی دارد استفاده می کند. مجموعه Emulator یک راه عالی برای نمونه سازی اولیه این قوانین است.

در ویرایشگر، فایل 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;
    }
  }
}

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

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

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

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

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

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

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

ابتدا به دایرکتوری توابع بروید (ما برای باقیمانده از Codelab اینجا خواهیم ماند):

$ cd functions

اکنون تست های موکا را در پوشه توابع اجرا کنید و به بالای خروجی بروید:

# Run the tests
$ npm test

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

  shopping carts
    1) can be created and updated by the cart owner
    2) can be read only by the cart owner

  shopping cart items
    3) can be read only by the cart owner
    4) can be added only by the cart owner

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


  0 passing (364ms)
  1 pending
  4 failing

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

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

دو شکست اول تست های "سبد خرید" هستند که آزمایش می کنند:

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

functions/test.js

  it('can be created and updated by the cart owner', async () => {
    // Alice can create her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Bob can't create Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Alice can update her own cart with a new total
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
      total: 1
    }));

    // Bob can't update Alice's cart with a new total
    await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
      total: 1
    }));
  });

  it("can be read only by the cart owner", async () => {
    // Setup: Create Alice's cart as admin
    await admin.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    });

    // Alice can read her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());

    // Bob can't read Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
  });

بیایید این آزمون ها را پاس کنیم. در ویرایشگر، فایل قوانین امنیتی، firestore.rules باز کنید و عبارات موجود در match /carts/{cartID} را به روز کنید:

firestore.قوانین

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

    // ...
  }
}

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

برای تأیید داده های ورودی و تأیید اعتبار کاربر، از دو شی استفاده می کنیم که در زمینه هر قانون در دسترس هستند:

  • شی request حاوی داده ها و فراداده های مربوط به عملیاتی است که در حال تلاش است.
  • اگر یک پروژه Firebase از احراز هویت Firebase استفاده می کند، شی request.auth کاربری را که درخواست می کند توصیف می کند.

10. تست دسترسی به سبد خرید

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

5680da418b420226.png

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

$ npm test

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

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

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

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

  2 passing (482ms)
  1 pending
  2 failing

آفرین! اکنون دسترسی به سبد خرید را ایمن کرده اید. بیایید به سراغ آزمون مردود بعدی برویم.

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

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

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

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

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

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

  it("can be read only by the cart owner", async () => {
    // Alice can read items in her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());

    // Bob can't read items in alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
  });

  it("can be added only by the cart owner",  async () => {
    // Alice can add an item to her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));

    // Bob can't add an item to alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));
  });

بنابراین می‌توانیم قاعده‌ای بنویسیم که در صورتی که کاربر فعلی همان UID مالک را در سند سبد خرید داشته باشد، اجازه دسترسی را می‌دهد. از آنجایی که نیازی به تعیین قوانین مختلف برای create, update, delete نیست، می‌توانید از یک قانون write استفاده کنید که برای همه درخواست‌هایی که داده‌ها را تغییر می‌دهند اعمال می‌شود.

قانون مدارک موجود در زیر مجموعه اقلام را به روز کنید. get در شرطی خواندن یک مقدار از Firestore است – در این مورد، ownerUID در سند سبد خرید.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ...

    // UPDATE THESE LINES
    match /carts/{cartID}/items/{itemID} {
      allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

13. تست دسترسی به سبد خرید

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

$ npm test

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

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

  shopping cart items
    ✓ can be read only by the cart owner (111ms)
    ✓ can be added only by the cart owner


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


  4 passing (401ms)
  1 pending

خوب! اکنون همه آزمون های ما قبول می شوند. ما یک آزمایش معلق داریم، اما در چند مرحله به آن خواهیم رسید.

14. دوباره جریان "افزودن به سبد خرید" را بررسی کنید

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

69ad26cee520bf24.png

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

خلاصه

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

ba5440b193e75967.gif

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

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

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

15. تست های Cloud Functions را تنظیم کنید

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

مجموعه Emulator آزمایش عملکردهای Cloud را بسیار آسان می کند، حتی عملکردهایی که از 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 را در بالای فایل پیدا کنید و آن را به ID پروژه Firebase واقعی خود تغییر دهید.

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

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

d6d0429b700d2b21.png

16. در تست های توابع قدم بزنید

از آنجایی که این تست تعامل بین 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() برای ثبت شنونده برای هرگونه تغییر در سند سبد خرید استفاده کنید. onSnapshot() تابعی را برمی گرداند که می توانید برای لغو ثبت شنونده آن را فراخوانی کنید.

برای این تست، دو مورد اضافه کنید که مجموعا 9.98 دلار قیمت دارند. سپس، بررسی کنید که آیا سبد خرید itemCount مورد انتظار و totalPrice دارد یا خیر. اگر چنین است، پس تابع کار خود را انجام داد.

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

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

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
    
    // Listen for every update to the cart. Every time an item is added to
    // the cart's subcollection of items, the function updates `totalPrice`
    // and `itemCount` attributes on the cart.
    // Returns a function that can be called to unsubscribe the listener.
    await new Promise((resolve) => {
      const unsubscribe = aliceCartRef.onSnapshot(snap => {
        // If the function worked, these will be cart's final attributes.
        const expectedCount = 2;
        const expectedTotal = 9.98;
  
        // When the `itemCount`and `totalPrice` match the expectations for the
        // two items added, the promise resolves, and the test passes.
        if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
          // Call the function returned by `onSnapshot` to unsubscribe from updates
          unsubscribe();
          resolve();
        };
      });
    });
   });
 });

17. تست ها را اجرا کنید

ممکن است همچنان شبیه‌سازهای آزمایش‌های قبلی را داشته باشید. اگر نه، شبیه سازها را راه اندازی کنید. از خط فرمان، اجرا کنید

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

یک برگه ترمینال جدید باز کنید (امولاتورها را در حال اجرا بگذارید) و به فهرست توابع بروید. ممکن است هنوز این مورد را از آزمایش قوانین امنیتی باز داشته باشید.

$ cd functions

اکنون تست های واحد را اجرا کنید، باید 5 تست را مشاهده کنید:

$ npm test

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

  shopping cart creation
    ✓ can be created by the cart owner (82ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (42ms)

  shopping cart items
    ✓ items can be read by the cart owner (40ms)
    ✓ items can be added by the cart owner

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

  4 passing (2s)
  1 failing

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

18. یک تابع بنویسید

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

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

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

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

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

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

زیر مجموعه items

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

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


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

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

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

مجموع قیمت و تعداد آیتم را محاسبه کنید

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

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

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

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

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

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

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

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

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

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

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

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

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

19. تکرار تست ها

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

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

  shopping cart creation
    ✓ can be created by the cart owner (306ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (59ms)

  shopping cart items
    ✓ items can be read by the cart owner
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    ✓ should sum the cost of their items (800ms)


  5 passing (1s)

آفرین!

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

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

69ad26cee520bf24.png

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

خلاصه

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

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

c6a7aeb91fe97a64.gif