اختبار منطق الأعمال ورفع الملفات
اختبار منطق الأعمال (Business Logic Testing) يفحص ما إذا كان التطبيق يفرض قواعده عندما لا يسير المستخدم في المسار المقصود: تخطي خطوة، أو تغيير قيمة كان يُفترض أن يحددها المتصفح، أو استخدام شيء أكثر مما هو مسموح. قد تكون الشيفرة خالية من الحقن وXSS ومع ذلك تسمح لعميل بشراء هاتف بقيمة 500 ريال مقابل ريال واحد، لأن الخادم وثق بحقل price المرسل من المتصفح.
واختبار رفع الملفات (File Upload Testing) هو الفكرة نفسها مطبَّقة على مُدخل واحد شديد الخطورة. فللملف اسم ونوع مُعلَن وحجم ومحتوى حقيقي، وكل واحد منها قد يكذب. يجمع الدرس بينهما لأنهما يشتركان في الطريقة: اقرأ القاعدة، ثم جرّب كل الطرق التي قد يكسرها بها مستخدم غير أمين أو مهمل، ببيانات اختبار غير ضارة وعلى نظام مصرَّح لك باختباره.
لماذا يهم
نادراً ما تكشف ماسحات الثغرات الآلية هذه العيوب، لأنه لا شيء فيها مشوَّه تقنياً: فالطلب JSON صالح، والملف المرفوع صالح. وحده المختبِر الذي يفهم ما تقصده قواعد العمل يرى أن discount: 100 أو تأكيداً ثانياً كان يجب رفضه. وتذكر OWASP هذه الفئة مرتين: التصميم غير الآمن (A06:2025) في قائمة Top 10، والوصول غير المقيَّد إلى تدفقات الأعمال الحساسة (API6:2023) في قائمة API Security Top 10، حيث الخطر المذكور هو الإساءة الآلية لتدفق شراء أو حجز أو تسجيل.
والتكلفة مال وثقة مباشرة: طلبات مجانية، وكوبونات مستنزَفة، ومخزون تشتريه الروبوتات حتى ينفد، وملف مرفوع يُنفَّذ على الخادم أو يشغّل سكربتاً في متصفح مستخدم آخر. أما للمختبِر فهذا هو الجزء من اختبار الأمان الذي يحتاج إلى أقل الأدوات وأكثر التفكير، وفيه تنتقل مهارات الاختبار الوظيفي مباشرةً.
إساءة استخدام المسار: خطوات متخطّاة وحقول معدَّلة
كل تدفق متعدد الخطوات آلة حالات: السلة، وكوبون مطبَّق، وبانتظار الدفع، ومدفوع، ومؤكَّد. يعرض المتصفح الخطوات بالترتيب، لكن الخادم يستقبل طلبات منفصلة ويجب أن يتحقق في كل منها أن طلب الشراء في الحالة الصحيحة. يبدأ الاختبار بكتابة الحالات والطلبات التي تنقل بينها، كما في درس اختبار انتقالات الحالة، ثم تجربة الانتقالات التي لا يسمح بها التصميم: استدعاء طلب التأكيد قبل الدفع، أو إعادة ترتيب الخطوات (اختيار الشحن بعد الدفع)، أو تكرار خطوة يجب أن تحدث مرة واحدة.
والمجموعة الثانية من الاختبارات تغيّر قيماً يجب أن يملكها الخادم. فحقول السعر والخصم والإجمالي والعملة والدور وis_admin لا ينبغي أن تصل من العميل أصلاً؛ فإن وصلت، غيّرها وانظر هل يعيد الخادم الحساب أم يصدّقها. وأضف القيم السالبة والمتطرفة التي تعرفها من تحليل القيم الحدّية: كمية -1 (استرداد متنكر في صورة شراء)، و0، و999999، وخصم 101 بالمئة، وسعر 0.001.
- التخطي: أرسل الخطوة الأخيرة أولاً. المتوقع: خطأ
4xxوعدم تغيّر الحالة. - إعادة الترتيب والتكرار: ادفع مرتين، وأكّد مرتين، وطبّق كوبوناً على سلة فارغة. المتوقع: رفض الاستدعاء الثاني أو جعله بلا أثر.
- التلاعب: غيّر
priceأوtotalأوdiscountأوroleفي الطلب، كما في طلب الدفع أدناه لطلب شراء قيمته 1,899.00. المتوقع:400أو422، أو تجاهل الحقل؛ وفي الحالتين يستخدم الخادم قيمه الخاصة ويعرضها الرد. أما رد200يحصّل 1.00 فقط فهو العيب. - الحدود وإعادة الاستخدام: كوبون واحد لكل عميل، وثلاث محاولات في الساعة، وخمسة عناصر في الطلب. المتوقع: يعدّ الخادم كل استخدام، فيصمد الحد عبر الجلسات والأجهزة وإعادة تحميل الصفحة، والكود المخصص لاستخدام واحد إجمالاً يُرفض أيضاً من حساب ثانٍ.
POST /orders/8841/pay HTTP/1.1
Host: shop.example
Content-Type: application/json
Cookie: session=...
{ "payment_token": "tok_test_123", "total": 1.00, "currency": "SAR" }حالات التسابق: الطلب نفسه مرتين في اللحظة نفسها
حالة التسابق (Race Condition) عيب لا يظهر إلا عندما يلمس طلبان أو أكثر السجل نفسه في اللحظة نفسها تقريباً. والمثال الكلاسيكي هو الإنفاق المزدوج (Double Spend): محفظة فيها 100 ريال، يصل سحبان بقيمة 100 ريال معاً، فيقرأ كلاهما الرصيد قبل أن يكتب أيٌّ منهما، وينجح كلاهما. والشكل نفسه ينتج طلبين من نقرة مزدوجة على «إتمام الطلب»، أو كوبوناً للاستخدام الواحد يُستخدم مرتين، أو حداً من خمس محاولات يسمح بمرور ست.
المفهوم في هذا المستوى أهم من الأدوات. والسلوك المتوقع أن يجري الخادم الفحص والتغيير في خطوة واحدة غير قابلة للتجزئة (Atomic)، عادةً بقفل على الصف داخل معاملة في قاعدة البيانات، أو بتحديث لا ينجح إلا إذا كان الرصيد ما زال كافياً، أو بقيد فريد، بحيث يفوز طلب واحد بالضبط. وللاختبار، أرسل الطلب نفسه مرتين أو أكثر بأقل تأخير ممكن (تبويبان، أو سكربت يطلق الطلبات بالتوازي، أو وظيفة التكرار في وكيل اعتراض)، وقارن الحالة النهائية بما ينبغي أن ينتجه طلب واحد. ومفاتيح منع التكرار (Idempotency Keys) في طلبات الدفع، التي قد تراها في ترويسة Idempotency-Key، هي الحل التصميمي لإعادة المحاولة، وتستحق أن تسأل المطور عنها.
- الإرسال المزدوج: زر الطلب مضغوط مرتين. المتوقع: طلب واحد، وتحصيل واحد للمبلغ، وبريد واحد.
- الإنفاق المزدوج: سحبان، أو استخدامان لكوبون واحد، في اللحظة نفسها. المتوقع: ينجح واحد بالضبط ولا ينزل الرصيد تحت الصفر أبداً.
- حدود العدّادات: محاولات متوازية كثيرة ضد حد. المتوقع: يصمد الحد بالضبط، وتُرفض المحاولة التي تتجاوزه مهما كان توقيت الطلبات.
رفع الملفات: الاسم والنوع والحجم والمحتوى ومكان الحفظ
يعطي الرفع التطبيقَ ثلاثة أشياء قد تتعارض: اسم ملف بامتداده، ونوعاً مُعلَناً للوسائط (MIME Type) في ترويسة Content-Type لجزء الطلب متعدد الأجزاء (Multipart)، والبايتات نفسها. والعميل يختار الأولين بحرية تامة. فملف اسمه report.pdf بنوع application/pdf قد يحتوي صفحة HTML. والمعالجة الآمنة، كما تصفها ورقة OWASP الإرشادية لرفع الملفات (File Upload Cheat Sheet)، هي قائمة سماح بالامتدادات، وفحص للمحتوى الفعلي (توقيع الملف، أي البايتات الأولى التي تحدد صيغته، وللصور: إعادة كتابتها)، وحد للحجم، واسم ملف يولّده الخادم، وتخزين على خادم منفصل أو خارج جذر الويب (المجلد الذي يقدّم منه خادم الويب الملفات)، حيث لن يشغّل الخادم الملف بوصفه شيفرة.
والنصف الثاني من الاختبار هو ما يحدث حين يقدّم التطبيق الملف المخزَّن إلى المتصفح. فملف HTML أو SVG مخزَّن يُفتح من عنوان على أصل التطبيق نفسه يشغّل سكربتاً بجلسة المستخدم: XSS مخزَّن مُسلَّم عبر الرفع. والردود الآمنة ترسل Content-Type الذي قرره الخادم بالضبط، وX-Content-Type-Options: nosniff، وContent-Disposition: attachment (فينزّل المتصفح الملف بدل عرضه) لكل ما ليس صورة نقطية (Raster) مثل PNG أو JPEG تحقّق الخادم من محتواها، أو تقدّم ملفات المستخدمين من نطاق منفصل. وملف SVG صورة أيضاً، لكنه قد يحمل سكربتاً، فيُعامَل معاملة HTML.
- النوع: ملف نصي غير ضار أُعيدت تسميته
.jpg؛ وصورة مرسلة بنوعtext/html؛ وامتداد مزدوجphoto.jpg.html؛ وامتداد بأحرف كبيرة.PHP. المتوقع: يكون الامتداد الأخير، بغض النظر عن حالة الأحرف، ضمن قائمة السماح ويطابقه المحتوى؛ ولا يحسم النوع المُعلَن وحده القرار أبداً. - الحجم: الحد بالضبط، وبايت واحد فوقه، وملف فارغ. المتوقع: قبول؛ ورفض برسالة واضحة؛ ورفض أو معالجة كملف فارغ.
- الاسم:
../../qa-traversal-test.png، وهو محاولة اجتياز مسار (Path Traversal): فإن بنى الخادم مسار الحفظ من الاسم الأصلي، صعد كل../مجلداً واحداً إلى أعلى وخرج من مجلد الرفع (والاسم فريد، فإن وُجد العيب أنشأ ملفاً جديداً بدل الكتابة فوق ملف قائم)؛ واسم من 300 حرف، ومسافات، وحروف عربية، و%00(بايت صفري مرمَّز)، واسم ليس إلا امتداداً. المتوقع: الحفظ باسم يولّده الخادم، والاسم الأصلي للعرض فقط ومرمَّزاً. - تقديم الملف المخزَّن: افتح العنوان المُعاد. المتوقع:
Content-Typeصحيح، وnosniff، وattachmentأو أصل منفصل؛ ولا شيء يُنفَّذ. - التخزين والتنفيذ: اسأل أين تُخزَّن الملفات وهل ذلك المسار قابل للتنفيذ؛ فإن قُبل ملف نصي باسم
.phpأو.aspxأصلاً، فيجب أن يُنزَّل نصاً ولا يُنفَّذ أبداً.
جرّبها: مرجع أنواع MIME جرّبها: مدقّق Content-Type جرّبها: مولّد مجموعات الأحرف الخاصة
مثال تطبيقي: إتمام شراء مع كوبون للاستخدام الواحد
المتطلب: «يضيف العميل عناصر إلى السلة، وله أن يطبّق كوبوناً واحداً (استخدام واحد لكل عميل، والحد الأدنى للطلب 200 ريال)، ويدفع عبر مزوّد الدفع، ويستلم تأكيداً. ويُخصم المخزون عند تأكيد الطلب». والطلبات هي POST /cart/items وPOST /cart/coupon وPOST /orders (ينشئ الطلب ويعيد الإجمالي) وPOST /orders/{id}/pay وPOST /orders/{id}/confirm.
ابدأ بنمذجته حالاتٍ: السلة، وكوبون مطبَّق، وبانتظار الدفع، ومدفوع، ومؤكَّد، ثم اشتق اختباراً لكل انتقال ممنوع، واختباراً لكل حقل يجب أن يملكه الخادم، والقيم الحدّية لكل قاعدة (199.99 و200.00 للحد الأدنى للطلب، مع فحصه مجدداً عند حذف عناصر بعد تطبيق الكوبون). ولكل اختبار مرجع اختبار (Test Oracle) واضح: حالة الطلب وإجماليه بعد الاستدعاء، يُقرآن بـGET /orders/{id}، وعدد المخزون.
| # | من | إلى | الطلب / السلوك المتوقع من الخادم |
|---|---|---|---|
| 1 | السلةالبداية | كوبون مطبَّق | POST /cart/coupon / يتحقق الخادم من الكود: صالح، ولم يستخدمه هذا العميل، والطلب 200.00 على الأقل |
| 2 | كوبون مطبَّق | بانتظار الدفع | POST /orders / يحسب الخادم الإجمالي من أسعاره هو ومن الخصم |
| 3 | بانتظار الدفع | مدفوع | POST /orders/{id}/pay / يؤكد مزوّد الدفع إجمالي الخادم |
| 4 | مدفوع | مؤكَّدالنهاية | POST /orders/{id}/confirm / يُؤكَّد الطلب، ويُخصم المخزون مرة واحدة، ويُرسل بريد واحد |
| 5 | بانتظار الدفع | مؤكَّدالنهاية | إساءة: إرسال confirm والطلب ما زال بانتظار الدفع، بتخطي /pay / المتوقع 4xx ويبقى الطلب بانتظار الدفعمسار بديل |
| 6 | بانتظار الدفع | مدفوع | إساءة: "total": 1.00 في جسم طلب الدفع / المتوقع 4xx أو تجاهل الحقل، ويُحتسب إجمالي الخادم نفسهمسار بديل |
| 7 | كوبون مطبَّق | كوبون مطبَّق | إساءة: تطبيق الكود نفسه مجدداً أو بعد استهلاكه / المتوقع 4xx واحتساب الخصم مرة واحدةمسار بديل |
| 8 | مؤكَّدالنهاية | مؤكَّدالنهاية | إساءة: تكرار confirm أو إرساله مرتين بالتوازي / المتوقع تأكيد واحد وخصم المخزون مرة واحدةمسار بديل |
نموذج مبسّط: الكوبون اختياري في الواقع، وعملية الشراء الحقيقية تضيف حالات مثل العنوان والشحن والاسترداد والإلغاء.
# Test Expected
1 POST /orders/{id}/confirm before /pay 4xx; stays Awaiting payment
2 /pay with "total": 1.00 in the body 4xx or ignored; server total kept
3 /cart/coupon twice with the same code second call 4xx; discount once
4 coupon on a 199.99 cart (minimum 200.00) 4xx; no discount
5 coupon on a 200.00 cart accepted; discount applied
6 coupon at 200.00, then an item removed discount dropped, or /orders 4xx
7 /cart/items with "quantity": -1 4xx; cart unchanged
8 /confirm sent twice in parallel one confirmation; stock reduced once
9 /pay on another customer's order 403 or 404 (access control)
10 coupon already used on an earlier order 4xx; no discountأخطاء شائعة
- اختبار المسار السعيد عبر الواجهة فقط، حيث يخفي المتصفح الخطوات والحقول التي يعدّلها المهاجم مباشرةً.
- الإبلاغ عن حقل
priceالمعدَّل عيباً دون التحقق هل استخدمه الخادم فعلاً؛ فالدليل هو الإجمالي الذي خزّنه الخادم وحصّله، مقروءاً بعد الاستدعاء. - فحص امتداد الملف وإعلان الرفع آمناً، بينما لم يُفحص المحتوى ولا ترويسات الرد قط.
- استخدام برمجيات خبيثة حقيقية أو ملفات استغلال أو بيانات شخصية لزميل ملفاتِ اختبار. فملف نصي باسم خاطئ يكفي لإثبات الفحص.
- تشغيل اختبارات التسابق أو الحدود على بيئة الإنتاج أو بيئة مشتركة، حيث تؤثر الطلبات المزدوجة والكوبونات المستنزَفة في بيانات حقيقية.
- نسيان النصف الثاني من الاختبار: أين يُخزَّن الملف، وكيف يقدّمه التطبيق إلى المتصفح، وهل يمكن أن يُنفَّذ يوماً.
من منظور ضمان الجودة
في الفريق يبدأ اختبار منطق الأعمال في جلسة التنقيح (Refinement): حين تذكر القصة حداً أو خصماً أو ترتيب خطوات أو رفعاً، يسأل المختبِر ماذا يفعل الخادم عند كسر القاعدة ويكتب الجواب معايير قبول. ثم تُشتق الاختبارات كأي اختبارات انتقال حالة أو قيم حدّية وتُحفظ في حزمة الانحدار، لأن إعادة هيكلة عملية الشراء قد تعيد الثقة بحقل من العميل بصمت.
لا تختبر إلا الأنظمة التي تملكها أو لديك إذن كتابي باختبارها، بحسابات اختبار ورموز دفع تجريبية وملفات غير ضارة، وتخبر الفريق قبل إطلاق طلبات متوازية على بيئة مشتركة. ويسجّل التقرير الطلب والتغيير الوحيد الذي أجريته والرد والحالة الناتجة، وتُحدَّد الخطورة بالمال والمدى: فالطلب المجاني أو الملف المرفوع الذي يُنفَّذ خطورته حرجة (Critical)، وتجاوز حدٍّ بمرة واحدة خطورته غالباً ثانوية (Minor).
تمرين عملي
خطّط اختبارات مرفقات تذكرة الدعم
يتيح مكتب مساعدة لعميل مسجّل الدخول إرفاق ملفات بتذكرة عبر POST /tickets/{id}/attachments: PDF وPNG وJPEG فقط، بحد أقصى 10 ميغابايت، وحتى 3 ملفات لكل تذكرة. ويفتح موظفو الدعم المرفقات من صفحة التذكرة، حيث يُقدَّم الملف من /attachments/{uuid}.
أعدّ خطة اختبار من ملفات غير ضارة وطلبات، مع النتيجة المتوقعة لكل منها، تغطي النوع والحجم والاسم وحد العدد وكيفية تقديم الملف المخزَّن إلى المتصفح. لا تحتاج إلى نظام حقيقي: سمّ الملفات التي ستنشئها وما يثبته كل منها.
- اكتب ملفات الاختبار: الصالحة والمتنكرة والزائدة الحجم، وكيف تنشئ كلاً منها دون برمجيات خبيثة.
- اكتب الطلبات التي تكسر قاعدة: الحد، والحالة، وتذكرة عميل آخر.
- اكتب ما ستفحصه لكل منها في الرد، وفي الملف حين يقدّمه التطبيق إلى المتصفح.
إظهار الإجابة النموذجية
الملفات: valid.pdf (ملف PDF حقيقي من صفحة واحدة) وvalid.png وvalid.jpg (صورتان حقيقيتان)، وتُقبل جميعها؛ وملف PDF صالح بحجم 10 ميغابايت بالضبط يُقبل، والملف نفسه أكبر ببايت واحد يُرفض برسالة عن الحجم (اسأل أولاً: هل تعني 10 ميغابايت 10,000,000 بايت أم 10,485,760؟)؛ وملف فارغ؛ وpage.html بعد تسميته page.png (محتوى HTML باسم صورة)؛ وnote.txt بعد تسميته note.pdf.exe؛ وscript.svg (ملف SVG فيه عنصر <script> لا يفعل سوى إظهار تنبيه)؛ وصورة PNG حقيقية مرسلة بنوع text/html؛ وصورة PNG حقيقية باسم ../../qa-traversal-test.png، وهو اسم فريد لا يمكن أن يكتب فوق أي ملف. المتوقع: يُرفض الملف الفارغ والملفات المتنكرة وملف SVG وملف .exe برسالة واضحة ولا يُخزَّن شيء؛ وصورة PNG ذات النوع الخاطئ تُرفض، أو تُخزَّن وتُقدَّم بنوع image/png؛ واسم الاجتياز يُرفض أو يُخزَّن باسم يولّده الخادم، ولا يظهر أي ملف خارج مجلد الرفع.
الطلبات: مرفق رابع على تذكرة فيها ثلاثة (المتوقع 4xx)؛ ورفعان متوازيان إلى تذكرة فيها ملفان حين لا يتبقى إلا مكان واحد (المتوقع قبول واحد بالضبط، ولا تُخزَّن أربعة ملفات أبداً)؛ ورفع إلى معرّف تذكرة عميل آخر (المتوقع 403 أو 404، كما في درس التحكم في الوصول)؛ ورفع إلى تذكرة مغلقة إن كانت القاعدة تجعل التذاكر المغلقة للقراءة فقط (المتوقع 4xx ولا يُخزَّن شيء).
تقديم الملف المخزَّن: افتح /attachments/{uuid} لملف PNG وملف PDF. المتوقع: Content-Type مطابق للنوع المخزَّن، وX-Content-Type-Options: nosniff، وContent-Disposition: attachment لملف PDF، والاسم الأصلي في نص صفحة التذكرة فقط ومرمَّزاً، وعنوان لا يمكن تخمينه من تذكرة أخرى، والرد 403 أو 404 حين تطلبه جلسة عميل آخر.
الأدلة التي تسجَّل: كل طلب، وحالة الرد وجسمه، وقائمة مرفقات التذكرة بعد الاستدعاء، وترويسات الرد الذي يُقدَّم به الملف.
أسئلة شائعة
هل اختبار منطق الأعمال نشاط أمني أم وظيفي؟
كلاهما. تُشتق الاختبارات كالاختبارات الوظيفية من القواعد والحالات، لكن مرجع الاختبار هو ما يجب ألا يستطيع مستخدم غير أمين فعله. وتغطيه OWASP تحت التصميم غير الآمن، وللواجهات البرمجية تحت الوصول غير المقيَّد إلى تدفقات الأعمال الحساسة.
هل أستطيع اختبار حالات التسابق يدوياً؟
جزئياً. تبويبان يرسلان في اللحظة نفسها، أو نقرة مزدوجة على زر، يكشفان عيب الإرسال المزدوج الشائع. أما النتائج الموثوقة فتحتاج إلى طلبات تُطلق بالتوازي من سكربت أو وكيل، وبيئة اختبار لا تضرّ فيها الطلبات المزدوجة.
ما ملفات الاختبار الآمنة للرفع؟
نص عادي، وصورة أو ملف PDF حقيقي صغير، والملفات نفسها بعد إعادة تسميتها أو إرسالها بنوع Content-Type خاطئ. وملف SVG فيه <script> غير ضار يُظهر تنبيهاً يثبت خطر تقديم الملف المخزَّن إلى المتصفح. ولا تستخدم أبداً برمجيات خبيثة حقيقية أو مستندات شخصية لشخص آخر.
هل يجعل فحص الامتداد الرفع آمناً؟
لا. فالامتداد اسم يختاره العميل. والمعالجة الآمنة تفحص المحتوى، وتفرض الحجم، وتولّد اسم الملف بنفسها، وتخزّن الملف حيث لا ينفّذه شيء، وتقدّمه بـContent-Type ثابت وnosniff.
المراجع
- OWASP Web Security Testing Guide (WSTG)المنهجية المرجعية لاختبار تطبيقات الويب وواجهات API بحثاً عن نقاط الضعف الأمنية، اختباراً تلو الآخر.
- OWASP Cheat Sheet Series: File Uploadالتحقق من الملفات المرفوعة: فحص الامتداد ونوع المحتوى، وحدود الحجم، والتخزين خارج جذر الويب، والفحص.
- OWASP API Security Top 10 (2023)أكثر عشر نقاط ضعف شيوعاً في واجهات API: خلل التفويض على مستوى الكائن، وخلل التوثيق، والاستهلاك غير المقيد للموارد، وغيرها.
- OWASP Top 10:2025أخطر عشرة مخاطر أمنية لتطبيقات الويب، مع كيفية اختبار كل منها والوقاية منه.
تدرّب بأدوات متقن
عرّف الحالات والأحداث، ثم ولّد جدول الانتقالات واختبارات الانتقالات الصالحة/غير الصالحة وتغطية 0-switch/1-switch.
ولّد حالات حدّية (edge cases) للسلاسل النصية والأرقام والتواريخ والملفات والقوائم والتزامن انطلاقاً من تعريف الحقول.
أنشئ طلبات HTTP وأرسلها (الطريقة وعنوان URL والترويسات والمصادقة والجسم) وافحص الاستجابة — بديل خفيف لـ Postman داخل متصفحك.
اعثر على نوع MIME لامتداد ملف، أو على الامتدادات المقابلة لنوع MIME.
التحقق من صحة ترويسات Content-Type والتأكد من أن متن الاستجابة يطابق فعلاً نوع الوسائط المعلن.
ولّد مدخلات مليئة بالأحرف الخاصة: علامات الاقتباس، والأحرف الوصفية لـ SQL/HTML/JSON، وأحرف التحكم، والرموز التعبيرية، وحيل المسارات.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.