التنمية المحلية باستخدام Firebase Emulator Suite

1. قبل أن تبدأ

من السهل جدًا استخدام أدوات الواجهة الخلفية بدون خادم، مثل Cloud Firestore وCloud Functions، ولكن قد يكون من الصعب اختبارها. يتيح لك Firebase Local Emulator Suite تشغيل الإصدارات المحلية من هذه الخدمات على جهاز التطوير الخاص بك حتى تتمكن من تطوير تطبيقك بسرعة وأمان.

المتطلبات الأساسية

  • محرر بسيط مثل Visual Studio Code أو Atom أو Sublime Text
  • Node.js 10.0.0 أو أعلى (لتثبيت Node.js، استخدم nvm للتحقق من الإصدار الخاص بك، قم بتشغيل node --version )
  • Java 7 أو أعلى (لتثبيت Java، استخدم هذه الإرشادات ، وللتحقق من الإصدار لديك، قم بتشغيل java -version )

ماذا ستفعل

في هذا الدرس التطبيقي حول التعليمات البرمجية، ستقوم بتشغيل وتصحيح تطبيق بسيط للتسوق عبر الإنترنت والذي يتم تشغيله بواسطة خدمات Firebase المتعددة:

  • Cloud Firestore: قاعدة بيانات NoSQL قابلة للتطوير عالميًا وبدون خادم مع إمكانات في الوقت الفعلي.
  • وظائف السحابة : رمز خلفي بدون خادم يتم تشغيله استجابة للأحداث أو طلبات HTTP.
  • مصادقة Firebase : خدمة مصادقة مُدارة تتكامل مع منتجات Firebase الأخرى.
  • استضافة Firebase : استضافة سريعة وآمنة لتطبيقات الويب.

ستقوم بتوصيل التطبيق بمجموعة Emulator Suite لتمكين التطوير المحلي.

2589e2f95b74fa88.png

ستتعلم أيضًا كيفية:

  • كيفية ربط تطبيقك بمجموعة Emulator Suite وكيفية توصيل المحاكيات المختلفة.
  • كيف تعمل قواعد أمان Firebase وكيفية اختبار قواعد أمان Firestore مقابل محاكي محلي.
  • كيفية كتابة وظيفة Firebase التي يتم تشغيلها بواسطة أحداث Firestore وكيفية كتابة اختبارات التكامل التي يتم تشغيلها مقابل Emulator Suite.

2. اقامة

احصل على الكود المصدري

في هذا الدرس التطبيقي حول التعليمات البرمجية، ستبدأ بإصدار من نموذج Fire Store الذي أوشك على الاكتمال، لذا فإن أول شيء يتعين عليك فعله هو استنساخ الكود المصدري:

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

بعد ذلك، انتقل إلى دليل 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 Suite جزءًا من 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

قم بعد ذلك بتشغيل الأمر التالي لإنشاء اسم مستعار للمشروع. استبدل $YOUR_PROJECT_ID بمعرف مشروع Firebase الخاص بك.

$ firebase use $YOUR_PROJECT_ID

أنت الآن جاهز لتشغيل التطبيق!

3. قم بتشغيل المحاكيات

في هذا القسم، ستقوم بتشغيل التطبيق محليًا. هذا يعني أن الوقت قد حان لتشغيل Emulator Suite.

ابدأ تشغيل المحاكيات

من داخل دليل مصدر Codelab، قم بتشغيل الأمر التالي لبدء المحاكيات:

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

يجب أن تشاهد بعض الإخراج مثل هذا:

$ firebase emulators:start --import=./seed
i  emulators: Starting emulators: auth, functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
i  firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://127.0.0.1:5000
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions...
✔  functions[calculateCart]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000                │
└─────────────────────────────────────────────────────────────┘

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

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

بمجرد ظهور رسالة بدء جميع المحاكيات ، يكون التطبيق جاهزًا للاستخدام.

قم بتوصيل تطبيق الويب بالمحاكيات

استنادًا إلى الجدول الموجود في السجلات، يمكننا أن نرى أن محاكي Cloud Firestore يستمع على المنفذ 8080 وأن محاكي المصادقة يستمع على المنفذ 9099 .

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

لنقم بتوصيل كود الواجهة الأمامية الخاص بك بالمحاكي، بدلاً من الإنتاج. افتح الملف public/js/homepage.js وابحث عن وظيفة onDocumentReady . يمكننا أن نرى أن الكود يصل إلى مثيلات Firestore و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.

الشاشة الرئيسية لواجهة المستخدم الخاصة بالمحاكيات

انقر لرؤية واجهة المستخدم الخاصة بـ Firestore Emulator. تحتوي مجموعة items بالفعل على بيانات بسبب البيانات المستوردة باستخدام علامة --import .

4ef88d0148405d36.png

4. قم بتشغيل التطبيق

افتح التطبيق

في متصفح الويب الخاص بك، انتقل إلى http://127.0.0.1:5000 ومن المفترض أن ترى Fire Store يعمل محليًا على جهازك!

939f87946bac2ee4.png

استخدم التطبيق

اختر عنصرًا من الصفحة الرئيسية وانقر فوق "إضافة إلى سلة التسوق" . لسوء الحظ، سوف تواجه الخطأ التالي:

a11bd59933a8e885.png

دعونا نصلح هذا الخلل! نظرًا لأن كل شيء يعمل في المحاكيات، يمكننا التجربة دون القلق بشأن التأثير على البيانات الحقيقية.

5. تصحيح أخطاء التطبيق

ابحث عن الخلل

حسنًا، لنلقِ نظرة على وحدة تحكم مطور Chrome. اضغط على Control+Shift+J (Windows وLinux وChrome OS) أو Command+Option+J (Mac) لرؤية الخطأ على وحدة التحكم:

74c45df55291dab1.png

يبدو أنه كان هناك خطأ ما في طريقة addToCart ، فلنلقي نظرة على ذلك. أين نحاول الوصول إلى شيء يسمى uid بهذه الطريقة ولماذا يكون null ؟ تبدو الطريقة الآن هكذا في public/js/homepage.js :

public/js/homepage.js

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

آها! لم نقم بتسجيل الدخول إلى التطبيق. وفقًا لمستندات مصادقة Firebase ، عندما لا نقوم بتسجيل الدخول، يكون auth.currentUser null . دعونا نضيف التحقق من ذلك:

public/js/homepage.js

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

    // ...
  }

اختبر التطبيق

الآن، قم بتحديث الصفحة، ثم انقر فوق "إضافة إلى سلة التسوق" . يجب أن تحصل على خطأ أجمل هذه المرة:

c65f6c05588133f7.png

ولكن إذا قمت بالنقر فوق تسجيل الدخول في شريط الأدوات العلوي ثم انقر فوق إضافة إلى سلة التسوق مرة أخرى، سترى أنه تم تحديث عربة التسوق.

ومع ذلك، لا يبدو أن الأرقام صحيحة على الإطلاق:

239f26f02f959eef.png

لا تقلق، سنقوم بإصلاح هذا الخلل قريبًا. أولاً، دعنا نتعمق في ما حدث بالفعل عندما أضفت عنصرًا إلى سلة التسوق الخاصة بك.

6. مشغلات الوظائف المحلية

يؤدي النقر فوق "إضافة إلى سلة التسوق" إلى إطلاق سلسلة من الأحداث التي تتضمن محاكيات متعددة. في سجلات Firebase CLI، من المفترض أن ترى شيئًا مثل الرسائل التالية بعد إضافة عنصر إلى سلة التسوق الخاصة بك:

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

حدثت أربعة أحداث رئيسية لإنتاج هذه السجلات وتحديث واجهة المستخدم الذي لاحظته:

68c9323f2ad10f7a.png

1) كتابة Firestore - العميل

تتم إضافة مستند جديد إلى مجموعة Firestore /carts/{cartId}/items/{itemId}/ . يمكنك رؤية هذا الرمز في وظيفة addToCart داخل public/js/homepage.js :

public/js/homepage.js

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

2) تم تشغيل وظيفة السحابة

تستمع وظيفة Cloud Function calculateCart إلى أي أحداث كتابة (إنشاء أو تحديث أو حذف) تحدث لعناصر سلة التسوق باستخدام مشغل 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) {
      }
    }
);

3) كتابة Firestore - المشرف

تقوم وظيفة calculateCart بقراءة جميع العناصر الموجودة في سلة التسوق وإضافة إجمالي الكمية والسعر، ثم تقوم بتحديث مستند "عربة التسوق" بالإجماليات الجديدة (راجع cartRef.update(...) أعلاه).

4) قراءة Firestore - العميل

تم الاشتراك في واجهة الويب لتلقي التحديثات حول التغييرات في سلة التسوق. يتم تحديثه في الوقت الفعلي بعد أن تقوم وظيفة السحابة بكتابة الإجماليات الجديدة وتحديث واجهة المستخدم، كما ترون في public/js/homepage.js :

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 Suite طريقة رائعة لوضع نماذج أولية لهذه القواعد.

في المحرر، افتح الملف 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/

انتقل أولاً إلى دليل الوظائف (سنبقى هنا لبقية الدرس التطبيقي حول التعليمات البرمجية):

$ 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. تأمين الوصول إلى العربة

أول فشلين هما اختبارات "عربة التسوق" التي تختبر ما يلي:

  • يمكن للمستخدمين فقط إنشاء وتحديث عربات التسوق الخاصة بهم
  • يمكن للمستخدمين قراءة عربات التسوق الخاصة بهم فقط

وظائف/test.js

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

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

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

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

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

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

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

دعونا ننجح في هذه الاختبارات. في المحرر، افتح ملف قواعد الأمان، firestore.rules ، وقم بتحديث البيانات داخل match /carts/{cartID} :

firestore.rules

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

    // ...
  }
}

تسمح هذه القواعد الآن فقط بوصول القراءة والكتابة لمالك سلة التسوق.

للتحقق من البيانات الواردة ومصادقة المستخدم، نستخدم كائنين متوفرين في سياق كل قاعدة:

  • يحتوي كائن request على بيانات وبيانات تعريفية حول العملية التي تتم محاولتها.
  • إذا كان مشروع Firebase يستخدم مصادقة Firebase ، فإن كائن request.auth يصف المستخدم الذي يقدم الطلب.

10. اختبار الوصول إلى العربة

يقوم Emulator Suite تلقائيًا بتحديث القواعد كلما تم حفظ 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

أحسنت! لقد قمت الآن بتأمين الوصول إلى عربات التسوق. دعنا ننتقل إلى الاختبار الفاشل التالي.

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 مثل OwnerUID في مستند سلة التسوق. نظرًا لعدم الحاجة إلى تحديد قواعد مختلفة create, update, delete ، يمكنك استخدام قاعدة write ، والتي تنطبق على جميع الطلبات التي تقوم بتعديل البيانات.

قم بتحديث قاعدة المستندات الموجودة في المجموعة الفرعية للعناصر. يقوم get الشرطي بقراءة قيمة من Firestore - في هذه الحالة، ownerUID في مستند سلة التسوق.

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

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

    // ...
  }
}

13. اختبار الوصول إلى عناصر عربة التسوق

الآن يمكننا إعادة الاختبار. قم بالتمرير إلى أعلى الإخراج وتأكد من اجتياز المزيد من الاختبارات:

$ npm test

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

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

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


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


  4 passing (401ms)
  1 pending

لطيف - جيد! الآن نجحت جميع اختباراتنا. لدينا اختبار واحد معلق، ولكننا سنصل إليه في بضع خطوات.

14. تحقق من تدفق "الإضافة إلى سلة التسوق" مرة أخرى

ارجع إلى الواجهة الأمامية للويب ( http://127.0.0.1:5000 ) وأضف عنصرًا إلى سلة التسوق. هذه خطوة مهمة للتأكد من أن اختباراتنا وقواعدنا تتوافق مع الوظيفة التي يطلبها العميل. (تذكر أنه في المرة الأخيرة التي جربنا فيها، لم يتمكن مستخدمو واجهة المستخدم من إضافة عناصر إلى سلة التسوق الخاصة بهم!)

69ad26cee520bf24.png

يقوم العميل تلقائيًا بإعادة تحميل القواعد عند حفظ firestore.rules . لذا، حاول إضافة شيء ما إلى سلة التسوق.

خلاصة

عمل جيد! لقد قمت للتو بتحسين أمان تطبيقك، وهي خطوة أساسية لتجهيزه للإنتاج! إذا كان هذا تطبيقًا إنتاجيًا، فيمكننا إضافة هذه الاختبارات إلى مسار التكامل المستمر الخاص بنا. وهذا من شأنه أن يمنحنا الثقة للمضي قدمًا في أن بيانات عربة التسوق الخاصة بنا ستحتوي على عناصر التحكم في الوصول هذه، حتى لو كان الآخرون يقومون بتعديل القواعد.

ba5440b193e75967.gif

ولكن انتظر هناك المزيد!

إذا تابعت ستتعلم:

  • كيفية كتابة دالة تم تشغيلها بواسطة حدث Firestore
  • كيفية إنشاء اختبارات تعمل عبر محاكيات متعددة

15. قم بإعداد اختبارات الوظائف السحابية

لقد ركزنا حتى الآن على الواجهة الأمامية لتطبيق الويب الخاص بنا وقواعد أمان Firestore. ولكن هذا التطبيق يستخدم أيضًا وظائف السحابة لإبقاء عربة التسوق الخاصة بالمستخدم محدثة، لذلك نريد اختبار هذا الرمز أيضًا.

تسهل Emulator Suite اختبار وظائف السحابة، حتى الوظائف التي تستخدم Cloud Firestore والخدمات الأخرى.

في المحرر، افتح ملف emulators-codelab/codelab-initial-state/functions/test.js وانتقل إلى الاختبار الأخير في الملف. في الوقت الحالي، تم وضع علامة "معلق" عليه:

//  REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
  // ...

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

لتمكين الاختبار، قم بإزالة .skip ، بحيث يبدو كما يلي:

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

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

بعد ذلك، ابحث عن المتغير REAL_FIREBASE_PROJECT_ID في الجزء العلوي من الملف وقم بتغييره إلى معرف مشروع Firebase الحقيقي.:

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

إذا نسيت معرف مشروعك، فيمكنك العثور على معرف مشروع Firebase الخاص بك في إعدادات المشروع في وحدة تحكم Firebase:

d6d0429b700d2b21.png

16. قم بإجراء اختبارات الوظائف

نظرًا لأن هذا الاختبار يتحقق من صحة التفاعل بين Cloud Firestore وCloud Functions، فإنه يتضمن إعدادًا أكثر من الاختبارات الموجودة في دروس التعليمات البرمجية السابقة. دعنا نسير خلال هذا الاختبار ونحصل على فكرة عما يتوقعه.

إنشاء عربة

تعمل وظائف السحابة في بيئة خادم موثوقة ويمكنها استخدام مصادقة حساب الخدمة التي تستخدمها 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. لقد كتبت دالة سحابية لاجتياز الاختبار. لقد أكدت أيضًا أن الوظيفة الجديدة تعمل في واجهة المستخدم! لقد فعلت كل هذا محليًا، وقمت بتشغيل المحاكيات على جهازك الخاص.

لقد قمت أيضًا بإنشاء عميل ويب يعمل ضد المحاكيات المحلية، وقواعد أمان مخصصة لحماية البيانات، واختبار قواعد الأمان باستخدام المحاكيات المحلية.

c6a7aeb91fe97a64.gif