التطوير المحلي باستخدام 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.

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

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

$ 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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost: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 │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost: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 === "localhost") {
    console.log("localhost detected!");
    auth.useEmulator("http://localhost:9099");
    db.useEmulator("localhost", 8080);
  }

الآن عندما يعمل التطبيق على مضيف محلي (يخدمه محاكي الاستضافة) ، يشير عميل Firestore أيضًا إلى المحاكي المحلي بدلاً من قاعدة بيانات الإنتاج.

افتح EmulatorUI

في متصفح الويب الخاص بك ، انتقل إلى http: // localhost: 4000 / . يجب أن تشاهد واجهة مستخدم Emulator Suite.

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

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

4ef88d0148405d36.png

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

افتح التطبيق

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

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 Authentication ، عندما لا نكون مسجلين الدخول ، 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 - العميل

تم الاشتراك في واجهة الويب لتلقي تحديثات حول التغييرات التي تم إجراؤها على سلة التسوق. يحصل على تحديث في الوقت الفعلي بعد أن تكتب Cloud Function المجاميع الجديدة وتقوم بتحديث واجهة المستخدم ، كما ترى في 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

الآن قم بتشغيل اختبارات mocha في دليل الوظائف ، وانتقل إلى أعلى الإخراج:

# 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_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://localhost: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: // localhost: 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 Console:

d6d0429b700d2b21.png

16. اجتياز اختبارات الوظائف

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

إنشاء عربة

تعمل وظائف السحابة في بيئة خادم موثوقة ويمكنها استخدام مصادقة حساب الخدمة المستخدمة بواسطة 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 UI

للاختبار النهائي ، ارجع إلى تطبيق الويب ( http: // localhost: 5000 / ) وأضف عنصرًا إلى سلة التسوق.

69ad26cee520bf24.png

تأكد من تحديث سلة التسوق بالمجموع الصحيح. جميل!

خلاصة

لقد مررت في حالة اختبار معقدة بين وظائف السحابة لـ Firebase و Cloud Firestore. لقد قمت بكتابة وظيفة السحابة لاجتياز الاختبار. لقد أكدت أيضًا أن الوظيفة الجديدة تعمل في واجهة المستخدم! لقد فعلت كل هذا محليًا ، وقمت بتشغيل المحاكيات على جهازك الخاص.

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

c6a7aeb91fe97a64.gif