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 لتمكين التطوير المحلي.
ستتعلم أيضًا كيفية:
- كيفية توصيل تطبيقك بـ 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://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. تحتوي مجموعة items
بالفعل على بيانات بسبب البيانات المستوردة بعلامة --import
.
4. قم بتشغيل التطبيق
افتح التطبيق
في متصفح الويب الخاص بك ، انتقل إلى http://127.0.0.1:5000 وسترى Fire Store يعمل محليًا على جهازك!
استخدم التطبيق
اختر عنصرًا في الصفحة الرئيسية وانقر فوق إضافة إلى عربة التسوق . لسوء الحظ ، ستواجه الخطأ التالي:
دعونا نصلح هذا الخطأ! نظرًا لأن كل شيء يعمل في برامج المحاكاة ، يمكننا التجربة وعدم القلق بشأن التأثير على البيانات الحقيقية.
5. تصحيح التطبيق
ابحث عن الخطأ
حسنًا ، لنلقِ نظرة على وحدة تحكم مطوري Chrome. اضغط على Control+Shift+J
(Windows ، Linux ، Chrome OS) أو Command+Option+J
(Mac) لرؤية الخطأ على وحدة التحكم:
يبدو أنه كان هناك خطأ ما في طريقة 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;
}
// ...
}
اختبر التطبيق
الآن ، قم بتحديث الصفحة ثم انقر فوق إضافة إلى عربة التسوق . يجب أن تحصل على خطأ أجمل هذه المرة:
ولكن إذا قمت بالنقر فوق "تسجيل الدخول" في شريط الأدوات العلوي ثم النقر فوق "إضافة إلى عربة التسوق" مرة أخرى ، فسترى أنه تم تحديث سلة التسوق.
ومع ذلك ، لا يبدو أن الأرقام صحيحة على الإطلاق:
لا تقلق ، سنصلح هذا الخطأ قريبًا. أولاً ، دعنا نتعمق في ما حدث بالفعل عندما أضفت عنصرًا إلى سلة التسوق الخاصة بك.
6. مشغلات الوظائف المحلية
يؤدي النقر فوق إضافة إلى عربة التسوق إلى بدء سلسلة من الأحداث التي تتضمن العديد من المحاكيات. في سجلات Firebase CLI ، من المفترض أن ترى شيئًا مثل الرسائل التالية بعد إضافة عنصر إلى سلة التسوق الخاصة بك:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
كانت هناك أربعة أحداث رئيسية حدثت لإنتاج تلك السجلات وتحديث واجهة المستخدم الذي لاحظته:
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 مختلفة للاختبار المحلي بالكامل.
ولكن انتظر هناك المزيد! ستتعلم في القسم التالي:
- كيفية كتابة اختبارات الوحدة التي تستخدم محاكيات 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
:
أعد إجراء الاختبارات ، وتحقق من اجتياز أول اختبارين الآن:
$ 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 ) وأضف عنصرًا إلى سلة التسوق. هذه خطوة مهمة لتأكيد أن اختباراتنا وقواعدنا تتطابق مع الوظائف المطلوبة من قبل العميل. (تذكر أنه في المرة الأخيرة التي جربنا فيها واجهة المستخدم لم يتمكن المستخدمون من إضافة عناصر إلى سلة التسوق الخاصة بهم!)
يقوم العميل تلقائيًا بإعادة تحميل القواعد عند حفظ firestore.rules
. لذا ، حاول إضافة شيء ما إلى عربة التسوق.
خلاصة
عمل جيد! لقد قمت للتو بتحسين أمان تطبيقك ، وهي خطوة أساسية لتجهيزه للإنتاج! إذا كان هذا تطبيقًا للإنتاج ، فيمكننا إضافة هذه الاختبارات إلى خط أنابيب التكامل المستمر. سيعطينا هذا الثقة من الآن فصاعدًا أن بيانات سلة التسوق الخاصة بنا ستشتمل على عناصر التحكم في الوصول ، حتى لو كان الآخرون يعدلون القواعد.
ولكن انتظر هناك المزيد!
إذا تابعت ستتعلم:
- كيفية كتابة دالة يتم تشغيلها بواسطة حدث 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:
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://127.0.0.1:5000/ ) وأضف عنصرًا إلى سلة التسوق.
تأكد من تحديث سلة التسوق بالمجموع الصحيح. رائع!
خلاصة
لقد مررت في حالة اختبار معقدة بين وظائف السحابة لـ Firebase و Cloud Firestore. لقد قمت بكتابة وظيفة السحابة لاجتياز الاختبار. لقد أكدت أيضًا أن الوظيفة الجديدة تعمل في واجهة المستخدم! لقد فعلت كل هذا محليًا ، وقمت بتشغيل المحاكيات على جهازك الخاص.
لقد أنشأت أيضًا عميل ويب يعمل ضد المحاكيات المحلية ، وقواعد أمان مخصصة لحماية البيانات ، واختبرت قواعد الأمان باستخدام المحاكيات المحلية.