التطوير المحلي باستخدام Firebase Emulator Suite

1. قبل أن تبدأ

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

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

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

ماذا ستفعل

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

  • سحابة Firestore: أ قابلة عالميا، serverless، قاعدة بيانات NoSQL مع قدرات الوقت الحقيقي.
  • سحابة وظائف: رمز الخلفية serverless أن يعمل في استجابة لأحداث أو 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.

بمجرد رؤية جميع برامج محاكاة التي رسالة، التطبيق جاهزا للاستخدام.

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

واستنادا إلى الجدول في سجلات يمكننا أن نرى أن المضاهاة سحابة 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: // المضيف المحلي: 4000 / . يجب أن تشاهد واجهة مستخدم Emulator Suite.

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

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

4ef88d0148405d36.png

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

افتح التطبيق

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

939f87946bac2ee4.png

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

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

a11bd59933a8e885.png

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

5. تصحيح التطبيق

ابحث عن الخطأ

حسنًا ، لنلقِ نظرة على وحدة تحكم مطوري Chrome. الصحافة Control+Shift+J (ويندوز، لينكس، والكروم OS) أو Command+Option+J (ماك) لمعرفة الخطأ على وحدة التحكم:

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) تفعيل وظيفة السحابة

سحابة وظيفة 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

الآن قم بتشغيل اختبارات 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. اختبار الوصول إلى عربة التسوق

جناح المحاكي تلقائيا بتحديث قواعد كلما 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: // المضيف المحلي: 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: // المضيف المحلي: 5000 / ) وإضافة بند إلى عربة.

69ad26cee520bf24.png

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

خلاصة

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

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

c6a7aeb91fe97a64.gif