From 6ba0a7d239d30b4cba752fcf0c4931a71bbdc074 Mon Sep 17 00:00:00 2001 From: Andra Blaj Date: Thu, 12 Dec 2024 14:29:57 +0200 Subject: [PATCH 1/2] feat(#9683): add Arabic translations (#9696) --- .../translations/messages-ar.properties | 1352 +++++++++++++++++ api/src/translations.js | 3 +- webapp/src/js/bootstrapper/translator.js | 18 + webapp/src/js/enketo/main.js | 1 + webapp/src/ts/main.ts | 1 + 5 files changed, 1374 insertions(+), 1 deletion(-) create mode 100644 api/resources/translations/messages-ar.properties diff --git a/api/resources/translations/messages-ar.properties b/api/resources/translations/messages-ar.properties new file mode 100644 index 00000000000..6e452c04603 --- /dev/null +++ b/api/resources/translations/messages-ar.properties @@ -0,0 +1,1352 @@ +Accept\ plain-text\ messages = قبول الرسائل النصية العادية +Accept\ plain-text\ messages\ help = حدّد مربع الاختيار إذا كنت تريد قبول الرسائل النصية SMS بالإضافة إلى التقارير. إذا لم يتم تحديد هذا المربّع، سيتم إرسال رسالة خطأ إلى أي شخص يرسل شيئاً غير التقارير. +Activity = النشاط +Add\ Message = إضافة رسالة +Add\ User = إضافة مستخدِم +Add\ Validation = إضافة تحقق +Add\ new\ language = إضافة لغة جديدة +Add\ person = إضافة شخص +Add\ place = إضافة مكان +Add\ recipient = إضافة مستلِم +Advanced = خيارات متقدّمة +Advanced\ settings\ intro = للتحقّق من وجود إصدارات أحدث من CHT وتثبيتها أو لضبط الإعدادات التقنية، يُرجى استخدام +Advanced\ settings\ outro = هذا مخصّص للمستخدمين ذوي خلفية تقنية عالية. +All\ contact\ types = كل أنواع جهات الاتصال +All\ facilities = كل الأماكن +All\ form\ types = كل أنواع النماذج +Analytics = التحليلات +Antenatal\ Care = الرعاية قبل الولادة +Any\ date = أي تاريخ +Any\ status = أي حالة +api.startup.checks = عمليات التحقق من التثبيت +api.startup.config = تطبيق التكوين +api.startup.forms = تثبيت النماذج +api.startup.install = جارٍ التثبيت +api.startup.index = جارٍ فهرسة البيانات +api.startup.migrate = جارٍ ترحيل البيانات +api.startup.title = جارٍ بدء {{ branding.name }} +Application\ Text = نص التطبيق +Apply = تطبيق +Appointment\ Date = تاريخ الموعد +At\ home\ with\ SBA = في المنزل بمساعدة قابلة ماهرة +At\ home\ without\ SBA = في المنزل بدون مساعدة قابلة ماهرة +Audit\ Logs = سجلات التدقيق +Automated\ Reply = رد تلقائي +Available\ Fields = الحقول المتاحة +Back = رجوع +Backup = نسخة احتياطية +Basic = أساسي +Bug\ description = وصف الخطأ +Cancel = إلغاء +Choose\ File = اختيار ملف +Choose\ file = اختيار ملف +Clear\ all\ filters = إعادة ضبط الفلاتر +Clinic = المنطقة +Clinic\ Contact\ Name = الاسم +Clinic\ Contact\ Phone = رقم الهاتف +Clinics = المناطق +Community\ health\ worker = عامل صحي مجتمعي +Configuration = تكوين +Confirm = تأكيد +Confirm\ Password = تأكيد كلمة المرور +Confirm\ delete = هل تريد بالتأكيد حذف {{name}}؟ لا يمكن التراجع عن هذه العملية. +Contact = جهة الاتصال +Contact\ type = نوع جهة الاتصال +Contacts = الأشخاص +Contacts\ file\ help = حدّد ملف json. الذي يحتوي على جهات الاتصال. +Content = المحتوى +Continue = متابعة +Current\ Password = كلمة المرور الحالية +Dashboard\ settings\ page = صفحة إعدادات لوحة المعلومات +Date\ display\ format = تنسيق عرض التاريخ +Datetime\ display\ format = تنسيق عرض التاريخ والوقت +Default = الإعدادات الافتراضية +Default\ Application\ Language = لغة التطبيق الافتراضية +Default\ country\ code = رمز البلد الافتراضي +Default\ country\ code\ help = سيتم تخصيص هذا الرمز لجميع أرقام الهواتف التي تم إدخالها في النظام بدون رمز البلد. +Delete = حذف +Deleting = جارٍ الحذف... +Disable = تعطيل +Discard\ changes\ to\ current\ language = هل تريد الاستمرار وتجاهل التغييرات التي أجريت على اللغة الحالية؟ +Display = عرض +District = المقاطعة +District\ Contact\ Name = اسم جهة الاتصال +District\ Contact\ Phone = رقم الهاتف +District\ Hospital = المقاطعة +District\ Name = اسم المقاطعة +Districts = المقاطعات +Download = تنزيل +EDD = تاريخ الولادة المتوقع +Edit = تعديل +Edit\ User = تعديل بيانات المستخدِم +Edit\ User\ Profile = تعديل ملف تعريف المستخدِم +Edit\ language = تعديل اللغة +Edit\ person = تعديل بيانات الشخص +Edit\ place = تعديل المكان +Edit\ translation = تعديل الترجمة +Email\ Address = عنوان البريد الإلكتروني +Enable = تمكين +Enter\ message = أدخل الرسالة +Error\ deleting\ document = حدث خطأ في حذف المستند +Error\ fetching\ contacts = حدث خطأ في إحضار الأشخاص +Error\ fetching\ messages = حدث خطأ في إحضار الرسائل +Error\ fetching\ reports = حدث خطأ في إحضار التقارير +Error\ fetching\ tasks = حدث خطأ في إحضار المهام +Error\ parsing\ file = حدث خطأ في تحليل الملف +Error\ parsing\ properties\ file = حدث خطأ في تحليل ملف الخصائص +Error\ retrieving\ settings = خطأ في استرداد الإعدادات +Error\ saving\ feedback = خطأ في حفظ الملاحظات +Error\ saving\ settings = حدث خطأ أثناء حفظ الإعدادات. يُرجى المحاولة مرة أخرى. +Error\ sending\ message = خطأ في إرسال الرسالة +Error\ updating\ contact = خطأ في التحديث +Error\ updating\ facility = خطأ في تحديث جهة الاتصال +Error\ updating\ group = خطأ في تحديث المجموعة +Error\ updating\ user = خطأ في تحديث بيانات المستخدِم +Errors = الأخطاء +Everyone\ at = {{facility}} - جميع جهة الاتصال وعددها {{count}} +Exactly = بالظبط +Export = تصدير +External\ ID = معرّف خارجي +Extra\ search\ words = كلمات بحث إضافية +Facility = المكان +Facility\ nurse = ممرضة المرفق +Failed\ validation = تعذر التحقّق +File\ not\ found = لم يتم العثور على الملف +For\ example = على سبيل المثال\: +Forms = نماذج التطبيق +From = مِن +Full\ Name = الاسم الكامل +Gateway\ number = رقم هاتف البوابة +Gateway\ number\ help = هذا هو الرقم الهاتفي الذي يجب على المراسلين المتنقلين إرسال تقاريرهم إليه، وهو أيضاً الرقم الذي سيتلقون منه الرسائل. +Generated\ report\ field = حقل التقرير المُنشأ +Health\ Center = مركز الصحة +Health\ Center\ Contact\ Name = اسم جهة الاتصال +Health\ Center\ Contact\ Phone = رقم الهاتف +Health\ Center\ Name = اسم مركز الصحة +Health\ Centers = مراكز الصحة +Help = المساعدة +Import = استيراد +Import\ translations = استيراد الترجمات +Incoming\ Reports = التقارير الواردة +Installed\ Forms = نماذج التطبيق المثبّتة +Institutional\ Delivery = التسليم المؤسسي +Invalid = غير صالح +Invalid\ contact\ numbers = هؤلاء المستلمون ليس لديهم رقم اتصال صالح/: {{recipients}} +LMP\ date = تاريخ الدورة الشهرية الأخيرة +Language = اللغة +Language\ For\ Outgoing\ Messages = لغة الرسائل الصادرة +Language\ code = رمز اللغة +Language\ code\ help = رمز اللغة المكوّن من رقمَين أو ثلاثة أرقام للغة التي تلي +Language\ name\ help = اسم العرض للغة. +Language\ to\ edit = اللغة التي يجب تعديلها +Languages = اللغات +Last\ Appointment = الموعد الأخير +Log\ Out = تسجيل الخروج +Message\ All = إرسال رسالة إلى الكل +Message\ Body = الرسالة +Message\ UUID = رسالة المعرّف الفريد العالمي +Messages = الرسائل +Messaging\ window = نافذة المراسلة +Missing\ translations = {{missing}}: ترجمات مفقودة +Mute = كتم الصوت +Name = الاسم +New\ person = شخص جديد +Next = التالي +No\ contact\ selected = لم يتم تحديد أي شخص +No\ contacts\ found = لم يتم العثور على أي أشخاص +No\ district = لا توجد مقاطعة +No\ forms\ found = لم يتم العثور على أي نماذج لهذا الجدول الزمني. +No\ message\ selected = لم يتم تحديد أي رسالة +No\ messages\ found = لم يتم العثور على أي رسائل. +No\ more\ contacts = لم يتم العثور على المزيد من الأشخاص +No\ more\ messages = لم يتم العثور على المزيد من الرسائل +No\ more\ reports = لم يتم العثور على المزيد من التقارير +No\ records\ found = لم يتم العثور على أي سجلات +No\ registrations\ found = لم يتم العثور على أي تسجيلات لهذا الجدول الزمني. يشير هذا إلى وجود مشكلة في التكوين. +No\ report\ selected = لم يتم تحديد أي تقرير +No\ reports\ found = لم يتم العثور على أي تقارير +No\ schedules\ found = لم يتم العثور على أي جداول زمنية +No\ submission = لا تشارك بيانات التأثير +No\ task\ selected = لم يتم تحديد أي مهمة +No\ tasks\ found = لم يتم العثور على أي مهام +Notes = ملاحظات +Notifications = الإشعارات +Number\ in\ month = {{count}} في {{month}} +Number\ of\ contact\ types = عدد أنواع جهات الاتصال {{number}} +Number\ of\ facilities = عدد الأماكن {{number}} +Number\ of\ form\ types = عدد أنواع النماذج {{number}} +Number\ of\ visits = زيارات تزيد عن {{number}}+ +On\ the\ day = في {{day}} +Outgoing\ Message = رسالة صادرة +Overview = نظرة عامة +Overwrite\ Existing\ Records = استبدال السجلات الموجودة +Password = كلمة المرور +Passwords\ must\ match = يجب أن تتطابق كلمات المرور. +Patient\ History = تاريخ المريضـ(ـة) +Patient\ ID = معرّف المريضـ(ـة) +Patient\ Name = اسم المريضـ(ـة) +Patient\ Report = تقرير المريضـ(ـة) +People = الأشخاص +Person = الشخص +Phone = الهاتف +Phone\ Number = الهاتف +Phone\ number\ conversion = تحويل رمز البلد +Phone\ number\ example = سيُضاف رمز البلد الافتراضي المكوّن مسبقاً إذا لزم الأمر، على سبيل المثال/: 0275551234 أو ‏‎+64275551234 +Phone\ number\ not\ valid = ليس رقم هاتف صالحاً. +Places = الأماكن +Please\ select\ a\ facility = يُرجى تحديد شخص لإعادة تعيينه كمرسِل لهذا التقرير. +Pregnant\ patient = المريضـ(ـة) الحامل +Previous = السابق +Primary\ Contact\ For = جهة اتصال رئيسية لـ +Primary\ contact = جهة اتصال رئيسية +Primary\ location = الموقع الأساسي +Processed\ number\ of\ total\ records = تمت معالجة {{number}} من السجلات وإجمالي عددها {{total}}... +RC\ Code = الرمز +Reading\ file = جارٍ قراءة الملف... +Registration\ example = على سبيل المثال، لتسجيل "{{name}}" سترسل\: +Registration\ format = قم بالتسجيل في سير عمل هذه الرسالة عن طريق إرسال رسالة SMS بالتنسيق التالي\: +Registrations = التسجيلات +Reload = إعادة تحميل +Replace\ country\ code\ for = استبدال رمز البلد لـ +Reply = رد +Reply\ to\ name = الرد على {{contact}} +Report\ Bug = الإبلاغ عن خطأ +Report\ format = تم تسجيله عن طريق إرسال رسالة SMS بالتنسيق التالي\: +Reporting\ Rates = معدلات إعداد التقارير +Reports = التقارير +Restore = استعادة +Save\ failed = تعذّر الحفظ +Saved = تم الحفظ +Schedule = الجدول الزمني +Schedule\ name = الجدول الزمني\: {{name}} +Schedules = الجداول الزمنية +Search = بحث +Select\ a\ language = اختيار لغة +Select\ a\ type = اختيار نوع +Send = إرسال +Send\ Message = إرسال رسالة +Send\ scheduled\ messages\ between = إرسال رسائل مُجدوَلة بين +Sending = جارٍ الإرسال... +Sent\ By = تم الإرسال مِن +Server\ Logs = سجلات الخادم +Set\ as\ default\ application\ language = ضبط كلغة تطبيق افتراضية +Set\ as\ language\ for\ outgoing\ messages = ضبط كلغة للرسائل الصادرة +Settings = الإعدادات +Showing\ number\ of\ total = إظهار {{number}} من {{total}}. +Skipped\ number\ of\ records = تم تخطي عدد من السجلات يبلغ {{number}}. +Start = ابدأ +Start\ messages\ based\ on = ابدأ الرسائل بناءً على +Start\ new\ conversation = ابدأ محادثة جديدة +Submit = إرسال +Submit\ Report = إرسال تقرير +Submit\ via\ either = جرّب أونلاين أولاً، ثم رسالة SMS +Submit\ via\ sms = رسائل SMS فقط +Submit\ via\ web = أونلاين فقط +Submitting = جارٍ الإرسال... +Task\ Message = رسالة المهمة +Tasks = المهام +The\ first\ time\ must\ be\ earlier\ than\ the\ second\ time = يجب أن تكون المرة الأولى قبل المرة الثانية. +The\ group\ must\ be\ an\ integer = يجب أن تكون المجموعة عدداً صحيحاً +The\ offset\ unit\ must\ be\ an\ integer = يجب أن تكون وحدة الإزاحة عدداً صحيحاً +The\ unit\ must\ be\ an\ integer = يجب أن تكون الوحدة عدداً صحيحاً +This\ message\ is\ part\ of\ group = هذه الرسالة جزء من المجموعة +To\ Phone = إلى الهاتف +Translation\ file = ملف الترجمة +Translation\ file\ help = حدِّد ملف properties. الذي تريد استيراده لاستبدال الترجمات الخاصة بهذه اللغة. أسهل طريقة لإنشاء ملف بالتنسيق الصحيح هي تصدير الترجمات لهذه اللغة، وإجراء أي تعديلات، ثم الاستيراد. +Translations = الترجمات +Unread\ below = غير مقروءة أدناه +Unsaved\ changes = تغييرات غير محفوظة +Unverified = لم يتم التحقق منه +Unverify = إلغاء التحقّق +Update = تحديث +Update\ Facility = تحرير بيانات المُرسِل +Upload\ Forms = تحميل نماذج التطبيق +Upload\ failed = تعذّر التحميل +Upload\ succeeded = نجح التحميل +User\ Feedback = ملاحظات المستخدِم +User\ Name = اسم المستخدِم +User\ Type = نوع المستخدِم +Users = المستخدمون +Valid = صالح +Validation\ message = إذا نجحت جميع عمليات التحقق، فسيتم إرسال هذه الرسالة إلى المُرسِل\: +Validations = عمليات التحقق +Validity = التحقّق +Verification = التحقق +Verified = تم التحقق منه +Verify = يتحقق +Village\ Name = المدينة +Visits = الزيارات +Weeks\ Pregnant = أسابيع الحمل +_id = تسجيل المعرّف الفريد العالمي +about = حول +about.cht = مشغّل بواسطة +action.clinic.add = إضافة منطقة +action.district_hospital.add = مقاطعة جديدة +action.health_center.add = مركز صحة جديد +action.person.add = شخص جديد +action.report.add = إجراء جديد +admin.app.name = إدارة التطبيق +admin.display = عرض +admin.display.datetime = التاريخ والوقت +admin.message.queue = الرسائل الصادرة +admin.message.queue.created = تم الإنشاء +admin.message.queue.due = الموعد النهائي +admin.message.queue.error = حدث خطأ في إحضار الرسائل +admin.message.queue.message = الرسالة +admin.message.queue.recipient = المستلِم +admin.message.queue.scheduled = مُجدوَل زمنياً +admin.message.queue.status = الحالة +admin.message.queue.tab.due = الموعد النهائي +admin.message.queue.tab.muted.future = لن يتم الإرسال +admin.message.queue.tab.muted.past = لم يتم الإرسال +admin.message.queue.tab.scheduled = مُجدوَل زمنياً +admin.message.queue.updated = آخر تحديث +admin.message.queue.view.report = عرض التقرير +admin.pagination.detail = إظهار {{first}} - {{last}} من إجمالي العناصر البالغ عددها {{total}} +admin.pagination.first = الأول +admin.pagination.last = الأخير +admin.pagination.next = التالي +admin.pagination.prev = السابق +admin.targets.description = سيُظهر هدف 1- مستهدفاً بدون هدف. +after\ the = بعد +analytics.anc.active-pregnancies = حالات الحمل النشطة +analytics.anc.delivery-locations = مواقع الولادة المبلغ عنها +analytics.anc.high-risk = حالات الحمل عالية الخطورة +analytics.anc.missed-appointments = المواعيد الفائتة مؤخراً +analytics.anc.missing-reports = تقارير الولادة الفائتة +analytics.anc.monthly-births = الولادات الشهرية +analytics.anc.monthly-registrations = حالات الحمل الشهرية المسجلة +analytics.anc.total-births = إجمالي عدد الولادات +analytics.anc.upcoming-appointments = المواعيد القادمة +analytics.anc.upcoming-edds = النساء اللاتي لديهن مواعيد مقبلة لتاريخ الولادة المتوقع +analytics.anc.visits-completed = الزيارات التي تم إكمالها أثناء الحمل +analytics.anc.visits-completed.description = حالات الحمل المكتملة التي كانت... +analytics.anc.visits-during = الزيارات المكتملة حتى الآن +analytics.anc.visits-during.description = حالات الحمل النشطة التي كانت... +analytics.connection.error = لم نتمكن من استرداد البيانات في هذا الوقت. +analytics.reporting.change_time_unit = تغيير وحدة الوقت +analytics.reporting.clinics_size = وحدة الإبلاغ +analytics.reporting.date_range = نطاق التاريخ +analytics.reporting.district = اختيار مقاطعة +analytics.reporting.form = اختيار نموذج +analytics.reporting.health_centers_size = مؤسسة صغير أو متوسطة الحجم +analytics.reporting.incomplete = تقارير غير صالحة +analytics.reporting.last_time_unit = آخِر {{quantity}} {{time_unit}} +analytics.reporting.not_submitted = التقارير مفقودة +analytics.reporting.reporting_rate = معدل الإبلاغ +analytics.reporting.reports = التقارير +analytics.target.add = إضافة مستهدف +analytics.target.aggregates = المجاميع المستهدفة +analytics.target.aggregates.disabled = المجاميع المستهدفة معطلة +analytics.target.aggregates.error = خطأ في إحضار المجاميع المستهدفة +analytics.target.aggregates.error.no.contact = خطأ في تحميل المجاميع المستهدفة. ليس لدى المستخدم مكان رئيسي، أو ليست لديه إمكانية الوصول إلى المكان الرئيسي المرتبط. تحدّث إلى المسؤول لديك لتصحيح هذا الأمر. +analytics.target.aggregates.error.not.found = خطأ في تحميل المجموع المستهدف\: لم يتم العثور على المستهدف. +analytics.target.aggregates.no.data = لا توجد بيانات +analytics.target.aggregates.no.target.selected = لم يتم تحديد المستهدف. +analytics.target.aggregates.no.targets = لم يتم العثور على المجاميع المستهدفة +analytics.target.aggregates.ratio = {{pass}} من {{total}} +analytics.target.aggregates.reported = تم الإبلاغ +analytics.target.aggregates.select.error = خطأ في تحميل المجموع المستهدف. +analytics.target.aggregates.supervisees.meeting.goal = هدف اجتماع العاملين الصحيين المجتمعيين +analytics.target.aggregates.total = الإجمالي +analytics.target.aggregates.reporting_period = فترة الإبلاغ +analytics.target.goal = الهدف +analytics.target.goal.help = إذا كنت لا تريد عرض هدف، فأدخِل قيمة "1-". +analytics.target.icon = أيقونة +analytics.target.icon.help = معرّف الأيقونة المُكوّنة. +analytics.target.id = معرّف فريد +analytics.target.monthly_goal = الهدف +analytics.target.name = الاسم +analytics.target.name.help = إذا أضفت ترجمات لاسم المستهدف هنا، فستظل بحاجة إلى إضافتها في صفحة اللغات أيضاً. +analytics.target.type = النوع +analytics.target.type.count = العدد +analytics.target.type.percent = النسبة المئوية +analytics.targets = المستهدفات +analytics.targets.registrations = العائلات المسجلة +analytics.targets.unique.id = يجب أن يكون معرّف المستهدف فريداً +analytics.unconfigured = لم يتم تكوين وحدات تحليلية. +and = و +and\ should\ be\ sent\ at = ويجب إرسالها في +android_app.data_usage.app.title = هذا التطبيق +android_app.data_usage.description = ملاحظة: هذه القيم موجودة منذ تشغيل الجهاز. +android_app.data_usage.rx = تم الاستلام +android_app.data_usage.system.title = على مستوى النظام +android_app.data_usage.title = استخدام بيانات التطبيق +android_app.data_usage.tx = تم النقل +android_app.os_android_version.title = نظام تشغيل أندرويد +android_app.package_name.title = اسم حزمة تطبيق أندرويد +android_app.version.title = إصدار تطبيق أندرويد +android_app.version_code.title = رمز نسخة تطبيق أندرويد +app.name = التطبيق +app.version.unknown = غير معروف - الاتصال بالإنترنت مطلوب. +associated.contact = جهة الاتصال المرتبطة +associated.contact.help = عندما ينشئ هذا المستخدم التقارير، سيتم تعيينها إلى جهة الاتصال هذه +autoreply = الرد التلقائي +birth_date = تاريخ الميلاد +branding = العلامة التجارية +branding.favicon.field = أيقونة صغيرة +branding.favicon.field.help = تظهر هذه الأيقونة في علامة تبويب المتصفح. يجب أن تكون بحجم 32 × 32 بكسل، ملف ico. +branding.logo.field = الشعار +branding.logo.field.help = يظهر في صفحة تسجيل الدخول والأجهزة ذات الشاشات الكبيرة. +branding.title.field = العنوان +branding.title.field.help = سيتم عرض هذا النص في علامة تبويب المتصفح وكعنوان لتطبيق الويب التقدمي. +branding.icon.field = أيقونة كبيرة +branding.icon.field.help = ستظهر لعمليات تثبيت تطبيق الويب التقدمي. يجب أن تكون بحجم 144 بكسل مربع على الأقل. +browser.compatibility.title = الاتصال بالمشرف +browser.compatibility.description = يُرجى الاتصال بالمشرف لديك وإخباره بأن متصفّحك يحتاج إلى تحديث. +browser.compatibility.confirm = موافق +bulkdelete.confirm.action = حذف +bulkdelete.confirm.title = هل تريد حذف السجل؟ +bulkdelete.confirm.title.plural = هل تريد حذف السجلات المحدّدة؟ +call = اتصال +case_id = معرّف الحالة +child_birth_date = تاريخ ميلاد الطفل +child_birth_outcome = حصيلة ميلاد الطفل +child_birth_weight = وزن الطفل عند الولادة +cleared = طابع زمني تم مسحه +clientDdoc.version = مراجعة تطبيق الويب الخاص بالعميلـ(ـة) +clinic.field.children = الأشخاص +clinic.field.contact = جهة اتصال رئيسية +clinic.field.external_id = معرّف خارجي +clinic.field.location = الموقع +clinic.field.name = الاسم +clinic.field.notes = ملاحظات +clinic.field.parent = مركز الصحة +close = إغلاق +configuration.authorization = الأدوار والأذونات +configuration.date.format.help = تنسيق Moment.js +configuration.enable.token.login = تمكين تسجيل الدخول عبر رابط رسائل SMS +configuration.enable.token.login.disable = تعطيل تسجيل الدخول عبر رسائل SMS. +configuration.enable.token.login.enabled.active = تم تمكين تسجيل الدخول عبر رسائل SMS لهذا المستخدم. تنتهي صلاحية الرابط في {{date}}. +configuration.enable.token.login.enabled.expired = تم تمكين تسجيل الدخول عبر رسائل SMS لهذا المستخدم. انتهت صلاحية الرابط في {{date}}. +configuration.enable.token.login.enabled.inactive = تم تمكين تسجيل الدخول عبر رسائل SMS لهذا المستخدم. تم الوصول إلى الرابط في {{date}}. +configuration.enable.token.login.help = عند الحفظ، سيتلقى المستخدِم تعليمات تسجيل الدخول ورابط تسجيل الدخول عبر رسائل SMS. يتطلب من المستخدِم أن يكون لديه رقم هاتف صالح. +configuration.enable.token.login.no.modify = لا تقم بإجراء أي تغييرات على تسجيل الدخول عبر رسائل SMS. +configuration.enable.token.login.phone = مطلوب رقم هاتف صالح لتسجيل الدخول عبر رسائل SMS. +configuration.enable.token.login.refresh = إعادة إنشاء وإعادة إرسال رسالة SMS لتسجيل الدخول. +configuration.enable.token.login.refresh.help = سيؤدي تعطيل أو إعادة إنشاء رسالة تسجيل الدخول عبر SMS إلى تغيير كلمة مرور المستخدِم، ما سيؤدي إلى تسجيل خروج المستخدِم. +configuration.messagetest = اختبار الرسالة +configuration.permission = الإذن +configuration.permissions = الأذونات +configuration.role = الدور +configuration.role.offline = غير متصل بالإنترنت +configuration.role.offline.warning = يستطيع المستخدمون الذين لديهم إمكانية الوصول بدون اتصال بالإنترنت عرض المستندات وتحديثها بدون الحاجة إلى اتصال بالإنترنت، ويمكنهم فقط عرض جزء من البيانات. يمكن لمستخدمي الإنترنت الوصول إلى جميع المستندات، ولكن يجب أن يكون لديهم اتصال بالإنترنت في جميع الأوقات. +configuration.roles = الأدوار +configuration.roles.add = إضافة دور جديد +configuration.sms = رسالة SMS +configuration.sms.forms = نماذج رسائل SMS +configuration.sms.forms.title = يجب اختيار ملف XML وملف بيانات التعريف قبل النقر على زر التحميل. لا يجوز تحميل أكثر من ملف نموذج تطبيق واحد في كل مرة، وسيتم استبدال أي نماذج موجودة. +configuration.sms.settings = الإعدادات الأساسية +configuration.sms.test.description = استخدم هذه الصفحة لإرسال رسالة اختبار إلى تطبيق الإنتاج بدون الحاجة إلى المرور عبر بوابة رسائل SMS. تأكد من استخدام رقم هاتف مسجل في ملف تعريف أحد العاملين الصحيين المجتمعيين لمحاكاة تقرير صادر عنه حول مريضـ(ـة) معيّنـ(ـة). +configuration.sms.test.from.number = من رقم الهاتف +configuration.sms.test.message.description = الحد الأقصى 144 حرفاً +configuration.sms.test.number.validation.description = يُرجى إدخال رقم هاتف صالح بدون شرطات أو علامات ترقيم. +configuration.sms.test.title = رسالة الاختبار +configuration.user.place.contact = يجب أن تكون جهة الاتصال المرتبطة تابعة للمكان. +configuration.user.replication.limit.exceeded = تحذير\! سيكرر هذا المستخدم {{total_docs}} من المستندات، ما يتجاوز الحد الموصى به. عدِّل "الدور" أو "المكان" لإجراء التغييرات اللازمة، ثم اضغط على "إرسال" للمتابعة. إذا كان هناك العديد من المستندات للمستخدم، يُنصح بتعديل قواعد حذف المستندات. +confirm.delete = حذف هذا السجل سيؤدي إلى إزالته نهائياً من تطبيقك. +confirm.delete.plural = سيتم حذف {{number}} سجلاً بشكل دائم. +confirm.delete.progress = الحذف جارٍ +confirm.delete.totals = تم حذف {{totalDeleted}}/{{totalSelected}}... +confirm.delete.user = هل تريد بالتأكيد حذف هذا المستخدم؟ لا يمكن التراجع عن هذه العملية. +confirm.destructive.navigation.forms = هذا النموذج غير مكتمل. ستفقد بياناتك إذا غادرت الآن. يكتمل النموذج فقط عندما تضغط على "إرسال". +confirm.destructive.navigation.submit = خروج +confirm.destructive.navigation.title = هل ترغب في الخروج من النموذج؟ +confirm.logout = ستحتاج إلى اتصال بالإنترنت لتسجيل الدخول مرة أخرى. +password.updated = تم تحديث كلمة مرورك بنجاح. +confirm.verification = سيتم التحقق من هذا التقرير كـ ?correct?. لا يمكن تغيير ذلك لاحقاً. +confirm.verification.submit = تحقق من الصحة +confirm.verification.title = تحقق من التقرير +contact.age = العمر +contact.created = تم إنشاء جهة الاتصال +contact.deceased.date.prefix = توفّي +contact.deceased.title = المتوفّون +contact.deceased.view = عرض عدد المتوفّين ({{count}}) +contact.history = سجلّ جهة الاتصال +contact.last.visited.date = تمت الزيارة في {{date}} +contact.last.visited.unknown = آخر زيارة غير معروفة +contact.muted = مكتوم +contact.muted.modal.text = جهة الاتصال هذه مكتومة حالياً. يمكنك المتابعة، ولكن ستظل جهة الاتصال مكتومة، ولن يتم إنشاء أي مهام لهذه الجهة حتى تقوم بإلغاء كتمها. +contact.muted.modal.title = جهة الاتصال المكتومة +contact.name = اسم جهة الاتصال +contact.new_place.button = مكان جديد +contact.no.children = لا يوجد أشخاص في هذا المكان. +contact.parent = ينتمي إلى +contact.parent.external_id = معرّف المنطقة الخارجية +contact.parent.name = اسم المنطقة +contact.parent.parent.contact.name = اسم جهة الاتصال بمركز الصحة +contact.parent.parent.external_id = المعرّف الخارجي لمركز الصحة +contact.parent.parent.name = اسم مركز الصحة +contact.parent.parent.parent.external_id = المعرّف الخارجي للمقاطعة +contact.parent.parent.parent.name = اسم المقاطعة +contact.place.id = معرّف المكان +contact.primary_contact_name = جهة اتصال رئيسية\: {{name || 'none'}} +contact.profile = ملف تعريف +contact.profile.anc_visit = زيارات الرعاية السابقة للولادة مكتملة +contact.profile.delivery_code.f = الولادة في المرفق +contact.profile.delivery_code.facility_birth = الولادة في المرفق +contact.profile.delivery_code.facility_stillbirth = ولادة وليد ميت في المرفق +contact.profile.delivery_code.home_no_sba_birth = الولادة في المنزل بدون مساعدة قابلة ماهرة +contact.profile.delivery_code.home_no_sba_stillbirth = ولادة وليد ميت بدون مساعدة قابلة ماهرة +contact.profile.delivery_code.home_sba_birth = الولادة في المنزل بمساعدة قابلة ماهرة +contact.profile.delivery_code.home_sba_stillbirth = ولادة وليد ميت في المنزل بمساعدة قابلة ماهرة +contact.profile.delivery_code.miscarriage = الإجهاض +contact.profile.delivery_code.ns = الولادة في المنزل بدون مساعدة قابلة ماهرة +contact.profile.delivery_code.s = الولادة في المنزل بمساعدة قابلة ماهرة +contact.profile.delivery_code.unknown = الولادة +contact.profile.delivery_date = تاريخ الولادة +contact.profile.edd = تاريخ الولادة المتوقع +contact.profile.exit_date = تاريخ الخروج +contact.profile.gender = الجنس +contact.profile.growth_monitoring = مراقبة النمو +contact.profile.height = الطول (سم) +contact.profile.height_at_exit = الطول عند الخروج (سم) +contact.profile.hfa = الدرجة المعيارية للطول بالنسبة للعمر +contact.profile.imam = الإدارة المتكاملة لسوء التغذية الحاد +contact.profile.imam_history = سجلّ الإدارة المتكاملة لسوء التغذية الحاد +contact.profile.imm.bcg = السل +contact.profile.imm.cholera = الكوليرا +contact.profile.imm.doses = {{count}} من {{total}} +contact.profile.imm.dpt = جرعة معززة من لقاح الدفتيريا والسعال الديكي والتيتانوس +contact.profile.imm.fipv = جرعة جزئية من لقاح شلل الأطفال المعطل +contact.profile.imm.flu = الإنفلونزا +contact.profile.imm.generic = عدد التقارير +contact.profile.imm.hep_a = التهاب الكبد A +contact.profile.imm.hep_b = التهاب الكبد B +contact.profile.imm.hpv = فيروس الورم الحليمي البشري +contact.profile.imm.ipv = شلل الأطفال غير النشط +contact.profile.imm.jap_enc = التهاب الدماغ الياباني +contact.profile.imm.meningococcal = المكورات السحائية +contact.profile.imm.mmr = الحصبة، النكاف، الحصبة الألمانية +contact.profile.imm.mmrv = الحصبة، النكاف، الحصبة الألمانية، الجدري المائي +contact.profile.imm.penta = اللقاح الخماسي التكافؤ +contact.profile.imm.pneumococcal = لقاح المكورات الرئوية +contact.profile.imm.polio = شلل الأطفال الفموي +contact.profile.imm.rotavirus = فيروس الروتا +contact.profile.imm.typhoid = التيفود +contact.profile.imm.vitamin_a = فيتامين A +contact.profile.imm.yellow_fever = الحمى الصفراء +contact.profile.immunizations = التطعيمات +contact.profile.last_treatment = آخر برنامج علاجي +contact.profile.muac = محيط العضد +contact.profile.nutrition_program = برنامج علاج التغذية +contact.profile.nutrition_program.otp = البرنامج العلاجي للمرضى الخارجيين +contact.profile.nutrition_program.sc = مركز إعادة التأهيل +contact.profile.nutrition_program.sfp = برنامج التغذية التكميلية +contact.profile.past_pregnancies = حالات الحمل السابقة +contact.profile.pnc_visit = زيارات رعاية ما بعد الولادة مكتملة +contact.profile.postnatal = رعاية ما بعد الولادة +contact.profile.pregnancy = الحمل +contact.profile.risk.high = عالي الخطورة +contact.profile.risk.normal = طبيعي +contact.profile.risk.title = الحالة +contact.profile.sessions = عدد الجلسات +contact.profile.visit = الزيارات المكتملة +contact.profile.visits.of = {{count}} من {{total}} +contact.profile.weight = الوزن (كغم) +contact.profile.weight_at_exit = الوزن عند الخروج (كغم) +contact.profile.wfa = الدرجة المعيارية للوزن بالنسبة للعمر +contact.profile.wfh = الدرجة المعيارية للوزن بالنسبة للطول +contact.select.error = خطأ في اختيار جهة الاتصال. +contact.sex = الجنس +contact.short = عامل صحي مجتمعي +contact.type.clinic = المنطقة +contact.type.clinic.edit = تعديل المنطقة +contact.type.clinic.new = منطقة جديدة +contact.type.clinic.plural = المناطق +contact.type.district_hospital = المقاطعة +contact.type.district_hospital.edit = تعديل المقاطعة +contact.type.district_hospital.new = مقاطعة جديدة +contact.type.district_hospital.plural = المقاطعات +contact.type.health_center = مركز الصحة +contact.type.health_center.edit = تعديل مركز الصحة +contact.type.health_center.new = مركز صحة جديد +contact.type.health_center.plural = مراكز الصحة +contact.type.person = الشخص +contact.type.person.edit = تعديل بيانات الشخص +contact.type.person.new = شخص جديد +contact.type.person.plural = الأشخاص +contact.type.place.edit = تعديل المكان +contact.type.place.new = مكان جديد +contact.type.wrong = النوع خطأ، جهة الاتصال ليست شخصاً. +contact.updated = تم تحديث جهة الاتصال +contacts.imported = تم استيراد جهات الاتصال بنجاح +contacts.results.sort = فرز النتائج +contacts.results.sort.alpha = أبجدياً +contacts.results.sort.date.last.visited = حسب تاريخ آخر زيارة +contacts.results.sort.title = ترتيب هذه القائمة +contacts.visits.count = {{count}} +contacts.visits.visits = {VISITS, plural, one{زيارة} other{زيارات}} +database_closed.description = حدث خطأ غير متوقع ويجب إعادة تحميل التطبيق. +database_closed.title = خطأ غير متوقع +date.from = مِن +date.incorrect.advice = يُرجى تصحيح إعدادات التاريخ والوقت في جهازك. +date.incorrect.confirm = موافق +date.incorrect.description = يبدو أن التاريخ/الوقت مضبوطان بشكل غير صحيح على هذا الجهاز. ضبط التاريخ أو الوقت بشكل غير صحيح يعني أن بياناتك ستُتلف، وقد يعني أنك قد ستفقد المستهدفات. +date.incorrect.expected = التاريخ المتوقع +date.incorrect.reported = التاريخ المُبلّغ عنه +date.incorrect.title = إعداد تاريخ خاطئ +date.time.title = التاريخ والوقت +date.to = إلى +date_filter.error.from_date = لا يمكن أن يكون تاريخ "من" بعد تاريخ "إلى". +date_filter.error.to_date = لا يمكن أن يكون تاريخ "إلى" قبل تاريخ "من". +days = الأيام +daysoverdue = الأيام منذ زيارة المريضـ(ـة) +ddoc.version = مراجعة تطبيق الويب للخادم +debug.db_info.docs = عدد المستندات +debug.db_info.name = الاسم +debug.db_info.seq = تحديث تسلسل +debug.db_info.title = معلومات قاعدة البيانات +debug.mode = تصحيح الأخطاء +debug.mode.description = وضع تصحيح الأخطاء سيعرض معلومات في وحدة تحكم المتصفح من أجل مساعدة المطوِّرين في تشخيص مشاكل التطبيق. بعد تغيير هذا الإعداد، أعِد تحميل التطبيق لتطبيق التغيير. +debug.mode.title = تمكين وضع تصحيح الأخطاء +debug.supported_browser = المتصفح المدعوم +debug.supported_browser.see_requirements = عرض المتطلبات +denied = الطابع الزمني المرفوض +display.language.accordion.title = ستكون اللغات الممكّنة متاحة للمستخدمين كخيار عند تحديد لغتهم الأساسية. ننصح باختيار لغة إلى 3 لغات مدعومة وتعطيل الباقي. من بين اللغات الممكّنة والمدعومة، يجب ألا تكون هناك ترجمات مفقودة. +display.privacy.policies.content.type = نوع المحتوى +display.privacy.policies.current = السياسة الحالية +display.privacy.policies.delete = حذف +display.privacy.policies.digest = الملخص +display.privacy.policies.failure = حدث خطأ أثناء معالجة طلبك +display.privacy.policies.name = اسم الملف المحلي +display.privacy.policies.no.changes = لم يتم الكشف عن أي تغييرات. +display.privacy.policies.preview = معاينة +display.privacy.policies.preview.error = خطأ في تحميل المعاينة +display.privacy.policies.preview.title = عرض سياسة الخصوصية للمعاينة للغة {{language}} +display.privacy.policies.preview.wrong.type = نوع المحتوى خاطئ. يُرجى استخدام نص/HTML. +display.privacy.policies.select = اختر ملف HTML +display.privacy.policies.size = الحجم +display.privacy.policies.submit.success = تم تحديث سياسات الخصوصية بنجاح +display.privacy.policies.title = سياسات الخصوصية +display.privacy.policies.update = تحديث السياسة +display.privacy.policies.upload = اضغط على "إرسال" لتحميل التغييرات. +display.translation.description = يُرجى ملاحظة أن أي تغييرات أو إضافات تجريها هنا على الترجمات لا يتم حفظها أو تعقبها في أي مكان آخر حالياً. +district_hospital.field.children = مراكز الصحة +district_hospital.field.contact = جهة اتصال رئيسية +district_hospital.field.external_id = معرّف خارجي +district_hospital.field.name = الاسم +district_hospital.field.notes = ملاحظات +document.deleted = تم حفظ السجل +document.deleted.plural = عدد السجلات المحذوفة {{number}} +edit.language.code.help.link = معيار ISO +edit.language.description = من الأفضل أن تُدمَج أي لغات جديدة تُضاف في صُلب CHT، بحيث تصبح متاحة للشركاء الآخرين. لا تتردد في التواصل إذا كانت لديك أي استفسارات حول إضافة لغة جديدة. +edit.name = تعديل الاسم +edit.user.settings = إعدادات المستخدم +edit_message_group.modal.title = تعديل الرسائل الصادرة +email.invalid = عنوان البريد الإلكتروني غير صالح. +empty = يبدو أنك أرسلت رسالة فارغة. يُرجى المحاولة مرة أخرى. إذا استمرت المشكلة، يُرجى التواصل مع المشرف. +enketo.constraint.invalid = القيمة غير مسموح بها +enketo.constraint.required = هذا الحقل مطلوب +enketo.drawwidget.annotation = توضيح +enketo.drawwidget.drawing = رسم +enketo.drawwidget.signature = توقيع +enketo.error.max_attachment_size = الملفات المرفوعة تتجاوز الحد الإجمالي للحجم. يُرجى رفع ملفات أصغر. +enketo.form.required = مطلوب +enketo.filepicker.file = ملف +enketo.filepicker.placeholder = انقر هنا لتحميل الملف. (< {{maxSize}}) +enketo.filepicker.notFound = لم يتم العثور على الملف {{existing}} (اتركه بدون تغيير إذا تم تقديمه مسبقاً وتريد الاحتفاظ به). +enketo.filepicker.waitingForPermissions = في انتظار أذونات المستخدم. +enketo.filepicker.resetWarning = هذا الأمر سيؤدي إلى حذف {{item}}. هل تريد بالتأكيد القيام بذلك؟ +enketo.filepicker.toolargeerror = الملف كبير جداً (> {{maxSize}}) +enketo.geopicker.accuracy = الدقة (م) +enketo.geopicker.altitude = الارتفاع (م) +enketo.geopicker.closepolygon = أغلق المضلع +enketo.geopicker.kmlcoords = إحداثيات KML +enketo.geopicker.kmlpaste = الصق إحداثيات KML هنا +enketo.geopicker.latitude = خط العرض (س.ص \B0) +enketo.geopicker.longitude = خط الطول (س.ص \B0) +enketo.geopicker.points = النقاط +enketo.geopicker.searchPlaceholder = البحث عن مكان أو عنوان +enketo.geopicker.removePoint = سيؤدي ذلك إلى حذف النقطة الجغرافية الحالية بالكامل من القائمة ولا يمكن التراجع عن ذلك. هل تريد بالتأكيد القيام بذلك؟ +enketo.selectpicker.noneselected = لا شيء محدد +enketo.selectpicker.numberselected = تم تحديد {{number}} +error.403.description = ليس لديك صلاحيات كافية لعرض هذه الصفحة. تحدث مع المسؤول لزيادة صلاحياتك. +error.403.title = الوصول مرفوض +error.404.description = الصفحة التي كنت تبحث عنها غير موجودة. +error.404.title = غير موجودة +error.503.description = خطأ في التحميل. تحقق من اتصالك بالإنترنت وحاول مرة أخرى أو تحقق مع المسؤول. +error.503.title = خطأ +error.file.size = يجب ألا يزيد حجم الملف عن {{size}} +error.general.description = خطأ. يُرجى إعادة المحاولة. +error.general.title = خطأ +error.loading.form = خطأ في تحميل النموذج. يُرجى المحاولة مرة أخرى أو التحقق مع المسؤول. +error.loading.form.no_contact = خطأ في تحميل النموذج. المستخدم ليس لديه جهة اتصال مرتبطة أو ليس لديه حق الوصول إلى جهة الاتصال المرتبطة. تحدّث إلى المسؤول لديك لتصحيح هذا الأمر. +error.loading.form.no_authorized = خطأ في تحميل النموذج. المستخدم غير مصرح له بالوصول إلى هذا النموذج. تحدّث إلى المسؤول لديك لتصحيح هذا الأمر. +error.report.save = خطأ في حفظ التقرير +error.settings.loading = خطأ في تحميل الإعدادات. يُرجى المحاولة مرة أخرى. +expected_date = التاريخ المتوقع +export.button.download = تنزيل +export.dhis.dataset.description = اختر من تكاملات DHIS2 المتوفرة. +export.dhis.dataset.label = مجموعة بيانات DHIS2 +export.dhis.description = نزّل ملفاً يحتوي على بيانات DHIS2 DataValueSet للتكامل مع DHIS2. +export.dhis.period.description = اختر من الفواصل الزمنية الشهرية المتاحة. +export.dhis.period.label = فلترة حسب الفترة +export.dhis.place.all = كل الأماكن +export.dhis.place.description = فلتر البيانات المصدّرة لتشمل البيانات المرتبطة بجهات الاتصال ضِمن هذا المكان في التسلسل الهرمي. +export.dhis.place.label = فلترة حسب المكان +export.dhis.unconfigured = لم يتم تكوين تكامل DHIS2. +export.feedback.description = نزّل سجل الأخطاء المكتشفة وملاحظات المستخدمين المرسَلة عبر ميزة ?Report bug? بتنسيق CSV. يوضح الجدول أدناه أحدث التقارير المرسَلة. +export.messages.description = نزّل جميع الرسائل التي تم إرسالها أو استلامها على الإطلاق بتنسيق CSV. +export.people.description = نزّل جميع جهات الاتصال المسجلة في النظام بتنسيق JSON. +export.reports.description = نزّل ملخص لجميع التقارير التي تم إرسالها على الإطلاق بتنسيق CSV. +export.tabs.user_devices = أجهزة المستخدم +export.user-devices.description = نزّل ملخصاً لإصدارات البرامج المتعلقة بـ CHT (المتصفح، ومثيل CHT، ونظام التشغيل) التي تعمل على أجهزة المستخدمين. +يتم تصدير الملخص بتنسيق JSON. +extra_fields = حقول إضافية. +android_app_launcher.button.launch = تشغيل التطبيق +android_app_launcher.message.disable = لا تتوفر إمكانية تشغيل تطبيقات Android على هذا الجهاز. +failed\ validation\ response\ message = أرسل رسالة الاستجابة التالية إذا +fast_action_button.phone_call = اتصال +fast_action_button.send_message = إرسال رسالة +fast_action_button.title = جديد +fast_action_button.update_facility = تحديث المرفق +feedback.submitted = تم إرسال الملاحظات +feedback.modal.submit = التقرير +field\ digits\ only = {{field}} يجب أن يحتوي فقط على أرقام. +field\ does\ not\ pass\ this\ validation = الحقل لا يجتاز عملية التحقق هذه +field\ is\ required = {{field}} هو حقل مطلوب. +fields = الحقول +fields.one.required = أحد الحقول التالية مطلوب\: "{{fields}}" +fields.required = الحقول المطلوبة المفقودة\: "{{fields}}" +form = النموذج +form_invalid = لم يتم إكمال النموذج '{{form}}' بشكل صحيح. يرجى إكماله وإعادة إرساله. إذا استمرت هذه المشكلة، اتصل بالمشرف لديك. +form_invalid_custom = لم يتم إكمال النموذج '{{form}}' بشكل صحيح. يرجى إكماله وإعادة إرساله. إذا استمرت هذه المشكلة، اتصل بالمشرف لديك. +form_not_found = لم يتم التعرف على النموذج المرسَل. يُرجى إكماله وإعادة إرساله. إذا استمرت هذه المشكلة، اتصل بالمشرف لديك. +form_received = تم استلام إرسال النموذج. شكراً لك. +forms.json = JSON +forms.none.available = لا توجد نماذج متاحة حالياً. +forms.xml = XML +freetext.search = ابحث في كل شيء +from = مِن +generate.feedback.description = عدد مستندات الملاحظات المطلوب إنشاؤها\: +generate.feedback.title = إنشاء مستندات الملاحظات +health_center.field.children = المناطق +health_center.field.contact = جهة اتصال رئيسية +health_center.field.external_id = معرّف خارجي +health_center.field.name = الاسم +health_center.field.notes = ملاحظات +health_center.field.parent = المقاطعة +hours = ساعات +icon = أيقونة +icon.library = مكتبة أيقونات CHT +icons = الأيقونات +image = صورة +images = صور +images.header.tabs.icons = أيقونات علامات التبويب في الرأس +images.header.tabs.icons.default = الأيقونة الافتراضية +images.header.tabs.icons.description = قم بتكوين أيقونات علامة تبويب الرأس. يمكنك الاختيار بين أيقونات التطبيق بتنسيق svg. وأيقونات FontAwesome. +images.header.tabs.icons.fa.icon = أيقونة FontAwesome +images.header.tabs.icons.fontawesome.link = يُرجى قراءة المزيد عن FontAwesome. +images.header.tabs.icons.request.error = خطأ في إحضار الإعدادات. +images.header.tabs.icons.resource.icon = أيقونة المورد +images.header.tabs.icons.submit.failure = خطأ في إرسال الإعدادات +images.header.tabs.icons.submit.success = تم الإرسال بنجاح +images.header.tabs.icons.tab = علامة تبويب +images.icons.description = يتم استخدام الصور التي تم تحميلها هنا في جميع أنحاء التطبيق في أماكن مختلفة على النحو المحدد في رمز التطبيق. على سبيل المثال، يتم تحديد الصورة التي تظهر لأداة معينة في صفحة المستهدفات داخل رمز المستهدف. يمكنك تحميل الصور واحدة تلو الأخرى. وإذا حمّلت صورة تحمل نفس اسم صورة موجودة، فسيتم استبدالها بالملف الجديد. يُنصح أن تكون الصور بصيغة svg. أو png. وأن يكون حجمها أقل من 500 كيلوبايت. الموارد متاحة في +images.partners.description = أي شعارات لشريك تُضاف هنا ستظهر في صفحة "حول" في التطبيق. +import.export = استيراد وتصدير البيانات +initial.replication.duration = المدة +initial.replication.status = الحالة +initial.replication.status.complete = إكمال +initial.replication.status.failed = فشل +initial.replication.status.in_progress = قيد التنفيذ +initial.replication.status.pending = معلّق +initial.replication.title = التكرار الأولي +instance.stage.complete = تم الانتهاء من الإعداد. قم بالتثبيت عندما تكون جاهزاً. +instance.stage.confirm = تأكيد الإعداد +instance.stage.confirm.note = ملاحظة\: أنت تقوم بإعداد هذا النشر، ويعني هذا أنه سيتم تنفيذ أكبر قدر ممكن من العمل في الخلفية لتحضير عملية التثبيت بدون مقاطعة المستخدمين. لن يتم النشر حتى تُجري عملية النشر بعد اكتمال الإعداد. +instance.stage.deployment = جارٍ إعداد النشر +instance.upgrade = الترقيات +instance.upgrade.at = تشغيل +instance.upgrade.betas = النسخ التجريبية +instance.upgrade.branches = الفروع +instance.upgrade.build.version = الإصدار +instance.upgrade.cancel = إلغاء الترقية +instance.upgrade.cancelling = جارٍ إلغاء الترقية +instance.upgrade.cancel.note = سيؤدي الإلغاء إلى إيقاف الترقية الحالية وإعادة ضبط أي تقدم أُحرز أثناء الإعداد. +instance.upgrade.cancel.summary = أنت على وشك إلغاء الترقية من {{before}} إلى {{after}}. +instance.upgrade.complete = اكتمل النشر. +instance.upgrade.confirm = تأكيد الترقية +instance.upgrade.confirming = جارٍ تأكيد الترقية +instance.upgrade.confirm.summary = أنت على وشك الترقية من {{before}} إلى {{after}}. +instance.upgrade.confirm.warning.text = لا يمكن التراجع عن هذا الإجراء. تأكد من نسخ بياناتك احتياطياً\! +instance.upgrade.current_version = الإصدار الحالي +instance.upgrade.date = تاريخ الإصدار +instance.upgrade.deployed_by = تم النشر بواسطة +instance.upgrade.deployment = النشر قيد التقدم +instance.upgrade.error.abort = خطأ أثناء إلغاء الترقية +instance.upgrade.error.deploy = خطأ أثناء تشغيل التحديث +instance.upgrade.error.deploy_info_fetch = خطأ أثناء إحضار معلومات النشر +instance.upgrade.error.get_upgrade = خطأ أثناء إحضار تقدم الترقية +instance.upgrade.error.version_fetch = خطأ أثناء جلب الإصدارات المتاحة +instance.upgrade.feature_releases = الإصدارات التجريبية من الميزات +instance.upgrade.install = تثبيت +instance.upgrade.interrupted = تسبب خطأ غير متوقع في الخادم في انقطاع الترقية. +instance.upgrade.no_betas = لا توجد إصدارات تجريبية جديدة يمكنك الترقية إليها. +instance.upgrade.no_branches = لا توجد فروع إصدارات متاحة. +instance.upgrade.no_details = (التفاصيل غير متوفرة) +instance.upgrade.no_feature_releases = لا توجد إصدارات ميزات جديدة يمكنك الترقية إليها. +instance.upgrade.no_horti = Horticulturalist مطلوب +instance.upgrade.no_horti.detail = لاستخدام هذه الصفحة، يجب نشر المثيل الخاص بك عبر Horticulturalist. +instance.upgrade.no_new_releases = لا توجد إصدارات جديدة يمكنك الترقية إليها. +instance.upgrade.potentiallyIncompatible = قد يكون هذا الإصدار غير متوافق مع الإصدار الحالي لديك. +instance.upgrade.releases = الإصدارات +instance.upgrade.retry = إعادة محاولة الترقية +instance.upgrade.stage = الإعداد +instance.upgrade.state.completing = جارٍ إكمال الترقية +instance.upgrade.state.complete = الترقية مكتملة +instance.upgrade.state.finalizing = جارٍ إنهاء الترقية +instance.upgrade.state.initiated = تم بدء الترقية +instance.upgrade.state.staged = تم إعداد الترقية +instance.upgrade.state.indexing = جارٍ فهرسة طرق العرض المُعدّة +instance.upgrade.state.indexed = اكتملت الفهرسة +instance.upgrade.state.interrupted = منقطع +instance.upgrade.upgrading = تم بدء ترقية من {{before}} إلى {{after}}. +instance.upgrade.version = الإصدار +instance.upgrade.pre_releases = الإصدارات التجريبية +instance.upgrade.pre_releases_warning = الإصدارات في هذا القسم غير آمنة للنشر في بيئات الإنتاج. +invalid.query = هذا الاستعلام غير صالح. يُرجى الاطّلاع على صفحة مساعدة البحث المتقدم للحصول على مزيد من المعلومات حول بناء جملة الاستعلام. +login = تسجيل الدخول +login.error = حدث خطأ غير متوقع أثناء تسجيل الدخول. يُرجى إعادة المحاولة. +login.hide_password = إخفاء كلمة المرور +login.incorrect = اسم المستخدم أو كلمة المرور غير صحيحة. يُرجى إعادة المحاولة. +login.show_password = إظهار كلمة المرور +login.token.expired = انتهت صلاحية الرابط. اتصل بالمسؤول لتلقي رابط جديد. +login.token.general.error = حدث خطأ أثناء معالجة طلبك. يُرجى إعادة المحاولة. إذا حدث هذا مرة أخرى، فاتصل بالمسؤول لتلقي رابط جديد. +login.token.invalid = الرابط الخاص بك غير صالح. اتصل بالمسؤول لتلقي رابط جديد. +login.token.loading = جارٍ تسجيل دخولك. يرجى الانتظار. +login.token.missing = الرابط لا يحتوي على المعلومات المطلوبة. اتصل بالمسؤول لتلقي رابط جديد. +login.token.redirect.login = صفحة تسجيل الدخول +login.token.redirect.login.info = إذا كنت تعرف اسم المستخدم وكلمة المرور الخاصين بك، انقر على الرابط التالي لتحميل صفحة تسجيل الدخول. +login.token.timeout = انتهت مهلة المحاولة لتسجيل الدخول. يُرجى إعادة المحاولة. إذا حدث هذا مرة أخرى، فاتصل بالمسؤول لتلقي رابط جديد. +login.unsupported_browser = للحصول على تجربة أفضل في التطبيق، يُرجى الاتصال بالمسؤول أو المشرف لديك. +login.unsupported_browser.outdated_cht_android = اطلب منهم تحديث cht-android. +login.unsupported_browser.outdated_webview_apk = اطلب منهم تحديث ملف APK الخاص بـ WebView. +login.unsupported_browser.outdated_browser = اطلب منهم تحديث متصفحك. +message.characters.left = عدد الحروف المتبقي {{characters}} +message.characters.left.multiple = عدد رسائل SMS‏ {{messages}}، عدد الحروف المتبقية {{characters}} +message.characters.left.multiple.many = عدد رسائل SMS‏ {{messages}}، عدد الحروف المتبقية {{characters}} - عدد الرسائل كبير جداً\! +message.list.complete = لم يتم العثور على المزيد من الرسائل +messages.c.report_accepted = شكراً لك {{contact.name}} لتسجيل {{patient_name}}. المعرّف هو {{patient_id}}. تم تسجيلهم في الجدول الزمني لصحة الطفل. +messages.c.validation.months_since_birth = صيغة التسجيل لـ {{patient_id}} غير صحيحة. يُرجى التأكد من أن العمر مُحدد بالأشهر ويتراوح بين 0 و59. +messages.c.validation.patient_name = {{\#patient_name}}تنسيق التسجيل غير صحيح. ابدأ بحرف C متبوعاً بمسافة، والعمر بالأسابيع متبوعاً بمسافة، واسم الطفل (بحد أقصى 30 حرفاً).{{/patient_name}}{{^patient_name}}.لا يحتوي نموذج التسجيل على اسم. ابدأ بحرف C متبوعاً بمسافة، ثم العمر بالأسابيع متبوعاً بمسافة، ثم اسم الطفل{{/patient_name}}. +messages.c.validation.weeks_since_birth = صيغة التسجيل لـ {{patient_id}} غير صحيحة. يُرجى التأكد من أن العمر مُحدد بالأسابيع ويتراوح بين 0 و260. +messages.d.report_accepted = شكراً لك {{contact.name}} على الإبلاغ عن ولادة {{patient_name}} ({{patient_id}}). +messages.d.validation.days_since_birth = تقرير ولادة {{patient_id}} غير صحيح. يُرجى التأكد من أن عدد الأيام منذ الولادة هو عدد أيام يتراوح بين 0 و365، أو تركه فارغاً. +messages.d.validation.delivery_code = {{#delivery_code}} لم يتم فهم رمز الولادة. يُرجى استخدام الرمز 'F' للمرفق، و'S' للولادة في المنزل بمساعدة قابِلة ماهرة، و'NS' للولادة بدون مساعدة قابِلة ماهرة.{{/delivery_code}}{{^delivery_code}}كان رمز الولادة مفقوداً. يُرجى استخدام الرمز 'F' للمرفق، و'S' للولادة في المنزل بمساعدة قابِلة ماهرة، و'NS' للولادة بدون مساعدة قابِلة ماهرة.{{/delivery_code}} +messages.errors.invalid = رسالة غير صالحة\: +messages.errors.message.empty = الترجمة مفقودة +messages.errors.patient.missing = لم يتم العثور على المريضـ(ـة) +messages.errors.place.missing = لم يتم العثور على المكان +messages.errors.unknown.contact = جهة الاتصال هذه لم تعد موجودة، ولا يمكنك إرسال رسالة إليها. +messages.f.report_accepted = شكراً لك على الإبلاغ عن {{patient_name}} ({{patient_id}}). إذا كانت هذه حالة طارئة، يرجى إحالتها إلى العيادة فوراً. +messages.f.report_accepted_parent = تم الإبلاغ عن {{patient_name}} ({{patient_id}}) من قِبل {{contact.name}} ({{contact.phone}}) لمزيد من المتابعة. +messages.generic.no_provided_patient_id = تم تكوين دعم رقم المعرّف الخارجي، ولكن لا يمكن العثور عليه في المستند في الموقع الذي تم تكوينه. +messages.generic.provided_patient_id_not_unique = رقم المعرّف المقدم مستخدَم بالفعل. +messages.generic.provided_phone_not_valid = رقم الهاتف المقدّم غير صالح. +messages.generic.registration_accepted = شكراً لك {{contact.name}} لتسجيل {{patient_name}}. المعرّف هو {{patient_id}}. +messages.generic.registration_not_found = لم يتم العثور على شخص يحمل رقم المعرّف '{{patient_id}}'. تحقّق من المعرّف وأعد إرسال الرسالة. +messages.generic.report_accepted = شكراً لك {{contact.name}}، تم تسجيل {{form}} لـ {{patient_name}} ({{patient_id}}). +messages.generic.sys.facility_not_found = لم يتم تسجيلك لاستخدام هذا التطبيق. +messages.generic.validation.patient_id = {{\#patient_id}}رقم المعرّف المقدّم غير صحيح، ويجب أن يتكوّن من 5 إلى 13 رقماً. يُرجى إرسال معرّف صالح.{{/patient_id}}{{^patient_id}}تنسيق الرسالة غير صحيح. يرجى التأكد من إرسال رسالة تبدأ بـ {{form}} متبوعة بمسافة ثم معرّف الشخص.{{patient_id}} +messages.m.report_accepted = شكراً لك {{contact.name}}، تم تسجيل زيارة بعد الولادة لِـ {{patient_name}} ({{patient_id}}). +messages.n.report_accepted = شكراً لك {{contact.name}} لتسجيل {{patient_name}}. المعرّف هو {{patient_id}}. إذا كانت المرأة حاملاً، يرجى تسجيلها في الرسالة السابقة للولادة باستخدام النموذج P. +messages.n.validation.patient_name = {{\#patient_name}}تنسيق التسجيل غير صحيح. لذا تأكّد من أن الرسالة تبدأ بحرف N متبوعاً بمسافة واسم الشخص (بحد أقصى 30 حرفاً).{{/patient_name}}{{^patient_name}}تنسيق التسجيل غير صحيح. لذا تأكّد من أن الرسالة تبدأ بحرف N متبوعاً بمسافة واسم الشخص.{{/patient_name}}. +messages.off.report_accepted = لن يتم إرسال أي إشعارات أخرى بخصوص {{patient_name}} حتى تُرسل 'ON {{patient_id}}'.{{\#chw_sms}} {{chw_sms}}{{/chw_sms}} +messages.on.report_accepted = تمت إعادة تنشيط الإشعارات الخاصة بـ {{patient_name}} ({{patient_id}}).{{\#chw_sms}} {{chw_sms}}{{/chw_sms}} +messages.p.report_accepted = شكراً لك على تسجيل الحمل لِـ {{patient_name}} ({{patient_id}}).{{\#expected_date}} تاريخ الولادة المتوقع هو {{\#date}}{{expected_date}}{{/date}}{{/expected_date}} +messages.p.validation.last_menstrual_period = تنسيق التسجيل لـ {{patient_name}} غير صحيح. يرجى التأكد من أن الدورة الشهرية الأخيرة هو رقم بين 2 و42. +messages.relay.chw_sms = {{\#chw_sms}}{{chw_sms}}{{/chw_sms}} +messages.schedule.anc.checkin = أين وضعت {{patient_name}} مولودها؟ أجب باستخدام 'D {{patient_id}} F'‏ للولادة في المرفق، و'D {{patient_id}} S' للولادة بمساعدة قابِلة ماهرة، و'D {{patient_id}} NS' للولادة في المنزل بدون مساعدة قابِلة ماهرة. +messages.schedule.anc.followup = هل حضر(ت) {{patient_name}} زيارة الرعاية السابقة للولادة؟ عندما تفعل ذلك، يُرجى الرد بـ 'V {{patient_id}}'. شكراً لك\! +messages.schedule.anc.reminder = يرجى تذكير {{patient_name}} ({{patient_id}}) بزيارة مرفق الصحة لإتمام زيارة الرعاية السابقة للولادة هذا الأسبوع. عندما تقوم بهذه الخطوة، يُرجى إعلامنا بذلك باستخدام 'V {{patient_id}}'. شكراً\! +messages.schedule.anc.reminder_due = يجب على {{patient_name}} ({{patient_id}}) إجراء زيارة الرعاية السابقة للولادة هذا الأسبوع. يرجى التأكيد برسالة 'V'. إذا كانت قد وضعت مولودها، يُرجى إرسال رسالة "D". +messages.schedule.anc.week_38 = {{patient_name}} ({{patient_id}}) حامل في الأسبوع الـ 38 وستلد قريباً. يرجى إحالتها إلى مرفق الصحة. +messages.schedule.anc.week_40 = {{patient_name}} ({{patient_id}}) حامل في الأسبوع الـ 40 وستلد قريباً. يرجى إحالتها إلى مرفق الصحة. +messages.schedule.child.month_01 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_02 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_03 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_04 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_05 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_06 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_07 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_08 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_09 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_10 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_11 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_12 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_13 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_14 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_15 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_16 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_17 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_18 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_19 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_20 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_21 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_22 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_23 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_24 = يرجى زيارة {{patient_name}} {{patient_id}} للتذكير بالتطعيمات الواجبة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_25 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_26 = يُرجى زيارة {{patient_name}} {{patient_id}} للتأكد من الحصول على التطعيمات اللازمة + التحقق من علامات اعتلال الصحة. شكراً لك\! +messages.schedule.child.month_27 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_28 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_29 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_30 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_31 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_32 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_33 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_34 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_35 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_36 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_37 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_38 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_39 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_40 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_41 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_42 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_43 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_44 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_45 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_46 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_47 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_48 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_49 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_50 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_51 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_52 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_53 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_54 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_55 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_56 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_57 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_58 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_59 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.child.month_60 = يُرجى زيارة {{patient_name}} {{patient_id}} للتحقق من علامات اعتلال الصحة. راجع مرفق الصحة إذا لزم الأمر. شكراً لك\! +messages.schedule.imm.checkin = يُرجى زيارة {{patient_name}} ({{patient_id}}) لتذكيرها بالتطعيمات الواجبة . يرجى الإبلاغ عن أي لقاحات جديدة تم تلقيها. شكراً لك\! +messages.schedule.imm.followup = هل حضر(ت) {{patient_name}} ({{patient_id}}) الموعد لتلقي التطعيمات؟ عندما تفعل ذلك، يرجى الإبلاغ عن التطعيمات التي تم تلقيها. شكراً لك\! +messages.schedule.imm.reminder = يرجى تذكير {{patient_name}} ({{patient_id}}) بحضور موعد التطعيمات هذا الأسبوع. عندما تفعل ذلك، يرجى الإبلاغ عن التطعيمات التي تم تلقيها. شكراً لك\! +messages.schedule.postnatal.day_0 = يرجى متابعة {{patient_name}} ({{patient_id}}) للتأكد من حضورها زيارة ما بعد الولادة في اليوم 0 غداً. عندما تفعل ذلك، أخبرنا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.day_0_overdue = هل حضر(ت) {{patient_name}} ({{patient_id}}) زيارة ما بعد الولادة في اليوم 0؟ أخبرونا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.day_3 = يرجى متابعة {{patient_name}} ({{patient_id}}) للتأكد من حضورها زيارة ما بعد الولادة في اليوم 3 غداً. عندما تفعل ذلك، أخبرنا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.day_3_overdue = هل حضر(ت) {{patient_name}} ({{patient_id}}) زيارة ما بعد الولادة في اليوم 3؟ أخبرونا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.day_7 = يرجى متابعة {{patient_name}} ({{patient_id}}) للتأكد من حضورها زيارة ما بعد الولادة في اليوم 7 غداً. عندما تفعل ذلك، أخبرنا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.day_7_overdue = هل حضر(ت) {{patient_name}} ({{patient_id}}) زيارة ما بعد الولادة في اليوم 7؟ أخبرونا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.week_6 = يرجى متابعة {{patient_name}} ({{patient_id}}) للتأكد من حضورها زيارة ما بعد الولادة في الأسبوع 6 هذا الأسبوع. عندما تفعل ذلك، أخبرنا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.postnatal.week_6_overdue = هل حضر(ت) {{patient_name}} ({{patient_id}}) زيارة ما بعد الولادة في الأسبوع 6؟ أخبرونا بـ 'M {{patient_id}}'. شكراً\! +messages.schedule.registration.followup_anc = مرحباً {{contact.name}}، يُرجى تذكر تقديم تسجيل الحمل لِـ {{patient_name}} {{patient_id}} مع 'P {{patient_id}} <الأسابيع بعد الدورة الشهرية الأخيرة>'. شكراً\! +messages.schedule.registration.followup_anc_pnc = {{contact.name}}، هل احتاجت {{patient_name}} ({{patient_id}}) إلى الرعاية؟ لتسجيل الحمل، أرسل 'P {{patient_id}} <الأسابيع بعد الدورة الشهرية الأخيرة>'. لرعاية ما بعد الولادة، أرسل تقرير الولادة باستخدام الصيغة 'D {{patient_id}} '. شكراً لك\! +messages.sent.by = تم الإرسال من قِبَل {{senderName}} +messages.unknown.sender = المرسِل غير معروف +messages.v.report_accepted = شكراً لك {{contact.name}}، تمت تسجيل زيارة {{patient_name}} ({{patient_id}}). +metadata = بيانات التعريف +minutes = الدقائق +missing_fields = حقول مفقودة أو غير صالحة\: {{fields}}. +month = شهر +month.plural = أشهر +months = أشهر +mother_outcome = حصيلة صحة الأم +mrdt.disabled = تطبيق MRDT غير متوفر على هذا الجهاز. +mrdt.verify = التقاط صورة +muted = طابع زمني معطل +n.month = {MONTHS, plural, one{1 month} other{\# months}} +n.week = {WEEKS, plural, one{1 week} other{\# weeks}} +no = لا +number.bytes = {{number}} بايت +number.seconds = {{number}} ثانية +number\ errors = واجهنا {{number}} أخطا\: +number\ records = عدد السجلات {{number}} +online.action.message = هذا الإجراء يتطلب اتصالاً بالإنترنت. يرجى المحاولة مرة أخرى عند توفر الشبكة. +online.action.title = يرجى التأكد من الاتصال بالإنترنت. +overwrite.existing.records.help = إذا تم تحديد هذا الخيار، فسيتم تحديث جهات الاتصال التي تم تحميلها إذا كانت موجودة بالفعل في قاعدة البيانات. وإذا لم يتم تحديده، فسيتم تجاوزها. +partner.logo.field = شعار الشريك +partner.logo.upload = تحميل شعار الشريك +partner.name.field = اسم الشريك +partner.supporting = الشركاء الداعمون +partner.tab.partners = الشركاء +password.incorrect = كلمة المرور غير صحيحة. +password.length.minimum = يجب أن تكون كلمة المرور مؤلفة على الأقل من {{minimum}} حرفاً. +password.update = تحديث كلمة المرور +password.weak = كلمة المرور سهلة جداً ليتم تخمينها. يرجى تضمين مجموعة متنوعة من الحروف لجعلها أكثر تعقيداً. +patient\ id\ not\ found\ response = يُرجى إرسال الرسالة التالية إذا اجتازت جميع عمليات التحقق ولكن لم يتم العثور على معرّف المريضـ(ـة). +patient_id = معرّف المريضـ(ـة) +pending = الطابع الزمني معلّق +people = الأشخاص +permission = الإذن +permission.description.can_access_gateway_api = السماح بالوصول إلى واجهات برمجة التطبيقات للعمل كبوابة رسائل SMS. +permission.description.can_aggregate_targets = السماح بالاطّلاع على صفحة المجاميع المستهدفة +permission.description.can_bulk_delete_reports = السماح باستخدام وظائف الحذف المجمّع لحذف تقارير متعددة. +permission.description.can_configure = يسمح بتهيئة جميع تكوينات التطبيق. +permission.description.can_create_people = السماح بإنشاء أشخاص جدد. +permission.description.can_create_places = السماح بإنشاء أماكن جديدة. +permission.description.can_create_records = السماح بالوصول إلى واجهات برمجة التطبيقات لإنشاء التقارير. +permission.description.can_create_users = السماح بإنشاء مستخدمين. +permission.description.can_delete_contacts = السماح بحذف الأشخاص والأماكن. +permission.description.can_delete_messages = السماح بحذف الرسائل. +permission.description.can_delete_reports = السماح بحذف التقارير. +permission.description.can_delete_users = السماح بحذف المستخدمين. +permission.description.can_edit = السماح بإنشاء المستندات وتعديلها في قاعدة البيانات. +permission.description.can_edit_profile = السماح بتعديل إعدادات المستخدم الخاصة بهم. +permission.description.can_edit_verification = السماح بتحديث حالات التحقق من التقارير. +permission.description.can_export_all = السماح بتصدير جميع البيانات حتى لو لم يكن لديهم الإذن عادةً لعرضها. +permission.description.can_export_contacts = السماح بتصدير جميع جهات الاتصال. +permission.description.can_export_feedback = السماح بتصدير جميع ملاحظات المستخدمين. +permission.description.can_export_messages = السماح بتصدير جميع التقارير والرسائل. +permission.description.can_log_out_on_android = السماح بتسجيل الخروج من تطبيق أندرويد. +permission.description.can_update_places = السماح بتحديث مستندات الأماكن الحالية. +permission.description.can_update_reports = السماح بتحديث مستندات التقارير الحالية. +permission.description.can_update_users = السماح بتحديث المستخدمين الحاليين. +permission.description.can_verify_reports = السماح بتعيين حالات التحقق من التقرير إذا لم يتم تعيين أي حالة في الوقت الحالي. +permission.description.can_view_analytics = السماح بعرض التحليلات. +permission.description.can_view_analytics_tab = عرض علامة تبويب التحليلات في التطبيق. إذا لم يتم تعيينها سيتم عرض عنصر قائمة في قائمة التطبيق بدلاً من ذلك. +permission.description.can_view_call_action = عرض زر لبدء مكالمة مع الشخص المحدد. +permission.description.can_view_contacts = السماح بعرض جهات الاتصال. +permission.description.can_view_contacts_tab = عرض علامة تبويب جهات الاتصال في التطبيق. وإذا لم يتم تعيينها، فسيتم عرض عنصر قائمة في قائمة التطبيق بدلاً من ذلك. +permission.description.can_view_last_visited_date = عرض تاريخ آخر زيارة في قائمة جهات الاتصال. +permission.description.can_view_uhc_stats = السماح بعرض إحصائيات التغطية الصحية الشاملة في ملخص جهات الاتصال. +permission.description.can_view_message_action = عرض زر لإرسال رسالة إلى الشخص المحدد. +permission.description.can_view_messages = السماح بعرض الرسائل. +permission.description.can_view_messages_tab = عرض علامة تبويب الرسائل في التطبيق. وإذا لم يتم تعيينها، فسيتم عرض عنصر قائمة في قائمة التطبيق بدلاً من ذلك. +permission.description.can_view_old_navigation = السماح بعرض شريط التنقل القديم. +permission.description.can_view_outgoing_messages = السماح بعرض شاشة الرسائل الصادرة في تطبيق المسؤول. +permission.description.can_view_reports = السماح بعرض التقارير. +permission.description.can_view_reports_tab = عرض علامة تبويب التقارير في التطبيق. وإذا لم يتم تعيينها، فسيتم عرض عنصر قائمة في قائمة التطبيق بدلاً من ذلك. +permission.description.can_view_tasks = السماح بعرض المهام. +permission.description.can_view_tasks_group = السماح بعرض صفحة المهام للأسرة. +permission.description.can_view_tasks_tab = عرض علامة تبويب المهام في التطبيق. وإذا لم يتم تعيينها، فسيتم عرض عنصر قائمة في قائمة التطبيق بدلاً من ذلك. +permission.description.can_view_unallocated_data_records = السماح بالاطّلاع على التقارير التي لم يتم تعيين جهة اتصال لها. +permission.description.can_view_users = السماح بالحصول على قائمة بجميع المستخدمين المهيئين. +permission.description.can_write_wealth_quintiles = السماح بتحديث جهات الاتصال بخُمس ثرواتهم. +permission.description.can_upgrade = السماح بترقية إصدار CHT Core Framework عبر واجهة برمجة التطبيقات أو واجهة الإدارة. +permission.description.can_have_multiple_places.not_allowed = الأدوار المحددة ليس لديها إذن لتعيين أماكن متعددة. +permission.description.can_have_multiple_places.incompatible_place = يجب أن تكون الأماكن المحددة من نفس النوع (أن تكون في نفس مستوى التسلسل الهرمي). +permissions = الأذونات +person.field.alternate_phone = رقم هاتف بديل +person.field.code = الرمز +person.field.date_of_birth = تاريخ الميلاد +person.field.first_name = الاسم الأول +person.field.last_name = اسم العائلة +person.field.name = الاسم +person.field.national_id_number = رقم الهوية الوطنية +person.field.notes = ملاحظات +person.field.parent = المنطقة +person.field.phone = رقم الهاتف +person.field.title = العنوان +phone\ number\ not\ unique = هذا الرقم مسجل بالفعل لجهة الاتصال {{name}} +place.deleted = [محذوف] +place.unavailable = [غير متاح] +privacy.policy = سياسة الخصوصية +privacy.policy.accept = قبول +privacy.policy.not.found = سياسة الخصوصية غير موجودة +purge.description = قم بتشغيل فحص فوراً لإزالة المستندات المحلية المؤهلة. +purge.title = إزالة +quarter = الربع +quarter.plural = الأرباع +received = الطابع الزمني المُستلَم +registrant = المسجِّل +registrants\ supervisor = مشرف المسجِّل +registration\ date = تاريخ التسجيل +related_entities.clinic.contact.name = اسم جهة اتصال المنطقة +related_entities.clinic.external_id = معرّف المنطقة الخارجية +related_entities.clinic.name = اسم المنطقة +related_entities.clinic.parent.contact.name = اسم جهة الاتصال بمركز الصحة +related_entities.clinic.parent.external_id = المعرّف الخارجي لمركز الصحة +related_entities.clinic.parent.name = اسم مركز الصحة +related_entities.clinic.parent.parent.external_id = المعرّف الخارجي للمقاطعة +related_entities.clinic.parent.parent.name = اسم المقاطعة +related_entities.health_center.contact.name = اسم جهة الاتصال بمركز الصحة +related_entities.health_center.name = اسم مركز الصحة +related_entities.health_center.parent.name = اسم المقاطعة +reload.app = إعادة تحميل التطبيق +replication.disabled = معطّل للمسؤولين +replication.last.success.from = آخر نسخ متماثل ناجح من الخادم +replication.last.success.to = آخر نسخ متماثل ناجح إلى الخادم +replication.last.success.unknown = غير معروف +report.child_health_registration.birth_date = تاريخ الولادة +report.child_health_registration.chw_name = اسم العامل الصحي المجتمعي +report.child_health_registration.chw_phone = رقم هاتف العامل الصحي المجتمعي +report.child_health_registration.chw_sms = ملاحظة إلى العامل الصحي المجتمعي +report.child_health_registration.patient_id = المعرّف +report.child_health_registration.patient_name = الاسم +report.clinic_visit.title.sms = زيارة العيادة (رسالة SMS) +report.content.raw = محتوى التقرير الأولي +report.created = التقرير المقدم +report.delivery.birth_date = تاريخ الميلاد +report.delivery.chw_name = اسم العامل الصحي المجتمعي +report.delivery.chw_phone = رقم هاتف العامل الصحي المجتمعي +report.delivery.chw_sms = ملاحظة إلى العامل الصحي المجتمعي +report.delivery.delivery_code = رمز الولادة +report.delivery.delivery_code_label = موقع الولادة +report.delivery.delivery_date = تاريخ الولادة +report.delivery.label_delivery_code = موقع الولادة +report.delivery.label_pregnancy_outcome = حصيلة الحمل +report.delivery.patient_id = المعرّف +report.delivery.patient_name = الاسم +report.delivery.pregnancy_outcome = حصيلة الحمل +report.delivery.pregnancy_outcome_label = حصيلة الحمل +report.delivery.title.sms = تقرير الولادة (رسالة SMS) +report.edit = تعديل التقرير +report.flag.title.sms = إشارة علامة الخطر (رسالة SMS) +report.new_report.button = تقرير جديد +report.new_child.title.sms = تسجيل طفل جديد + تسجيل تطعيم (رسالة SMS) +report.new_person.title.sms = شخص جديد (رسالة SMS) +report.new_pregnancy.title.sms = حمل جديد (رسالة SMS) +report.off.title.sms = إيقاف تشغيل الإشعارات (رسالة SMS) +report.on.title.sms = تشغيل الإشعارات (رسالة SMS) +report.postnatal_visit.chw_name = اسم العامل الصحي المجتمعي +report.postnatal_visit.chw_phone = رقم هاتف العامل الصحي المجتمعي +report.postnatal_visit.chw_sms = ملاحظة إلى العامل الصحي المجتمعي +report.postnatal_visit.danger_signs_baby = علامات الخطر على الطفل الرضيع +report.postnatal_visit.danger_signs_mom = علامات خطر على الأم +report.postnatal_visit.patient_id = المعرّف +report.postnatal_visit.patient_name = الاسم +report.postnatal_visit.referral_follow_up_needed_baby = متابعة الإحالة المطلوبة للطفل الرضيع +report.postnatal_visit.referral_follow_up_needed_mom = متابعة الإحالة المطلوبة للأم +report.postnatal_visit.visit_confirmed = تم تأكيد الزيارة +report.pregnancy.chw_name = اسم العامل الصحي المجتمعي +report.pregnancy.chw_phone = رقم هاتف العامل الصحي المجتمعي +report.pregnancy.chw_sms = ملاحظة إلى العامل الصحي المجتمعي +report.pregnancy.danger_signs = علامات الخطر +report.pregnancy.edd = تاريخ الولادة المتوقع +report.pregnancy.lmp_date = تاريخ الدورة الشهرية الأخيرة +report.pregnancy.patient_id = المعرّف +report.pregnancy.patient_name = الاسم +report.pregnancy.risk_factors = عوامل الخطر +report.pregnancy_visit.chw_name = اسم العامل الصحي المجتمعي +report.pregnancy_visit.chw_phone = رقم هاتف العامل الصحي المجتمعي +report.pregnancy_visit.chw_sms = ملاحظة إلى العامل الصحي المجتمعي +report.pregnancy_visit.danger_signs = علامات الخطر +report.pregnancy_visit.patient_id = المعرّف +report.pregnancy_visit.patient_name = الاسم +report.pregnancy_visit.referral_follow_up_needed = متابعة الإحالة المطلوبة +report.pregnancy_visit.visit_confirmed = تم تأكيد الزيارة +reports.sidebar.filter.date = التاريخ +reports.sidebar.filter.from_date = مِن +reports.sidebar.filter.form_type = نوع النموذج +reports.sidebar.filter.place = المكان +reports.sidebar.filter.reset = إعادة ضبط +reports.sidebar.filter.status = الحالة +reports.sidebar.filter.submit = تطبيق +reports.sidebar.filter.title = الفلتر +reports.sidebar.filter.to_date = إلى: +report.subject.unknown = موضوع غير معروف +report.updated = تم تحديث التقرير +reported_date = التاريخ المُبلّغ عنه +reporter.link = انتقل إلى Medic Reporter +reporter.section.help = استخدم برنامج Medic Reporter لاختبار نماذج رسائل SMS. +reporter.section.title = Medic Reporter +reporting_unit_not_found = معرّف وحدة الإبلاغ غير صحيح. يرجى التصحيح وتقديم تقرير كامل مرة أخرى. +reports.none = لم يتم العثور على أي تقارير. +reports.none.n.months = {MONTHS, plural, one{No reports in the last month.} other{No reports in the last \# months.}} +reports.verify = المراجعة +reports.verify.invalid = يحتوي على أخطاء +reports.verify.valid = صحيح +responses = الاستجابات +save = حفظ التغييرات +saving = حفظ +schedule.anc_lmp = تذكيرات الرعاية السابقة للولادة الدورة الشهرية الأخيرة\: مجموعة {{group}} +schedule.anc_lmp_from_app = تذكيرات الرعاية السابقة للولادة الدورة الشهرية الأخيرة\: مجموعة {{group}} +schedule.anc_no_lmp = تذكيرات الرعاية السابقة للولادة\: مجموعة {{group}} +schedule.child_health = صحة الطفل\: مجموعة {{group}} +schedule.delivery_lmp = تذكيرات الولادة\: مجموعة {{group}} +schedule.delivery_lmp_from_app = تذكيرات الولادة\: مجموعة {{group}} +schedule.delivery_no_lmp = تذكيرات الولادة\: مجموعة {{group}} +schedule.imm = التطعيمات\: مجموعة {{group}} +schedule.nutrition_evaluation = تقييم التغذية +schedule.nutrition_otp = الجدول الزمني للبرنامج العلاجي للمرضى الخارجيين +schedule.nutrition_sc = الجدول الزمني لمركز إعادة التأهيل +schedule.nutrition_sc_exit = الجدول الزمني للخروج من مركز إعادة التأهيل +schedule.nutrition_sfp = الجدول الزمني لبرنامج التغذية التكميلية +schedule.pnc = رعاية ما بعد الولادة\: مجموعة {{group}} +schedule.pnc_facility_birth = رعاية ما بعد الولادة للولادة في المرفق\: مجموعة {{group}} +schedule.pnc_from_app = رعاية ما بعد الولادة من التطبيق\: مجموعة {{group}} +schedule.registration_anc = متابعة التسجيل +schedule.registration_anc_pnc = متابعة التسجيل +scheduled = الطابع الزمني المُجدوَل +scheduled_tasks = المهام المُجدوَلة +search_bar.filter.label = الفلتر +search_bar.sort.label = ترتيب +select.mode.count.plural = عدد السجلات المحددة {{number}} +select.mode.count.singular = سجل واحد تم تحديده +select.mode.delete.all = حذف الكل +select.mode.deselect.all = مسح الاختيار +select.mode.select.all = اختيار الكل +select.mode.start = اختيار +select.mode.stop = إغلاق +selection.doc.content.raw = محتوى التقرير الأولي +send\ the\ following\ message\ to\ the = أرسل الرسالة التالية إلى +sent = الطابع الزمني المُرسَل +sent_timestamp = الطابع الزمني المُرسَل +settings.backup.action = تنزيل الإعدادات الحالية +settings.backup.description = قم بتنزيل نسخة من الإعدادات الحالية. +settings.backup.title = رمز تطبيق النسخ الاحتياطي +settings.backuprestore = رمز تطبيق النسخ الاحتياطي +settings.backuprestore.description = يمكنك هنا تنزيل إعدادات التطبيق أو تحميلها بتنسيق JSON. يحتوي JSON هذا على معظم رمز التطبيق بخلاف النماذج والأيقونات والترجمات. +settings.restore.action = تحميل رمز التطبيق +settings.restore.description = استيراد ملف JSON جديد واستبدال الإعدادات الحالية. +settings.restore.title = تحميل رمز التطبيق +setup.language.outgoing.subtitle = اختر اللغة التي يرغب منسقو الرعاية لديك في تلقي الرسائل الآلية من CHT بها. +setup.language.subtitle = اختر اللغة الافتراضية التي سيستعملها مستخدموCHT. يمكن للمستخدمين ضبط التفضيلات الفردية بعد تسجيل الدخول. +sidebar_menu.title = القائمة +sms_message.message = رسالة واردة +sms_received = تم استلام رسالة SMS. ستتم مراجعتها قريباً. إذا كنت تحاول إرسال نموذج نصي، يرجى إدخال رمز النموذج الصحيح والمحاولة مرة أخرى. +state.cleared = تم المسح +state.delivered = تم التسليم +state.denied = مرفوض +state.duplicate = مكرر +state.failed = فشل +state.forwarded-by-gateway = أُعيد توجيهها عن طريق البوابة +state.forwarded-to-gateway = أُعيد توجيهها إلى البوابة +state.muted = تم الكتم +state.pending = معلّق +state.received = تم الاستلام +state.received-by-gateway = تم استلامها بواسطة البوابة +state.scheduled = تمت الجدولة +state.sent = تم الإرسال +status.review.correct = تمت المراجعة\: صحيح +status.review.errors = تمت المراجعة\: الأخطاء +status.review.title = مراجعة المدير +status.review.unverified = لم تتم المراجعة +status.sms.invalid = رسالة SMS غير صالحة +status.sms.title = صلاحية رسائل SMS +status.sms.valid = رسالة SMS صالحة +submit.icon = أيقونة التحميل +sync.last_success = آخر مزامنة +sync.now = إجراء المزامنة الآن +sync.retry = إعادة المحاولة +sync.feedback.failure.unknown = تعذّر إجراء المزامنة. تعذر الاتصال. +sync.status.in_progress = هل تجري المزامنة حالياً؟ +sync.status.not_required = تمت مزامنة جميع التقارير +sync.status.required = تقارير للمزامنة +sync.status.unknown = تعذر الاتصال +sys.empty = تظهر الرسالة فارغة. +sys.facility_not_found = المرفق غير موجود. +sys.form_not_found = النموذج '{{form}}' غير موجود. +sys.incorrect_type = نوع الحقل غير صحيح {{key}}، المتوقع {{expectedType}}. +sys.missing_fields = حقول مفقودة أو غير صالحة\: {{fields}}. +sys.recipient_not_found = تعذر العثور على مستلم الرسالة. +targets.all_time.subtitle = كل الوقت +targets.births.title = الولادات +targets.count.default = ({{pass}} من {{total}}) +targets.disabled = تم تعطيل المستهدفات لحساب المستخدم. +targets.no_targets = لم يتم العثور على مستهدف. +targets.this_month.subtitle = هذا الشهر +targets.last_month.subtitle = الشهر الماضي +task.date = تاريخ الاستحقاق +task.days.left = {DAYS, plural, one{1 day left} other{\# days left}} +task.list.complete = لا مزيد من المهام +task.overdue = موعد الاستحقاق اليوم +task.overdue.days = {DAYS, plural, +task.priority = الأولوية +task.state = حالة الرسالة +task.type = نوع الرسالة +tasks = الرسائل الصادرة +tasks.0.messages.0.message = الرسالة +tasks.0.messages.0.to = إلى +tasks.0.state = حالة +tasks.0.timestamp = الطابع الزمني +tasks.disabled = تم تعطيل المهام لحساب المستخدم. +tasks.none = لا توجد مهام. +tasks.none.n.weeks = {WEEKS, plural, one{No tasks in the next week.} other{No tasks in the next \# weeks.}} +tasks.group.leave = هل تريد بالتأكيد مغادرة هذه الصفحة؟ لن تتمكن بعد الآن من رؤية المهام الأخرى لهذه الأسرة. +tasks.group.completed = اكتمال جميع مهام الأسرة +tasks.group.title = مهام أخرى للأسرة +testing.description = لقد وجدت صفحة الاختبارات السرية الخاصة بنا التي تحتوي على الكثير من الوظائف الخطيرة. إذا لم تكن مستعداً لكسر التثبيت، فارجع الآن\! +testing.title = صفحة الاختبار +title = العنوان +to\ recipient = إلى {{recipient}} +today = اليوم +tomorrow = غداً +training_cards.confirm.exit = لم يتم الانتهاء من هذا التدريب. إذا غادرت الآن، ستفقد تقدمك وسيطلب منك إكماله في وقت لاحق. +training_cards.confirm.button.no = إلغاء +training_cards.confirm.button.yes = مغادرة +training_cards.confirm.title = هل تريد ترك التدريب؟ +training_cards.error.loading = خطأ في تحميل التدريب. يُرجى التواصل مع المشرف. +training_cards.error.save = خطأ في حفظ التدريب. +training_cards.form.saved = تم الانتهاء من التدريب. +training_materials.page.no_more_trainings = لا مزيد من التدريبات +training_materials.page.no_selected = لم يتم تحديد أي مواد تدريبية +training_materials.page.no_trainings = لم يتم العثور على أي تدريبات +training_materials.page.title = المواد التدريبية +translation.add = إضافة مفتاح ترجمة جديد +translation.key = مفتاح الترجمة +unique.id = معرّف فريد +unknown.contact = جهة اتصال غير معروفة +upgrade = الترقية +upgrade.description = لترقية تطبيقك إلى إصدار أو نسخة تجريبية أو فرع محدد، يوصى بإجراء الإعداد أولاً. وهذا يسمح بإنجاز العمل في الخلفية للتحضير لعملية التثبيت بدون مقاطعة المستخدمين. بمجرد اكتمال عملية الإعداد، انقر على "تثبيت" لمواصلة الترقية. لا يمكن التراجع عن هذا الإجراء، لذا يُرجى التأكد من عمل نسخة احتياطية لبياناتك وإخطار المستخدمين بفترة التوقف. +upload = التحميل +upload.favicon = رمز المفضلة +upload.header.logo = شعار الرأس +upload.icon = أيقونة التحميل +upload.json.forms.help = يجب أن يحتوي الملف على قائمة مشفرة بصيغة JSON لتعريفات النماذج. لا يجوز تحميل أكثر من ملف نموذج تطبيق واحد في كل مرة، وسيتم استبدال أي نماذج موجودة. +upload.json.forms.title = تحميل نماذج JSON +upload.sms.forms = تحميل نماذج رسائل SMS +upload.xform.context = التعريف +upload.xform.help = يجب أن تحتوي الملفات على نموذج XForm وسياق JSON المرتبط به. إذا كان هناك نموذج موجود، فسيتم استبداله. +upload.xform.meta.help = ملف بيانات التعريف بتنسيق JSON بما في ذلك الرمز والعنوان والسياق وما إلى ذلك. +upload.xform.title = تحميل نموذج XML +upload.xform.xml = XML +upload.xform.xml.help = تعريف xform في XML. +url = عنوان URL +user.fullname.help = الاسم الكامل للمستخدم +user.password.current = كلمة المرور الحالية +user.phone.help = يجب أن يكون رقم الهاتف صالحاً بدون شرطات أو علامات ترقيم. +user.place.help = يجب ربط جميع المستخدمين بمكان في النظام ليتم تحديد موقعهم بشكل صحيح في التسلسل الهرمي ورؤية البيانات المناسبة عند تسجيل الدخول. +user.username.help = هذا هو ما ستستخدمه لتسجيل الدخول إلى التطبيق. +username.invalid = اسم المستخدم غير صالح. الأحرف الصالحة هي الأحرف الصغيرة، والأرقام، والشرطة السفلية (_)، والواصلات (-). +username.taken = اسم المستخدم "{{username}}" محجوز بالفعل. +users.import.accepted_types = يتم قبول ملف بتنسيق csv. فقط. +users.import.cancel_upload = إلغاء +users.import.confirm_upload = نعم، قم بالتحميل +users.import.confirm_upload_text = هل تريد بالتأكيد تحميل +users.import.instructions = لإضافة عدة مستخدمين، يرجى تحميل جدول البيانات المناسب. +users.import.processing.title = معالجة المستخدمين +users.import.processing.instructions = قد تستغرق هذه العملية وقتاً طويلاً، إذا كنت تقوم بتحميل عدد كبير من المستخدمين، فيرجى التحلي بالصبر. +users.import.summary.added_users = تمت إضافة المستخدمين +users.import.summary.click = انقر +users.import.summary.download_status_file = لتحميل ملف الحالة الخاص بك. +users.import.summary.failed_users = تحتوي على أخطاء +users.import.summary.here = هنا +users.import.summary.previously_added_users = المستوردة سابقاً +users.import.summary.resubmit.instructions = لإعادة إرسال بيانات المستخدمين التي تحتوي على أخطاء: +users.import.summary.resubmit.step1 = 1. افتح ملف الحالة الذي تم تنزيله +users.import.summary.resubmit.step2 = 2. قارن الأخطاء مع جدول البيانات المنسق +users.import.summary.resubmit.step3 = 3. صحح الصفوف التي بها أخطاء وصدّرها إلى ملف بتنسيق csv. +users.import.summary.resubmit.step4 = 4. أعِد تحميل جدول البيانات المحدّث +users.import.title = الاستيراد من ملف +users.import.unavailable.instructions = يقوم شخص آخر في النظام حالياً بعملية إضافة عدة مستخدمين. +users.import.unavailable.retry = يرجى المحاولة مرة أخرى في وقت لاحق +users.import.unavailable.title = غير متوفر حالياً +users.import.upload_button = تحميل ملف csv. +users.manage.add_single_user = إضافة مستخدم +users.manage.back = العودة إلى قائمة المستخدمين +users.manage.import_users = الاستيراد من ملف +usertype._admin = المسؤول +usertype.admin = المسؤول +usertype.analytics = التحليلات - تصدير البيانات عبر عنوان URL فقط +usertype.computers = أجهزة الكمبيوتر +usertype.data-entry = إدخال البيانات - الوصول إلى Medic Reporter فقط +usertype.district-manager = المدير الإقليمي - وصول مقيّد +usertype.gateway = بوابة - مستخدم ذو وصول محدود لبوابة Medic +usertype.national-manager = المدير الوطني - الوصول إلى جميع المستندات +usertype.people = الأشخاص +usertype.unknown = غير معروف +validate.required = هذا الحقل مطلوب. +version = الإصدار +version.update.description = تم تحديث تطبيقك ويجب إعادة تحميله. إذا قمت بالإلغاء الآن، سيتم تذكيرك مرة أخرى بعد ساعتين. +version.update.title = التحديث متاح +view.all = عرض الكل +week = أسبوع +week.plural = أسابيع +weeks = أسابيع +wipe.device.description = تحذير\! سيؤدي ذلك إلى حذف جميع البيانات من جهازك وتسجيل خروجك. ستفقد أي بيانات غير متزامنة\! +wipe.device.title = مسح الجهاز +with = مع +with.lmp = مع الدورة الشهرية الأخيرة +without.lmp = بدون الدورة الشهرية الأخيرة +year = سنة +year.plural = سنوات +years = سنوات +yes = نعم +yesterday = أمس diff --git a/api/src/translations.js b/api/src/translations.js index 7c2c2bd64f3..a2da98857b5 100644 --- a/api/src/translations.js +++ b/api/src/translations.js @@ -23,7 +23,8 @@ const LOCAL_NAME_MAP = { sw: 'Kiswahili (Swahili)', hi: 'हिन्दी (Hindi)', id: 'Bahasa Indonesia (Indonesian)', - lg: 'Luganda (Ganda)' + lg: 'Luganda (Ganda)', + ar: 'عربي (Arabic)' }; const extractLocaleCode = filename => { diff --git a/webapp/src/js/bootstrapper/translator.js b/webapp/src/js/bootstrapper/translator.js index b9d52d5a170..d658d054344 100644 --- a/webapp/src/js/bootstrapper/translator.js +++ b/webapp/src/js/bootstrapper/translator.js @@ -130,6 +130,24 @@ const TRANSLATIONS = { SESSION_EXPIRED_DESCRIPTION: 'Sesi Anda telah berakhir dan Anda telah keluar. Silakan masuk kembali untuk melanjutkan.', LOGIN: 'Login', }, + ar: { + FETCH_INFO: ({ count, total }) => `جارٍ جلب المعلومات (${count} من مستندات ${total})...`, + LOAD_APP: 'جارٍ تحميل التطبيق…', + PURGE_INIT: 'جارٍ التحقق من البيانات…', + PURGE_INFO: ({ count }) => `تم تنظيف ${count} من المستندات...`, + PURGE_META: 'جارٍ تنظيف البيانات التعريفية…', + STARTING_APP: 'جارٍ بدء التطبيق…', + DOWNLOAD_APP: 'جارٍ تنزيل التطبيق…', + ERROR_MESSAGE: 'حدث خطأ أثناء التحميل، يرجى التحقق من اتصالك.', + TRY_AGAIN: 'حاول ثانية', + POLL_REPLICATION: 'استقصاء بيانات النسخ المتماثل...', + TOO_MANY_DOCS: ({ count, limit }) => `تحذير! أنت على وشك تنزيل مستندات ${count}، وهو ما يتجاوز الحد الموصى به وهو ${limit}. هل ترغب في الاستمرار؟`, + CONTINUE: 'متابعة', + ABORT: 'إلغاء', + SESSION_EXPIRED_TITLE: 'انتهت صلاحية الجلسة', + SESSION_EXPIRED_DESCRIPTION: 'لقد انتهت جلستك وتم تسجيل خروجك. الرجاء تسجيل الدخول للمتابعة.', + LOGIN: 'تسجيل الدخول', + }, }; /* eslint-enable max-len */ diff --git a/webapp/src/js/enketo/main.js b/webapp/src/js/enketo/main.js index fa92b6ee01a..41843ed3a48 100644 --- a/webapp/src/js/enketo/main.js +++ b/webapp/src/js/enketo/main.js @@ -6,6 +6,7 @@ require('bootstrap-datepicker/js/locales/bootstrap-datepicker.sw'); require('bootstrap-datepicker/js/locales/bootstrap-datepicker.id'); require('bootstrap-datepicker/js/locales/bootstrap-datepicker.bm'); require('bootstrap-datepicker/js/locales/bootstrap-datepicker.hi'); +require('bootstrap-datepicker/js/locales/bootstrap-datepicker.ar'); require('./bootstrap-datepicker.ceb'); require('./bootstrap-datepicker.hil'); require('./bootstrap-datepicker.tl'); diff --git a/webapp/src/ts/main.ts b/webapp/src/ts/main.ts index d4b492a22c9..191f50efb7b 100644 --- a/webapp/src/ts/main.ts +++ b/webapp/src/ts/main.ts @@ -32,6 +32,7 @@ require('moment/locale/hi'); require('moment/locale/id'); require('moment/locale/ne'); require('moment/locale/sw'); +require('moment/locale/ar'); require('select2'); require('../js/enketo/main'); From 58a5e7d901c5946c4cdebd23611bf13db1c35b3f Mon Sep 17 00:00:00 2001 From: Diana Barsan <35681649+dianabarsan@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:37:27 +0300 Subject: [PATCH 2/2] chore(#6250): remove request-promise-native from e2e tests (#9703) Updates e2e tests request to use built-in fetch instead of request-promise-native. Removes sessions from e2e tests, these are not needed anymore. Updates all (except 2) datasource e2e tests to query api directly. These tests required that global fetch had a session enabled to work, and stopped working once sessions were removed. #6250 --- tests/e2e/default/contacts/edit.wdio-spec.js | 3 +- .../e2e/default/logging/logging.wdio-spec.js | 1 - tests/e2e/default/pwa/manifest.wdio-spec.js | 4 +- tests/e2e/default/sms/rapidpro.wdio-spec.js | 10 +- ...ser-for-contacts.replace-user.wdio-spec.js | 8 +- tests/integration/.mocharc-base.js | 2 + .../api/controllers/all-docs.spec.js | 1 - .../api/controllers/bulk-docs.spec.js | 14 +- .../api/controllers/bulk-get.spec.js | 2 +- .../api/controllers/changes.spec.js | 6 +- .../api/controllers/contacts-by-phone.spec.js | 36 ++-- .../api/controllers/db-doc.spec.js | 166 +++++++++--------- .../api/controllers/hydration.spec.js | 32 ++-- .../integration/api/controllers/login.spec.js | 21 ++- .../api/controllers/person.spec.js | 52 +++--- .../integration/api/controllers/place.spec.js | 52 +++--- .../api/controllers/places.spec.js | 4 +- .../api/controllers/records.spec.js | 9 - .../api/controllers/settings.spec.js | 2 - .../integration/api/controllers/users.spec.js | 152 ++++++---------- .../api/controllers/well-known.spec.js | 2 +- tests/integration/api/rate-limit.spec.js | 4 +- tests/integration/api/routing.spec.js | 80 ++++----- tests/integration/api/server.spec.js | 117 ++++++------ tests/integration/infodocs/infodocs.spec.js | 2 +- .../medic-collect/medic-collect.spec.js | 31 ++-- tests/integration/nginx/nginx.spec.js | 20 +-- .../create-user-for-contacts.spec.js | 36 ++-- .../default/login/login.wdio.page.js | 4 +- tests/utils/index.js | 157 ++++++++++------- tests/utils/sentinel.js | 2 +- tests/utils/sms.js | 2 +- 32 files changed, 496 insertions(+), 538 deletions(-) diff --git a/tests/e2e/default/contacts/edit.wdio-spec.js b/tests/e2e/default/contacts/edit.wdio-spec.js index 2f7dd5972ad..c9c7ff74f06 100644 --- a/tests/e2e/default/contacts/edit.wdio-spec.js +++ b/tests/e2e/default/contacts/edit.wdio-spec.js @@ -1,3 +1,4 @@ +const cloneDeep = require('lodash/cloneDeep'); const loginPage = require('@page-objects/default/login/login.wdio.page'); const commonPage = require('@page-objects/default/common/common.wdio.page'); const placeFactory = require('@factories/cht/contacts/place'); @@ -16,7 +17,7 @@ describe('Edit ', () => { const offlineUserContact = personFactory.build({ name: CONTACT_NAME, parent: healthCenter }); const onlineUserContact = personFactory.build({ parent: healthCenter }); - healthCenter.contact = offlineUserContact; + healthCenter.contact = cloneDeep(offlineUserContact); const offlineUser = userFactory.build({ username: 'offline_user', diff --git a/tests/e2e/default/logging/logging.wdio-spec.js b/tests/e2e/default/logging/logging.wdio-spec.js index b5a309c62c5..4288d53ae9a 100644 --- a/tests/e2e/default/logging/logging.wdio-spec.js +++ b/tests/e2e/default/logging/logging.wdio-spec.js @@ -41,7 +41,6 @@ describe('audit log', () => { continuous: false }; const requestOptions = { - resolveWithFullResponse: true, path: '/_replicator', method: 'POST', body diff --git a/tests/e2e/default/pwa/manifest.wdio-spec.js b/tests/e2e/default/pwa/manifest.wdio-spec.js index fc88697b135..a6b1b8d137e 100644 --- a/tests/e2e/default/pwa/manifest.wdio-spec.js +++ b/tests/e2e/default/pwa/manifest.wdio-spec.js @@ -31,7 +31,7 @@ describe('manifest.json', () => { try { await utils.deleteDoc('branding'); } catch (err) { - if (err.statusCode === 404) { + if (err.status === 404) { return; // already not there - success! } throw err; @@ -53,7 +53,7 @@ describe('manifest.json', () => { try { return await utils.getDoc('branding'); } catch (e) { - if (e.statusCode === 404) { + if (e.status === 404) { return { _id: 'branding' }; } throw e; diff --git a/tests/e2e/default/sms/rapidpro.wdio-spec.js b/tests/e2e/default/sms/rapidpro.wdio-spec.js index fd5816a026c..9d3c378a324 100644 --- a/tests/e2e/default/sms/rapidpro.wdio-spec.js +++ b/tests/e2e/default/sms/rapidpro.wdio-spec.js @@ -75,7 +75,7 @@ describe('RapidPro SMS Gateway', () => { }); throw new Error('should have thrown'); } catch (err) { - expect(err.responseBody).to.eql({ code: 403, error: 'No incoming key configured' }); + expect(err.body).to.eql({ code: 403, error: 'No incoming key configured' }); } }); @@ -91,7 +91,7 @@ describe('RapidPro SMS Gateway', () => { }); throw new Error('should have thrown'); } catch (err) { - expect(err.responseBody).to.eql({ code: 403, error: 'Missing authorization token' }); + expect(err.body).to.eql({ code: 403, error: 'Missing authorization token' }); } }); @@ -108,7 +108,7 @@ describe('RapidPro SMS Gateway', () => { }); throw new Error('should have thrown'); } catch (err) { - expect(err.responseBody).to.eql({ code: 403, error: 'Incorrect token' }); + expect(err.body).to.eql({ code: 403, error: 'Incorrect token' }); } }); @@ -125,7 +125,7 @@ describe('RapidPro SMS Gateway', () => { }); throw new Error('should have thrown'); } catch (err) { - expect(err.responseBody).to.eql({ code: 403, error: 'Missing authorization token' }); + expect(err.body).to.eql({ code: 403, error: 'Missing authorization token' }); } }); @@ -143,7 +143,7 @@ describe('RapidPro SMS Gateway', () => { }); throw new Error('should have thrown'); } catch (err) { - expect(err.responseBody).to.eql({ code: 400, error: 'Message was not saved' }); + expect(err.body).to.eql({ code: 400, error: 'Message was not saved' }); } }); diff --git a/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js b/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js index 8597e8b2b7b..a67c72d1c3d 100644 --- a/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js +++ b/tests/e2e/default/transitions/create-user-for-contacts.replace-user.wdio-spec.js @@ -109,7 +109,7 @@ describe('Create user for contacts', () => { path: '/medic/login', body: { user: username, password, locale: 'en' }, method: 'POST', - simple: false, + resolveWithFullResponse: true, noAuth: true, }; return utils.request(opts); @@ -118,7 +118,7 @@ describe('Create user for contacts', () => { const assertUserPasswordChanged = async (user) => { // Cannot login because user's password has been automatically reset const resp0 = await submitLoginRequest(user); - expect(resp0.statusCode).to.equal(401); + expect(resp0.status).to.equal(401); // Update user's password to something we know await utils.request({ @@ -129,7 +129,7 @@ describe('Create user for contacts', () => { // Can login with new password const resp1 = await submitLoginRequest({ ...user, password: DISABLED_USER_PASSWORD }); - expect(resp1.statusCode).to.equal(302); + expect(resp1.status).to.equal(302); }; const assertNewUserSettings = (newUserSettings, newContact, originalUser) => { @@ -799,7 +799,7 @@ describe('Create user for contacts', () => { expect(updatedOriginalContact.user_for_contact).to.be.undefined; // Can still login as original user const resp1 = await submitLoginRequest(ONLINE_USER); - expect(resp1.statusCode).to.equal(302); + expect(resp1.status).to.equal(302); // New user not created const newUserSettings = await utils.getUserSettings({ contactId: replacementContactId }); expect(newUserSettings).to.be.empty; diff --git a/tests/integration/.mocharc-base.js b/tests/integration/.mocharc-base.js index 99223dd2f61..068a4b04fb7 100644 --- a/tests/integration/.mocharc-base.js +++ b/tests/integration/.mocharc-base.js @@ -7,7 +7,9 @@ const deepEqualInAnyOrder = require('deep-equal-in-any-order'); chai.use(chaiExclude); chai.use(chaiAsPromised); chai.use(deepEqualInAnyOrder); +chai.use(require('chai-shallow-deep-equal')); global.expect = chai.expect; +global.chai = chai; module.exports = { allowUncaught: false, diff --git a/tests/integration/api/controllers/all-docs.spec.js b/tests/integration/api/controllers/all-docs.spec.js index f67f88b71cd..3df2c65bbd8 100644 --- a/tests/integration/api/controllers/all-docs.spec.js +++ b/tests/integration/api/controllers/all-docs.spec.js @@ -206,7 +206,6 @@ describe('all_docs handler', () => { const request = { method: 'POST', body: { keys }, - headers: { 'Content-Type': 'application/json' } }; return utils diff --git a/tests/integration/api/controllers/bulk-docs.spec.js b/tests/integration/api/controllers/bulk-docs.spec.js index d20c07a09a9..10cf7bb7fc6 100644 --- a/tests/integration/api/controllers/bulk-docs.spec.js +++ b/tests/integration/api/controllers/bulk-docs.spec.js @@ -286,13 +286,13 @@ describe('bulk-docs handler', () => { }).then(result => { chai.expect(result.length).to.equal(8); chai.expect(result[0]).excluding('_rev').to.deep.equal(docs[0]); - chai.expect(result[1]).to.deep.nested.include({ 'responseBody.error': 'not_found' }); + chai.expect(result[1]).to.deep.nested.include({ 'body.error': 'not_found' }); chai.expect(result[2]).excluding('_rev').to.deep.equal(existentDocs[2]); chai.expect(result[3]).excluding('_rev').to.deep.equal(existentDocs[3]); chai.expect(result[4]).excluding('_rev').to.deep.equal(existentDocs[0]); chai.expect(result[5]).excluding('_rev').to.deep.equal(docs[5]); - chai.expect(result[6]).to.deep.nested.include({ 'responseBody.error': 'not_found' }); + chai.expect(result[6]).to.deep.nested.include({ 'body.error': 'not_found' }); chai.expect(result[7]).excluding( ['_rev', '_id']).to.deep.equal(docs[7]); return sUtils.waitForSentinel(ids).then(() => sUtils.getInfoDocs(ids)); @@ -757,12 +757,12 @@ describe('bulk-docs handler', () => { chai.expect(results.length).to.equal(8); chai.expect(results[0]).excluding('_rev').to.deep.equal(docs[0]); - chai.expect(results[1]).to.deep.nested.include({ 'responseBody.error': 'not_found' }); + chai.expect(results[1]).to.deep.nested.include({ 'body.error': 'not_found' }); chai.expect(results[2]).excluding('_rev').to.deep.equal(existentDocs[2]); chai.expect(results[3]).excluding('_rev').to.deep.equal(existentDocs[3]); chai.expect(results[4]).excluding('_rev').to.deep.equal(existentDocs[0]); chai.expect(results[5]).excluding('_rev').to.deep.equal(docs[5]); - chai.expect(results[6]).to.deep.nested.include({ 'responseBody.error': 'not_found' }); + chai.expect(results[6]).to.deep.nested.include({ 'body.error': 'not_found' }); chai.expect(results[7]).excluding(['_rev', '_id']).to.deep.equal(docs[7]); }); }); @@ -804,7 +804,7 @@ describe('bulk-docs handler', () => { chai.expect(result[0]).to.include({ id: 'denied_report', error: 'forbidden' }); } else { // CouchDB interprets this as an attachment POST request - chai.expect(result).to.deep.nested.include({ 'responseBody.error': 'method_not_allowed' }); + chai.expect(result).to.deep.nested.include({ 'body.error': 'method_not_allowed' }); } }); }); @@ -862,9 +862,9 @@ describe('bulk-docs handler', () => { }) .then(results => { chai.expect(results[0]).to.deep.equal(docs[0]); - chai.expect(results[1]).to.include({ statusCode: 404 }); + chai.expect(results[1]).to.include({ status: 404 }); chai.expect(results[2]).to.deep.equal(docs[2]); - chai.expect(results[3]).to.include({ statusCode: 404 }); + chai.expect(results[3]).to.include({ status: 404 }); }); }); diff --git a/tests/integration/api/controllers/bulk-get.spec.js b/tests/integration/api/controllers/bulk-get.spec.js index cc23e31f647..d8d60701217 100644 --- a/tests/integration/api/controllers/bulk-get.spec.js +++ b/tests/integration/api/controllers/bulk-get.spec.js @@ -504,7 +504,7 @@ describe('bulk-get handler', () => { if (result.results) { chai.expect(result.results.length).to.equal(0); } else { - chai.expect(result.responseBody).to.equal('Server error'); + chai.expect(result.body).to.equal('Server error'); } }); }); diff --git a/tests/integration/api/controllers/changes.spec.js b/tests/integration/api/controllers/changes.spec.js index 9daab7da414..1c6d2791c32 100644 --- a/tests/integration/api/controllers/changes.spec.js +++ b/tests/integration/api/controllers/changes.spec.js @@ -214,8 +214,8 @@ describe('changes handler', () => { }) .then(response => { expect(response.headers).to.be.ok; - expect(response.headers['content-type']).to.equal('application/json'); - expect(response.headers.server).to.be.ok; + expect(response.headers.get('content-type')).to.equal('application/json'); + expect(response.headers.get('server')).to.be.ok; }); }); }); @@ -316,7 +316,7 @@ describe('changes handler', () => { if (result.results) { return assertChangeIds(result, ...changesIDs, bobUserId); } - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.body.error).to.equal('forbidden'); }); }); }); diff --git a/tests/integration/api/controllers/contacts-by-phone.spec.js b/tests/integration/api/controllers/contacts-by-phone.spec.js index 286f3c9e616..3ead3ec99c3 100644 --- a/tests/integration/api/controllers/contacts-by-phone.spec.js +++ b/tests/integration/api/controllers/contacts-by-phone.spec.js @@ -185,8 +185,8 @@ describe('Contacts by phone API', () => { .request(noAuthRequestOptions) .then(() => chai.assert.fail('Should not allow unauthenticated requests')) .catch(err => { - chai.expect(err.statusCode).to.equal(401); - chai.expect(err.error).to.deep.include({ code: 401, error: 'unauthorized' }); + chai.expect(err.status).to.equal(401); + chai.expect(err.body).to.deep.include({ code: 401, error: 'unauthorized' }); }); }); @@ -197,8 +197,8 @@ describe('Contacts by phone API', () => { .request(noAuthRequestOptions) .then(() => chai.assert.fail('Should not allow unauthenticated requests')) .catch(err => { - chai.expect(err.statusCode).to.equal(401); - chai.expect(err.error).to.deep.include({ code: 401, error: 'unauthorized' }); + chai.expect(err.status).to.equal(401); + chai.expect(err.body).to.deep.include({ code: 401, error: 'unauthorized' }); }); }); }); @@ -210,8 +210,8 @@ describe('Contacts by phone API', () => { .request(offlineRequestOptions) .then(() => chai.assert.fail('Should not allow offline users')) .catch(err => { - chai.expect(err.statusCode).to.equal(403); - chai.expect(err.error).to.deep.include({ code: 403, error: 'forbidden' }); + chai.expect(err.status).to.equal(403); + chai.expect(err.body).to.deep.include({ code: 403, error: 'forbidden' }); }); }); @@ -222,8 +222,8 @@ describe('Contacts by phone API', () => { .request(offlineRequestOptions) .then(() => chai.assert.fail('Should not allow offline users')) .catch(err => { - chai.expect(err.statusCode).to.equal(403); - chai.expect(err.error).to.deep.include({ code: 403, error: 'forbidden' }); + chai.expect(err.status).to.equal(403); + chai.expect(err.body).to.deep.include({ code: 403, error: 'forbidden' }); }); }); }); @@ -234,8 +234,8 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`phone` parameter is required and must be a valid phone number' }); @@ -248,8 +248,8 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`phone` parameter is required and must be a valid phone number' }); @@ -284,7 +284,7 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should 404 when not found')) .catch(result => { - chai.expect(result.error).to.deep.equal({ error: 'not_found', reason: 'no matches found' }); + chai.expect(result.body).to.deep.equal({ error: 'not_found', reason: 'no matches found' }); }); }); @@ -309,8 +309,8 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`phone` parameter is required and must be a valid phone number' }); @@ -323,8 +323,8 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail with incorrect params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`phone` parameter is required and must be a valid phone number' }); @@ -369,7 +369,7 @@ describe('Contacts by phone API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should 404 when not found')) .catch(result => { - chai.expect(result.error).to.deep.equal({ error: 'not_found', reason: 'no matches found' }); + chai.expect(result.body).to.deep.equal({ error: 'not_found', reason: 'no matches found' }); }); }); diff --git a/tests/integration/api/controllers/db-doc.spec.js b/tests/integration/api/controllers/db-doc.spec.js index 45b042fd03e..28bb8bca911 100644 --- a/tests/integration/api/controllers/db-doc.spec.js +++ b/tests/integration/api/controllers/db-doc.spec.js @@ -273,7 +273,7 @@ describe('db-doc handler', () => { return utils.getDoc('db_doc_delete'); }) .catch(err => { - chai.expect(err.responseBody.error).to.equal('not_found'); + chai.expect(err.body.error).to.equal('not_found'); }); }); @@ -344,10 +344,10 @@ describe('db-doc handler', () => { chai.expect(results[3]) .to.deep.include(patients.find(patient => patient._id === 'fixture:offline:clinic:patient')); - chai.expect(results[4]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[5]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[6]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[7]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[4]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[5]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[6]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[7]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -422,7 +422,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -456,7 +456,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -496,7 +496,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -655,7 +655,7 @@ describe('db-doc handler', () => { if (patientsToDelete[idx]._id.startsWith('temp:offline')) { chai.expect(result).to.deep.include(patientsToDelete[idx]); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }) @@ -668,7 +668,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc, idx); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}, idx); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}, idx); } }); }) @@ -683,7 +683,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc, idx); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}, idx); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}, idx); } }); }) @@ -700,7 +700,7 @@ describe('db-doc handler', () => { chai.expect(result).to.deep.include(reportScenarios[idx].doc); chai.expect(result._deleted).to.equal(true); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -776,7 +776,7 @@ describe('db-doc handler', () => { .then(results => { // cannot read patients results.forEach(result => { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }) .then(() => Promise.all(reportScenarios.map(scenario => utils.requestOnTestDb( @@ -788,7 +788,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }) @@ -801,7 +801,7 @@ describe('db-doc handler', () => { .then(results => { // cannot read deleted patients results.forEach(result => { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }) .then(() => Promise.all(reportScenarios.map(scenario => utils.requestOnTestDb( @@ -813,7 +813,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}, idx); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}, idx); } }); }) @@ -828,7 +828,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include(reportScenarios[idx].doc); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }) @@ -845,7 +845,7 @@ describe('db-doc handler', () => { chai.expect(result).to.deep.include(reportScenarios[idx].doc); chai.expect(result._deleted).to.equal(true); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1024,9 +1024,9 @@ describe('db-doc handler', () => { chai.expect(results[2]._revs_info.length).to.deep.equal(results[2]._revisions.ids.length); chai.expect(results[2]._revs_info[0]).to.deep.equal({ rev: revs.allowed_attach[1], status: 'available' }); - chai.expect(results[3].statusCode).to.deep.equal(403); - chai.expect(results[4].statusCode).to.deep.equal(403); - chai.expect(results[5].statusCode).to.deep.equal(403); + chai.expect(results[3].status).to.deep.equal(403); + chai.expect(results[4].status).to.deep.equal(403); + chai.expect(results[5].status).to.deep.equal(403); }); }); @@ -1076,9 +1076,9 @@ describe('db-doc handler', () => { ])) .then(results => { chai.expect(results[0]).to.deep.include(allowedTask); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); chai.expect(results[2]).to.deep.include(allowedTarget); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }) .then(() => Promise.all([ utils.requestOnTestDb(_.defaults({ path: '/fixture:user:offline' }, supervisorRequestOptions)), @@ -1097,13 +1097,13 @@ describe('db-doc handler', () => { // supervisor can see the user's contact chai.expect(results[0]._id).to.equal('fixture:user:offline'); // supervisor can't see the user's user-settings document - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); // supervisor has replication depth of 2 - chai.expect(results[2]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); // supervisor can't see the any user's tasks - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[4]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[4]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); // supervisor can see both targets chai.expect(results[5]).to.deep.include(allowedTarget); @@ -1137,7 +1137,7 @@ describe('db-doc handler', () => { .then(() => utils.requestOnTestDb(_.defaults({ path: `/${doc._id}` }, offlineRequestOptions)).catch(err => err)) .then(result => { // user can't see the unallocated report without permissions - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }) .then(() => utils.updateSettings(settings, { ignoreReload: true })) .then(() => utils.requestOnTestDb(_.defaults({ path: `/${doc._id}` }, offlineRequestOptions)).catch(err => err)) @@ -1253,7 +1253,7 @@ describe('db-doc handler', () => { chai.expect(result).excluding('_rev').to.deep.equal(originalDoc); } else { // a private report, expect an error - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1282,8 +1282,8 @@ describe('db-doc handler', () => { ]) .then(([allowed, denied, forbidden]) => { chai.expect(allowed).to.include({ id: 'allowed_doc_post', ok: true, }); - chai.expect(denied).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(forbidden).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(denied).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(forbidden).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); return Promise.all([ utils.getDoc('allowed_doc_post'), @@ -1292,7 +1292,7 @@ describe('db-doc handler', () => { }) .then(([allowed, denied]) => { chai.expect(allowed).to.deep.include(allowedDoc); - chai.expect(denied.statusCode).to.deep.equal(404); + chai.expect(denied.status).to.deep.equal(404); const ids = ['allowed_doc_post', 'denied_doc_post']; return sentinelUtils.waitForSentinel(ids).then(() => sentinelUtils.getInfoDocs(ids)); @@ -1385,7 +1385,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include({ ok: true, id: reportScenarios[idx].doc._id }); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1419,7 +1419,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include({ ok: true, id: reportScenarios[idx].doc._id }); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1469,9 +1469,9 @@ describe('db-doc handler', () => { ]) .then(results => { chai.expect(results[0]).to.deep.include({ ok: true, id: 'task1' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); chai.expect(results[2]).to.deep.include({ ok: true, id: 'target1' }); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -1534,12 +1534,12 @@ describe('db-doc handler', () => { }) .then(results => { chai.expect(results[0]).to.include({ ok: true, id: 'n_put_1' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); chai.expect(results[2]).to.include({ ok: true, id: 'a_put_1', }); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[4]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[5]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[4]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[5]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); const ids = ['a_put_1', 'a_put_2', 'd_put_1', 'd_put_2', 'n_put_1', 'n_put_2']; @@ -1607,10 +1607,10 @@ describe('db-doc handler', () => { .catch(err => err))); }) .then(results => { - chai.expect(results[0]).to.deep.nested.include({ statusCode: 409, 'responseBody.error': 'conflict'}); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[2]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 409, 'responseBody.error': 'conflict'}); + chai.expect(results[0]).to.deep.nested.include({ status: 409, 'body.error': 'conflict'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 409, 'body.error': 'conflict'}); }); }); @@ -1685,7 +1685,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include({ ok: true, id: reportScenarios[idx].doc._id }); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1719,7 +1719,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include({ ok: true, id: reportScenarios[idx].doc._id }); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1758,7 +1758,7 @@ describe('db-doc handler', () => { if (reportScenarios[idx].allowed) { chai.expect(result).to.deep.include({ ok: true, id: reportScenarios[idx].doc._id }); } else { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); } }); }); @@ -1812,9 +1812,9 @@ describe('db-doc handler', () => { }) .then(results => { chai.expect(results[0]).to.deep.include({ ok: true, id: 'task1' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); chai.expect(results[2]).to.deep.include({ ok: true, id: 'target1' }); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -1843,7 +1843,7 @@ describe('db-doc handler', () => { ])) .then(results => { chai.expect(results[0]).to.deep.include({ id: 'allowed_del', ok: true }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); return Promise.all([ utils.getDoc('allowed_del').catch(err => err), @@ -1852,7 +1852,7 @@ describe('db-doc handler', () => { }) .then(results => { chai.expect(results[0]).to.deep.include({ - statusCode: 404, responseBody: { error: 'not_found', reason: 'deleted' } + status: 404, body: { error: 'not_found', reason: 'deleted' } }); chai.expect(results[1]).to.deep.include({ _id: 'denied_del', @@ -1915,10 +1915,10 @@ describe('db-doc handler', () => { .then(results => { chai.expect(results[0]).to.equal('my attachment content'); chai.expect(results[1]).to.deep.include( - { statusCode: 404, responseBody: { error: 'bad_request', reason: 'Invalid rev format' }} + { status: 404, body: { error: 'bad_request', reason: 'Invalid rev format' }} ); chai.expect(results[2]).to.deep.include( - { statusCode: 403, responseBody: { error: 'forbidden', reason: 'Insufficient privileges' }} + { status: 403, body: { error: 'forbidden', reason: 'Insufficient privileges' }} ); return Promise.all([ @@ -1945,19 +1945,19 @@ describe('db-doc handler', () => { }) .then(results => { // allowed_attach is allowed, but missing attachment - chai.expect(results[0].responseBody).to.deep.equal({ + chai.expect(results[0].body).to.deep.equal({ error: 'not_found', reason: 'Document is missing attachment', }); // allowed_attach is allowed and has attachment chai.expect(results[1]).to.equal('my attachment content'); // allowed_attach is not allowed and has attachment - chai.expect(results[2].responseBody.error).to.equal('forbidden'); + chai.expect(results[2].body.error).to.equal('forbidden'); // denied_attach is not allowed, but missing attachment - chai.expect(results[3].responseBody.error).to.equal('forbidden'); + chai.expect(results[3].body.error).to.equal('forbidden'); // denied_attach is not allowed and has attachment - chai.expect(results[4].responseBody.error).to.equal('forbidden'); + chai.expect(results[4].body.error).to.equal('forbidden'); // denied_attach is allowed and has attachment chai.expect(results[5]).to.equal('my attachment content'); @@ -1987,9 +1987,9 @@ describe('db-doc handler', () => { ]); }) .then(results => { - chai.expect(results[0]).to.deep.nested.include({ statusCode: 404, 'responseBody.error': 'bad_request' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[2]).to.deep.nested.include({ statusCode: 404, 'responseBody.error': 'bad_request' }); + chai.expect(results[0]).to.deep.nested.include({ status: 404, 'body.error': 'bad_request' }); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 404, 'body.error': 'bad_request' }); chai.expect(results[3]).to.equal('my attachment content'); }); }); @@ -2050,8 +2050,8 @@ describe('db-doc handler', () => { chai.expect(results[0]).to.equal('my attachment content'); chai.expect(results[1]).to.equal('my attachment content'); - chai.expect(results[2]).to.deep.nested.include({ statusCode: 404, 'responseBody.error': 'bad_request' }); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 404, 'body.error': 'bad_request' }); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); return Promise.all([ utils.getDoc('allowed_attach_1'), @@ -2078,18 +2078,18 @@ describe('db-doc handler', () => { }) .then(results => { // allowed_attach is allowed, but missing attachment - chai.expect(results[0].responseBody).to.deep.equal({ + chai.expect(results[0].body).to.deep.equal({ error: 'not_found', reason: 'Document is missing attachment', }); // allowed_attach is allowed and has attachment chai.expect(results[1]).to.equal('my attachment content'); // allowed_attach is not allowed and has attachment - chai.expect(results[2].responseBody.error).to.equal('forbidden'); + chai.expect(results[2].body.error).to.equal('forbidden'); // denied_attach is not allowed, but missing attachment - chai.expect(results[3].responseBody.error).to.equal('forbidden'); + chai.expect(results[3].body.error).to.equal('forbidden'); // denied_attach is not allowed and has attachment - chai.expect(results[4].responseBody.error).to.equal('forbidden'); + chai.expect(results[4].body.error).to.equal('forbidden'); // denied_attach is allowed and has attachment chai.expect(results[5]).to.equal('my attachment content'); }); @@ -2127,7 +2127,7 @@ describe('db-doc handler', () => { )) .then(results => { chai.expect(results[0]).to.deep.include({ ok: true, id: 'a_with_attachments' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); return Promise.all([ utils.requestOnTestDb({ path: '/a_with_attachments' }), @@ -2145,7 +2145,7 @@ describe('db-doc handler', () => { chai.expect(results[2]._attachments).to.be.undefined; chai.expect(results[2]._id).to.equal('d_with_attachments'); - chai.expect(results[3].responseBody.error).to.equal('not_found'); + chai.expect(results[3].body.error).to.equal('not_found'); }); }); }); @@ -2164,7 +2164,7 @@ describe('db-doc handler', () => { type: 'person', parent: { _id: 'fixture:offline', parent: { _id: 'PARENT_PLACE' } }, }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -2218,11 +2218,11 @@ describe('db-doc handler', () => { }) .then(results => { chai.expect(results[0]).to.deep.include({ ok: true, id: 'n_put_1' }); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); chai.expect(results[2]).to.deep.include({ ok: true, id: 'a_put_1' }); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[4]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[5]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[4]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[5]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -2306,7 +2306,7 @@ describe('db-doc handler', () => { .catch(err => err), ])) .then(results => { - chai.expect(results.every(result => result.statusCode === 403 || result.statusCode === 404)).to.equal(true); + chai.expect(results.every(result => result.status === 403 || result.status === 404)).to.equal(true); }); }); @@ -2347,7 +2347,7 @@ describe('db-doc handler', () => { return utils.requestOnTestDb(offlineRequestOptions).catch(err => err); }) .then(result => { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); return utils.getDoc('fb1'); }) .then(result => { @@ -2368,9 +2368,9 @@ describe('db-doc handler', () => { .then(results => { chai.expect(results[0]._id).to.equal('_design/medic-client'); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[2]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -2392,10 +2392,10 @@ describe('db-doc handler', () => { .catch(err => err), ]) .then(results => { - chai.expect(results[0]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[1]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[2]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); - chai.expect(results[3]).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(results[0]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[1]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[2]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); + chai.expect(results[3]).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); @@ -2425,7 +2425,7 @@ describe('db-doc handler', () => { ]) .then(results => { results.forEach(result => { - chai.expect(result).to.deep.nested.include({ statusCode: 403, 'responseBody.error': 'forbidden'}); + chai.expect(result).to.deep.nested.include({ status: 403, 'body.error': 'forbidden'}); }); }); }); diff --git a/tests/integration/api/controllers/hydration.spec.js b/tests/integration/api/controllers/hydration.spec.js index df74fee36da..a32e9668a40 100644 --- a/tests/integration/api/controllers/hydration.spec.js +++ b/tests/integration/api/controllers/hydration.spec.js @@ -269,8 +269,8 @@ describe('Hydration API', () => { .request(noAuthRequestOptions) .then(() => chai.assert.fail('Should not allow unauthenticated requests')) .catch(err => { - chai.expect(err.statusCode).to.equal(401); - chai.expect(err.error).to.deep.include({ code: 401, error: 'unauthorized' }); + chai.expect(err.status).to.equal(401); + chai.expect(err.body).to.deep.include({ code: 401, error: 'unauthorized' }); }); }); @@ -281,8 +281,8 @@ describe('Hydration API', () => { .request(noAuthRequestOptions) .then(() => chai.assert.fail('Should not allow unauthenticated requests')) .catch(err => { - chai.expect(err.statusCode).to.equal(401); - chai.expect(err.error).to.deep.include({ code: 401, error: 'unauthorized' }); + chai.expect(err.status).to.equal(401); + chai.expect(err.body).to.deep.include({ code: 401, error: 'unauthorized' }); }); }); }); @@ -294,8 +294,8 @@ describe('Hydration API', () => { .request(offlineRequestOptions) .then(() => chai.assert.fail('Should not allow offline users')) .catch(err => { - chai.expect(err.statusCode).to.equal(403); - chai.expect(err.error).to.deep.include({ code: 403, error: 'forbidden' }); + chai.expect(err.status).to.equal(403); + chai.expect(err.body).to.deep.include({ code: 403, error: 'forbidden' }); }); }); @@ -306,8 +306,8 @@ describe('Hydration API', () => { .request(offlineRequestOptions) .then(() => chai.assert.fail('Should not allow offline users')) .catch(err => { - chai.expect(err.statusCode).to.equal(403); - chai.expect(err.error).to.deep.include({ code: 403, error: 'forbidden' }); + chai.expect(err.status).to.equal(403); + chai.expect(err.body).to.deep.include({ code: 403, error: 'forbidden' }); }); }); }); @@ -318,8 +318,8 @@ describe('Hydration API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`doc_ids` parameter must be a json array.' }); @@ -332,8 +332,8 @@ describe('Hydration API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`doc_ids` parameter must be a json array.' }); @@ -480,8 +480,8 @@ describe('Hydration API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`doc_ids` parameter must be a json array.' }); @@ -494,8 +494,8 @@ describe('Hydration API', () => { .request(onlineRequestOptions) .then(() => chai.assert.fail('Should fail when no params')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); - chai.expect(err.error).to.deep.equal({ + chai.expect(err.status).to.equal(400); + chai.expect(err.body).to.deep.equal({ error: 'bad_request', reason: '`doc_ids` parameter must be a json array.' }); diff --git a/tests/integration/api/controllers/login.spec.js b/tests/integration/api/controllers/login.spec.js index dc9be54a07e..c14a19c86be 100644 --- a/tests/integration/api/controllers/login.spec.js +++ b/tests/integration/api/controllers/login.spec.js @@ -20,10 +20,10 @@ const loginWithData = data => { const opts = { path: '/medic/login?aaa=aaa', method: 'POST', - simple: false, + resolveWithFullResponse: true, noAuth: true, body: data, - followRedirect: false, + redirect: 'manual', headers: { 'X-Forwarded-For': randomIp() }, }; return utils.request(opts); @@ -33,10 +33,9 @@ const loginWithTokenLink = (token = '') => { const opts = { path: `/medic/login/token/${token}`, method: 'POST', - simple: false, resolveWithFullResponse: true, noAuth: true, - followRedirect: false, + redirect: 'manual', body: {}, headers: { 'X-Forwarded-For': randomIp() }, }; @@ -44,16 +43,16 @@ const loginWithTokenLink = (token = '') => { }; const expectLoginToWork = (response) => { - chai.expect(response).to.include({ statusCode: 302 }); - chai.expect(response.headers['set-cookie']).to.be.an('array'); - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('userCtx'))).to.be.ok; + chai.expect(response).to.include({ status: 302 }); + chai.expect(response.headers.getSetCookie()).to.be.an('array'); + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('userCtx'))).to.be.ok; chai.expect(response.body).to.equal('/'); }; const expectLoginToFail = (response) => { - chai.expect(response.headers['set-cookie']).to.be.undefined; - chai.expect(response.statusCode).to.equal(401); + chai.expect(response.headers.getSetCookie()).to.deep.equal([]); + chai.expect(response.status).to.equal(401); }; const getUser = (user) => { @@ -135,7 +134,7 @@ describe('login', () => { it('should fail with invalid url', () => { return setupTokenLoginSettings() .then(() => loginWithTokenLink()) - .then(response => chai.expect(response).to.deep.include({ statusCode: 401 })); + .then(response => chai.expect(response).to.deep.include({ status: 401 })); }); it('should fail with invalid data', () => { diff --git a/tests/integration/api/controllers/person.spec.js b/tests/integration/api/controllers/person.spec.js index d35b93d179d..fa21dc6f22e 100644 --- a/tests/integration/api/controllers/person.spec.js +++ b/tests/integration/api/controllers/person.spec.js @@ -1,8 +1,6 @@ const utils = require('@utils'); const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); -const { getRemoteDataContext, Person, Qualifier } = require('@medic/cht-datasource'); -const { expect } = require('chai'); const userFactory = require('@factories/cht/users/users'); describe('Person API', () => { @@ -47,7 +45,6 @@ describe('Person API', () => { roles: ['chw'] })); const allDocItems = [contact0, contact1, contact2, place0, place1, place2, patient]; - const dataContext = getRemoteDataContext(utils.getOrigin()); const personType = 'person'; const e2eTestUser = { '_id': 'e2e_contact_test_id', @@ -96,16 +93,13 @@ describe('Person API', () => { }); describe('GET /api/v1/person/:uuid', async () => { - const getPerson = Person.v1.get(dataContext); - const getPersonWithLineage = Person.v1.getWithLineage(dataContext); - it('returns the person matching the provided UUID', async () => { - const person = await getPerson(Qualifier.byUuid(patient._id)); + const person = await utils.request(`/api/v1/person/${patient._id}`); expect(person).excluding(['_rev', 'reported_date']).to.deep.equal(patient); }); it('returns the person with lineage when the withLineage query parameter is provided', async () => { - const person = await getPersonWithLineage(Qualifier.byUuid(patient._id)); + const person = await utils.request({ path: `/api/v1/person/${patient._id}`, qs: { with_lineage: true } }); expect(person).excludingEvery(['_rev', 'reported_date']).to.deep.equal({ ...patient, parent: { @@ -124,8 +118,8 @@ describe('Person API', () => { }); it('returns null when no person is found for the UUID', async () => { - const person = await getPerson(Qualifier.byUuid('invalid-uuid')); - expect(person).to.be.null; + await expect(utils.request('/api/v1/person/invalid-uuid')) + .to.be.rejectedWith('404 - {"code":404,"error":"Person not found"}'); }); [ @@ -143,13 +137,11 @@ describe('Person API', () => { }); describe('GET /api/v1/person', async () => { - const getPage = Person.v1.getPage(dataContext); const limit = 4; - const cursor = null; const invalidContactType = 'invalidPerson'; it('returns a page of people for no limit and cursor passed', async () => { - const responsePage = await getPage(Qualifier.byContactType(personType)); + const responsePage = await utils.request({ path: `/api/v1/person`, qs: { type: personType } }); const responsePeople = responsePage.data; const responseCursor = responsePage.cursor; @@ -158,8 +150,11 @@ describe('Person API', () => { }); it('returns a page of people when limit and cursor is passed and cursor can be reused', async () => { - const firstPage = await getPage(Qualifier.byContactType(personType), cursor, limit); - const secondPage = await getPage(Qualifier.byContactType(personType), firstPage.cursor, limit); + const firstPage = await utils.request({ path: `/api/v1/person`, qs: { type: personType, limit } }); + const secondPage = await utils.request({ + path: `/api/v1/person`, + qs: { type: personType, cursor: firstPage.cursor, limit } + }); const allPeople = [...firstPage.data, ...secondPage.data]; @@ -230,17 +225,18 @@ describe('Person API', () => { }); }); - describe('Person.v1.getAll', async () => { - it('fetches all data by iterating through generator', async () => { - const docs = []; - - const generator = Person.v1.getAll(dataContext)(Qualifier.byContactType(personType)); - - for await (const doc of generator) { - docs.push(doc); - } - - expect(docs).excluding(['_rev', 'reported_date']).to.deep.equalInAnyOrder(expectedPeople); - }); - }); + // todo rethink this once datasource works with authentication #9701 + // describe('Person.v1.getAll', async () => { + // it('fetches all data by iterating through generator', async () => { + // const docs = []; + // + // const generator = Person.v1.getAll(dataContext)(Qualifier.byContactType(personType)); + // + // for await (const doc of generator) { + // docs.push(doc); + // } + // + // expect(docs).excluding(['_rev', 'reported_date']).to.deep.equalInAnyOrder(expectedPeople); + // }); + // }); }); diff --git a/tests/integration/api/controllers/place.spec.js b/tests/integration/api/controllers/place.spec.js index d80ed338461..962144d403f 100644 --- a/tests/integration/api/controllers/place.spec.js +++ b/tests/integration/api/controllers/place.spec.js @@ -1,8 +1,6 @@ const utils = require('@utils'); const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); -const { getRemoteDataContext, Place, Qualifier } = require('@medic/cht-datasource'); -const { expect } = require('chai'); const userFactory = require('@factories/cht/users/users'); describe('Place API', () => { @@ -62,7 +60,6 @@ describe('Place API', () => { }, roles: ['chw'] })); - const dataContext = getRemoteDataContext(utils.getOrigin()); const expectedPlaces = [place0, clinic1, clinic2]; before(async () => { @@ -76,16 +73,13 @@ describe('Place API', () => { }); describe('GET /api/v1/place/:uuid', async () => { - const getPlace = Place.v1.get(dataContext); - const getPlaceWithLineage = Place.v1.getWithLineage(dataContext); - it('returns the place matching the provided UUID', async () => { - const place = await getPlace(Qualifier.byUuid(place0._id)); + const place = await utils.request(`/api/v1/place/${place0._id}`); expect(place).excluding(['_rev', 'reported_date']).to.deep.equal(place0); }); it('returns the place with lineage when the withLineage query parameter is provided', async () => { - const place = await getPlaceWithLineage(Qualifier.byUuid(place0._id)); + const place = await utils.request({ path: `/api/v1/place/${place0._id}`, qs: { with_lineage: true } }); expect(place).excludingEvery(['_rev', 'reported_date']).to.deep.equal({ ...place0, contact: contact0, @@ -101,8 +95,8 @@ describe('Place API', () => { }); it('returns null when no place is found for the UUID', async () => { - const place = await getPlace(Qualifier.byUuid('invalid-uuid')); - expect(place).to.be.null; + await expect(utils.request('/api/v1/place/invalid-uuid')) + .to.be.rejectedWith('404 - {"code":404,"error":"Place not found"}'); }); [ @@ -120,13 +114,11 @@ describe('Place API', () => { }); describe('GET /api/v1/place', async () => { - const getPage = Place.v1.getPage(dataContext); const limit = 2; - const cursor = null; const invalidContactType = 'invalidPlace'; it('returns a page of places for no limit and cursor passed', async () => { - const responsePage = await getPage(Qualifier.byContactType(placeType)); + const responsePage = await utils.request({ path: `/api/v1/place`, qs: { type: placeType } }); const responsePlaces = responsePage.data; const responseCursor = responsePage.cursor; @@ -136,8 +128,11 @@ describe('Place API', () => { }); it('returns a page of places when limit and cursor is passed and cursor can be reused', async () => { - const firstPage = await getPage(Qualifier.byContactType(placeType), cursor, limit); - const secondPage = await getPage(Qualifier.byContactType(placeType), firstPage.cursor, limit); + const firstPage = await utils.request({ path: `/api/v1/place`, qs: { type: placeType, limit } }); + const secondPage = await utils.request({ + path: `/api/v1/place`, + qs: { type: placeType, cursor: firstPage.cursor, limit } + }); const allPeople = [...firstPage.data, ...secondPage.data]; @@ -208,17 +203,18 @@ describe('Place API', () => { }); }); - describe('Place.v1.getAll', async () => { - it('fetches all data by iterating through generator', async () => { - const docs = []; - - const generator = Place.v1.getAll(dataContext)(Qualifier.byContactType(placeType)); - - for await (const doc of generator) { - docs.push(doc); - } - - expect(docs).excluding(['_rev', 'reported_date']).to.deep.equalInAnyOrder(expectedPlaces); - }); - }); + // todo rethink this once datasource works with authentication #9701 + // describe('Place.v1.getAll', async () => { + // it('fetches all data by iterating through generator', async () => { + // const docs = []; + // + // const generator = Place.v1.getAll(dataContext)(Qualifier.byContactType(placeType)); + // + // for await (const doc of generator) { + // docs.push(doc); + // } + // + // expect(docs).excluding(['_rev', 'reported_date']).to.deep.equalInAnyOrder(expectedPlaces); + // }); + // }); }); diff --git a/tests/integration/api/controllers/places.spec.js b/tests/integration/api/controllers/places.spec.js index 3d0862b4ef6..d607da09363 100644 --- a/tests/integration/api/controllers/places.spec.js +++ b/tests/integration/api/controllers/places.spec.js @@ -205,7 +205,7 @@ describe('Places API', () => { return utils.request(onlineRequestOptions) .then(() => fail('Call should fail as contact type is not a person')) .catch(err => { - chai.expect(err.responseBody.error).to.equal('Wrong type, this is not a person.'); + chai.expect(err.body.error).to.equal('Wrong type, this is not a person.'); }); }); @@ -220,7 +220,7 @@ describe('Places API', () => { return utils.request(onlineRequestOptions) .then(() => fail('Call should fail as contact does not exist')) .catch(err => { - chai.expect(err.responseBody.error).to.equal('Failed to find person.'); + chai.expect(err.body.error).to.equal('Failed to find person.'); }); }); diff --git a/tests/integration/api/controllers/records.spec.js b/tests/integration/api/controllers/records.spec.js index 97c2434cee5..bcea32cfd9a 100644 --- a/tests/integration/api/controllers/records.spec.js +++ b/tests/integration/api/controllers/records.spec.js @@ -46,9 +46,6 @@ describe('Import Records', () => { .then(() => utils.request({ method: 'POST', path: '/api/v2/records', - headers: { - 'Content-type': 'application/json' - }, body: { _meta: { form: 'TEST', @@ -94,9 +91,6 @@ describe('Import Records', () => { .then(() => utils.request({ method: 'POST', path: '/api/v2/records', - headers: { - 'Content-type': 'application/json' - }, body: { _meta: { form: 'TEST' @@ -134,9 +128,6 @@ describe('Import Records', () => { .then(() => utils.request({ method: 'POST', path: '/api/v2/records', - headers: { - 'Content-type': 'application/json' - }, body: { _meta: { form: 'TEST', diff --git a/tests/integration/api/controllers/settings.spec.js b/tests/integration/api/controllers/settings.spec.js index c4bda666dc9..643a2ba01a9 100644 --- a/tests/integration/api/controllers/settings.spec.js +++ b/tests/integration/api/controllers/settings.spec.js @@ -29,7 +29,6 @@ describe('Settings API', () => { path: uri, method: 'PUT', body: updates, - headers: { 'Content-Type': 'application/json' } }); }; @@ -124,7 +123,6 @@ describe('Settings API', () => { path: '/api/v1/settings', method: 'PUT', body: updates, - headers: { 'Content-Type': 'application/json' }, qs, }); }; diff --git a/tests/integration/api/controllers/users.spec.js b/tests/integration/api/controllers/users.spec.js index cea1f61b179..a129526f3ab 100644 --- a/tests/integration/api/controllers/users.spec.js +++ b/tests/integration/api/controllers/users.spec.js @@ -1,10 +1,6 @@ -const constants = require('@constants'); -const https = require('https'); const utils = require('@utils'); const uuid = require('uuid').v4; const querystring = require('querystring'); -const chai = require('chai'); -chai.use(require('chai-shallow-deep-equal')); const sentinelUtils = require('@utils/sentinel'); const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); @@ -29,10 +25,10 @@ describe('Users API', () => { const opts = { path: '/login', method: 'POST', - simple: false, + resolveWithFullResponse: true, noAuth: true, body: { user: user.username, password: user.password }, - followRedirect: false, + redirect: 'manual', headers: { 'X-Forwarded-For': randomIp() }, }; @@ -40,12 +36,12 @@ describe('Users API', () => { .requestOnMedicDb(opts) .then(response => { chai.expect(response).to.include({ - statusCode: 302, + status: 302, body: '/', }); - chai.expect(response.headers['set-cookie']).to.be.an('array'); - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('userCtx'))).to.be.ok; + chai.expect(response.headers.getSetCookie()).to.be.an('array'); + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('userCtx'))).to.be.ok; }); }; @@ -53,7 +49,7 @@ describe('Users API', () => { const opts = { path: '/login', method: 'POST', - simple: false, + resolveWithFullResponse: true, noAuth: true, body: { user: user.username, password: user.password }, headers: { 'X-Forwarded-For': randomIp() }, @@ -62,7 +58,7 @@ describe('Users API', () => { return utils .requestOnMedicDb(opts) .then(response => { - chai.expect(response).to.deep.include({ statusCode: 401, body: { error: 'Not logged in' } }); + chai.expect(response).to.deep.include({ status: 401, body: { error: 'Not logged in' } }); }); }; @@ -119,61 +115,33 @@ describe('Users API', () => { await utils.request({ path: '/_users', method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, body: _usersUser }); await utils.saveDocs(medicData); - return new Promise((resolve, reject) => { - const options = { - hostname: constants.API_HOST, - path: '/_session', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - auth: `${username}:${password}` - }; - - // Use http service to extract cookie - const req = https.request(options, res => { - if (res.statusCode !== 200) { - return reject(new Error(`Expected 200 from _session authing, but got ${res.statusCode}`)); - } - - // Example header: - // AuthSession=cm9vdDo1MEJDMDEzRTp7Vu5GKCkTxTVxwXbpXsBARQWnhQ; Version=1; Path=/; HttpOnly - try { - cookie = res.headers['set-cookie'][0].match(/^(AuthSession=[^;]+)/)[0]; - } catch (err) { - return reject(err); - } - - resolve(cookie); - }); - - req.write(JSON.stringify({ + const res = await utils.request({ + path: '/_session', + auth: { username, password }, + method: 'POST', + resolveWithFullResponse: true, + body: { name: username, password: password - })); - req.end(); + } }); + if (!res.ok) { + throw new Error(`Expected 200 from _session authing, but got ${res.status}`); + } + + cookie = res.headers.getSetCookie().find(cookie => cookie.startsWith('AuthSession')); + if (!cookie) { + throw new Error('Expected auth cookie from _session authing'); + } }); after(async () => { - const { _rev } = await utils.request(`/_users/${getUserId(username)}`); - await utils.request({ - path: `/_users/${getUserId(username)}`, - method: 'PUT', - body: { - _id: getUserId(username), - _rev, - _deleted: true, - } - }); + await utils.deleteUsers([{ username }]); await utils.revertSettings(true); await utils.revertDb([], true); }); @@ -214,7 +182,7 @@ describe('Users API', () => { }) .then(() => fail('You should get a 401 in this situation')) .catch(err => { - chai.expect(err.responseBody.error).to.equal('You do not have permissions to modify this person'); + chai.expect(err.body.error).to.equal('You do not have permissions to modify this person'); }); }); @@ -230,7 +198,7 @@ describe('Users API', () => { }) .then(() => fail('You should get an error in this situation')) .catch(err => { - chai.expect(err.responseBody.error).to.equal('unauthorized'); + chai.expect(err.body.error).to.equal('unauthorized'); }); }); @@ -268,7 +236,7 @@ describe('Users API', () => { }) .then(() => fail('You should get an error in this situation')) .catch(err => { - chai.expect(err.responseBody.error).to.equal('You must authenticate with Basic Auth to modify your password'); + chai.expect(err.body.error).to.equal('You must authenticate with Basic Auth to modify your password'); }); }); @@ -516,7 +484,7 @@ describe('Users API', () => { .then(() => chai.expect.fail('should have thrown')) .catch(err => { // online users require the "can_update_users" permission to be able to access this endpoint - chai.expect(err.error).to.deep.equal({ + chai.expect(err.body).to.deep.equal({ code: 403, error: 'Insufficient privileges', }); @@ -535,7 +503,7 @@ describe('Users API', () => { .then(() => chai.expect.fail('should have thrown')) .catch(err => { // online users require the "can_update_users" permission to be able to access this endpoint - chai.expect(err.error).to.deep.equal({ + chai.expect(err.body).to.deep.equal({ code: 403, error: 'Insufficient privileges', }); @@ -580,12 +548,11 @@ describe('Users API', () => { facility_id: 'fixture:offline' }; onlineRequestOptions.path += '?' + querystring.stringify(params); - onlineRequestOptions.headers = { 'Content-Type': 'application/json' }; return utils .request(onlineRequestOptions) .then(resp => chai.expect(resp).to.equal('should have thrown')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); + chai.expect(err.status).to.equal(400); }); }); @@ -599,7 +566,7 @@ describe('Users API', () => { .request(onlineRequestOptions) .then(resp => chai.expect(resp).to.equal('should have thrown')) .catch(err => { - chai.expect(err.statusCode).to.equal(400); + chai.expect(err.status).to.equal(400); }); }); @@ -609,7 +576,6 @@ describe('Users API', () => { facility_id: 'IdonTExist' }; onlineRequestOptions.path += '?' + querystring.stringify(params); - onlineRequestOptions.headers = { 'Content-Type': 'application/json' }; return utils .request(onlineRequestOptions) .then(resp => { @@ -682,18 +648,17 @@ describe('Users API', () => { const opts = { uri: url, method: 'POST', - simple: false, resolveWithFullResponse: true, noAuth: true, - followRedirect: false, + redirect: 'manual', body: {}, headers: { 'X-Forwarded-For': randomIp() }, }; return utils.request(opts).then(response => { - chai.expect(response).to.include({ statusCode: 302, body: '/' }); - chai.expect(response.headers['set-cookie']).to.be.an('array'); - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; - chai.expect(response.headers['set-cookie'].find(cookie => cookie.startsWith('userCtx'))).to.be.ok; + chai.expect(response).to.include({ status: 302, body: '/' }); + chai.expect(response.headers.getSetCookie()).to.be.an('array'); + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('AuthSession'))).to.be.ok; + chai.expect(response.headers.getSetCookie().find(cookie => cookie.startsWith('userCtx'))).to.be.ok; }); }; @@ -701,16 +666,15 @@ describe('Users API', () => { const opts = { uri: url, method: 'POST', - simple: false, noAuth: true, - followRedirect: false, + redirect: 'manual', resolveWithFullResponse: true, body: {}, headers: { 'X-Forwarded-For': randomIp() }, }; return utils.request(opts).then(response => { - chai.expect(response.headers['set-cookie']).to.be.undefined; - chai.expect(response).to.deep.include({ statusCode: 401, body: { error: expired ? 'expired': 'invalid' } }); + chai.expect(response.headers.getSetCookie()).to.deep.equal([]); + chai.expect(response).to.deep.include({ status: 401, body: { error: expired ? 'expired': 'invalid' } }); }); }; @@ -1253,8 +1217,8 @@ describe('Users API', () => { }) .then(() => chai.assert.fail('should have thrown')) .catch(err => { - chai.expect(err.response).to.shallowDeepEqual({ - statusCode: 400, + chai.expect(err).to.shallowDeepEqual({ + status: 400, body: { code: 400, error: { message: 'Missing required fields: phone' }} }); }); @@ -1273,8 +1237,8 @@ describe('Users API', () => { }) .then(() => chai.assert.fail('should have thrown')) .catch(err => { - chai.expect(err.response).to.shallowDeepEqual({ - statusCode: 400, + chai.expect(err).to.shallowDeepEqual({ + status: 400, body: { code: 400, error: { message: 'Missing required fields: phone' }} }); @@ -1610,7 +1574,7 @@ describe('Users API', () => { chai.expect(tokenLoginDoc.user).to.equal('org.couchdb.user:testuser'); const onlineRequestOpts = { - auth: { user: 'onlineuser', password }, + auth: { username: 'onlineuser', password }, method: 'PUT', path: `/${tokenLoginDoc._id}`, body: tokenLoginDoc, @@ -1618,8 +1582,8 @@ describe('Users API', () => { return utils.requestOnTestDb(onlineRequestOpts).catch(err => err); }) .then(err => { - chai.expect(err.response).to.deep.include({ - statusCode: 403, + chai.expect(err).to.deep.include({ + status: 403, body: { error: 'forbidden', reason: 'Insufficient privileges' @@ -1694,9 +1658,9 @@ describe('Users API', () => { await utils.request({ path: `/api/v2/users/invalidUsername`, }); - } catch ({ error }) { - expect(error.code).to.equal(404); - expect(error.error).to + } catch ({ body }) { + expect(body.code).to.equal(404); + expect(body.error).to .match(/Failed to find user with name \[invalidUsername\] in the \[(users|medic)\] database./); return; } @@ -1710,9 +1674,9 @@ describe('Users API', () => { path: `/api/v2/users/${userProgramOfficer.username}`, auth: { username: user.username, password }, }); - } catch ({ error }) { - expect(error.code).to.equal(403); - expect(error.error).to.equal('Insufficient privileges'); + } catch ({ body }) { + expect(body.code).to.equal(403); + expect(body.error).to.equal('Insufficient privileges'); return; } @@ -2012,8 +1976,8 @@ describe('Users API', () => { await utils.request({ path: '/api/v3/users', method: 'POST', body: offlineUserPayload }); expect.fail('Should have thrown'); } catch (error) { - expect(error.statusCode).to.equal(400); - expect(error.error.error.message).to.equal('This user cannot have multiple places'); + expect(error.status).to.equal(400); + expect(error.body.error.message).to.equal('This user cannot have multiple places'); } }); @@ -2072,8 +2036,8 @@ describe('Users API', () => { await utils.request({ path: '/api/v3/users', method: 'POST', body: onlineUserPayload }); expect.expect.fail('Should have thrown'); } catch (err) { - expect(err.responseBody.code).to.equal(400); - expect(err.responseBody.error.message).to.equal('Invalid facilities list'); + expect(err.body.code).to.equal(400); + expect(err.body.error.message).to.equal('Invalid facilities list'); } const offlineUserPayload = { @@ -2088,8 +2052,8 @@ describe('Users API', () => { await utils.request({ path: '/api/v3/users', method: 'POST', body: offlineUserPayload }); expect.expect.fail('Should have thrown'); } catch (err) { - expect(err.responseBody.code).to.equal(400); - expect(err.responseBody.error.message).to.equal('Missing required fields: place'); + expect(err.body.code).to.equal(400); + expect(err.body.error.message).to.equal('Missing required fields: place'); } }); }); diff --git a/tests/integration/api/controllers/well-known.spec.js b/tests/integration/api/controllers/well-known.spec.js index 6d77886ce78..a27d0bf94ea 100644 --- a/tests/integration/api/controllers/well-known.spec.js +++ b/tests/integration/api/controllers/well-known.spec.js @@ -17,7 +17,7 @@ describe('well-known', () => { }) .then(() => chai.expect.fail('should have thrown')) .catch(error => { - chai.expect(error.response.statusCode).to.equal(404); + chai.expect(error.status).to.equal(404); }); }); diff --git a/tests/integration/api/rate-limit.spec.js b/tests/integration/api/rate-limit.spec.js index 677ff00bf2e..f7bb6791404 100644 --- a/tests/integration/api/rate-limit.spec.js +++ b/tests/integration/api/rate-limit.spec.js @@ -18,7 +18,7 @@ describe('rate limit', () => { await requestThat401s(); expect.fail('should have been rate limited'); } catch (e) { - expect(e.statusCode).to.equal(429); + expect(e.status).to.equal(429); } await new Promise((resolve) => { @@ -30,7 +30,7 @@ describe('rate limit', () => { expect.fail('should have rejected due to no auth'); } catch (e) { // the rate limit period has passed, so we're back to 401s - expect(e.statusCode).to.equal(401); + expect(e.status).to.equal(401); } }); diff --git a/tests/integration/api/routing.spec.js b/tests/integration/api/routing.spec.js index 4191f742682..affb0cae7cb 100644 --- a/tests/integration/api/routing.spec.js +++ b/tests/integration/api/routing.spec.js @@ -142,9 +142,9 @@ describe('routing', () => { .request(options) .catch(err => err) .then(result => { - expect(result.statusCode).to.equal(401); - expect(result.response.headers['logout-authorization']).to.equal('CHT-Core API'); - expect(result.responseBody.error).to.equal('unauthorized'); + expect(result.status).to.equal(401); + expect(result.headers.get('logout-authorization')).to.equal('CHT-Core API'); + expect(result.body.error).to.equal('unauthorized'); }); }); }); @@ -234,12 +234,12 @@ describe('routing', () => { results.forEach((result, idx) => { if (idx === 0) { // online user request - expect(result.statusCode).to.equal(404); - expect(result.responseBody.error).to.equal('not_found'); + expect(result.status).to.equal(404); + expect(result.body.error).to.equal('not_found'); } else { // offline user requests - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -275,12 +275,12 @@ describe('routing', () => { results.forEach((result, idx) => { if (idx === 0) { // online user request - expect(result.statusCode).to.equal(404); - expect(result.responseBody.error).to.equal('not_found'); + expect(result.status).to.equal(404); + expect(result.body.error).to.equal('not_found'); } else { // offline user requests - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -317,12 +317,12 @@ describe('routing', () => { results.forEach((result, idx) => { if (idx === 0) { // online user request - expect(result.statusCode).to.equal(404); - expect(result.responseBody.error).to.equal('not_found'); + expect(result.status).to.equal(404); + expect(result.body.error).to.equal('not_found'); } else { // offline user requests - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -363,8 +363,8 @@ describe('routing', () => { expect(result.docs.length).to.be.above(0); } else { // offline user request - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -406,8 +406,8 @@ describe('routing', () => { expect(result.fields).to.deep.equal([]); } else { // offline user requests - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -444,8 +444,8 @@ describe('routing', () => { expect(result.indexes.length).to.equal(1); } else { // offline user request - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -488,8 +488,8 @@ describe('routing', () => { expect(result.ok).to.equal(true); } else { // offline user request - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -522,11 +522,11 @@ describe('routing', () => { results.forEach((result, idx) => { if (idx === 0) { // online user request - expect(result.statusCode).to.be.undefined; + expect(result.status).to.be.undefined; } else { // offline user requests - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); } }); }); @@ -559,8 +559,8 @@ describe('routing', () => { .catch(err => err) ]).then(results => { results.forEach(result => { - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); }); }); }); @@ -658,8 +658,8 @@ describe('routing', () => { ]) .then(results => { results.forEach(result => { - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); }); }); }); @@ -695,8 +695,8 @@ describe('routing', () => { .catch(err => err), ]).then(results => { results.forEach(result => { - expect(result.statusCode).to.equal(403); - expect(result.responseBody.error).to.equal('forbidden'); + expect(result.status).to.equal(403); + expect(result.body.error).to.equal('forbidden'); }); }); }); @@ -739,17 +739,17 @@ describe('routing', () => { return createSession() .then(res => { - expect(res.statusCode).to.equal(200); - expect(res.headers['set-cookie'].length).to.equal(1); - const sessionCookie = res.headers['set-cookie'][0].split(';')[0]; + expect(res.status).to.equal(200); + expect(res.headers.getSetCookie().length).to.equal(1); + const sessionCookie = res.headers.getSetCookie()[0].split(';')[0]; expect(sessionCookie.split('=')[0]).to.equal('AuthSession'); return sessionCookie; }) .then(sessionCookie => getSession(sessionCookie)) .then(res => { - expect(res.statusCode).to.equal(200); - expect(res.headers['set-cookie'].length).to.equal(1); - const [ content, age, path, expires, samesite ] = res.headers['set-cookie'][0].split('; '); + expect(res.status).to.equal(200); + expect(res.headers.getSetCookie().length).to.equal(1); + const [ content, age, path, expires, samesite ] = res.headers.getSetCookie()[0].split('; '); // check the cookie content is unchanged const [ contentKey, contentValue ] = content.split('='); @@ -827,7 +827,7 @@ describe('routing', () => { return utils.requestOnTestDb(_.defaults(params, offlineRequestOptions)).catch(err => err); }) .then(response => { - expect(response.statusCode).to.equal(403); + expect(response.status).to.equal(403); }) .then(() => { const params = { @@ -838,7 +838,7 @@ describe('routing', () => { return utils.requestOnMedicDb(_.defaults(params, offlineRequestOptions)).catch(err => err); }) .then(response => { - expect(response.statusCode).to.equal(403); + expect(response.status).to.equal(403); }) .then(() => utils.getDoc('settings')) .then(settings => { diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index 2b50dec116c..bc6d056347c 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -1,5 +1,4 @@ const utils = require('@utils'); -const request = require('request'); const constants = require('@constants'); const _ = require('lodash'); const placeFactory = require('@factories/cht/contacts/place'); @@ -15,80 +14,61 @@ describe('server', () => { json: false }; - return utils.requestOnTestDb(opts, true) + return utils.requestOnTestDb(opts) .then(() => expect.fail('should have thrown')) .catch(e => { - expect(e.responseBody).to.equal('Content-Type must be application/json'); + expect(e.body).to.equal('Content-Type must be application/json'); }); }); }); describe('response compression', () => { - const requestWrapper = (options) => { - _.defaults(options, { - auth: { - sendImmediately: true, - username: constants.USERNAME, - password: constants.PASSWORD - }, - method: 'GET', - baseUrl: constants.BASE_URL + '/' + constants.DB_NAME, - uri: '/', - gzip: true - }); + const requestWrapper = async (options) => { + const opts = { path: '/', gzip: true, resolveWithFullResponse: true, ...options }; - return new Promise((resolve, reject) => { - request(options, (err, res, body) => { - if (err) { - return reject(err); - } - - if (res.headers['content-type'] === 'application/json' && typeof body === 'string') { - try { - body = JSON.parse(body); - } catch (err) { - // an error occured when trying parse 'body' to Object - } - } - - resolve({ res, body }); - }); - }); + const res = await utils.request(opts); + return { res, body: res.body }; }; it('compresses proxied CouchDB application/json requests which send accept-encoding gzip headers', () => { - const options = { uri: '/_all_docs' }; + const options = { path: '/medic/_all_docs' }; return requestWrapper(options).then(({res}) => { - expect(res.headers['content-encoding']).to.equal('gzip'); - expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers.get('content-encoding')).to.equal('gzip'); + expect(res.headers.get('content-type')).to.equal('application/json'); }); }); it('compresses proxied CouchDB application/json requests which send accept-encoding deflate headers', () => { - const options = { uri: '/_all_docs', gzip: false, headers: { 'Accept-Encoding': 'deflate' } }; + const options = { + path: '/medic/_all_docs', + gzip: false, + headers: { 'Accept-Encoding': 'deflate' } + }; return requestWrapper(options).then(({res}) => { - expect(res.headers['content-encoding']).to.equal('deflate'); - expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers.get('content-encoding')).to.equal('deflate'); + expect(res.headers.get('content-type')).to.equal('application/json'); }); }); it('does not compress when no accept-encoding headers are sent', () => { - const options = { uri: '/_all_docs', gzip: false }; + const options = { + path: '/medic/_all_docs', + gzip: false + }; return requestWrapper(options).then(({res}) => { - expect(res.headers['content-type']).to.equal('application/json'); - expect(res.headers['content-encoding']).to.be.undefined; + expect(res.headers.get('content-type')).to.equal('application/json'); + expect(res.headers.get('content-encoding')).to.be.null; }); }); it('compresses audited endpoints responses', () => { // compression threshold is 1024B const options = { - uri: '/_bulk_docs', + path: '/medic/_bulk_docs', method: 'POST', - json: true, body: { docs: [ { _id: 'sample_doc' }, { _id: 'sample_doc2' }, { _id: 'sample_doc3' }, @@ -102,9 +82,8 @@ describe('server', () => { }; return requestWrapper(options).then(({res, body}) => { - expect(res.headers['content-type']).to.equal('application/json'); - expect(res.headers['content-encoding']).to.equal('gzip'); - expect(body.length).to.equal(18); + expect(res.headers.get('content-type')).to.equal('application/json'); + expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(_.omit(body[0], 'rev')).to.eql({ id: 'sample_doc', ok: true }); expect(_.omit(body[1], 'rev')).to.eql({ id: 'sample_doc2', ok: true }); expect(_.omit(body[2], 'rev')).to.eql({ id: 'sample_doc3', ok: true }); @@ -116,22 +95,28 @@ describe('server', () => { .getDoc('sample_doc') .then(doc => { const options = { - uri: '/sample_doc/attach?rev=' + doc._rev, + path: '/medic/sample_doc/attach', body: 'my-attachment-content', headers: { 'Content-Type': 'text/plain' }, - method: 'PUT' + method: 'PUT', + json: false, + qs: { rev: doc._rev } }; return requestWrapper(options); }) .then(({body}) => { - const options = { uri: '/sample_doc/attach?rev=' + body.rev}; + const options = { + path: '/medic//sample_doc/attach', + json: false, + qs: { rev: body.rev } + }; return requestWrapper(options); }) .then(({res, body}) => { - expect(res.headers['content-type']).to.equal('text/plain'); - expect(res.headers['content-encoding']).to.equal('gzip'); + expect(res.headers.get('content-type')).to.equal('text/plain'); + expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(body).to.equal('my-attachment-content'); }); }); @@ -144,22 +129,27 @@ describe('server', () => { .getDoc('sample_doc2') .then(doc => { const options = { - uri: '/sample_doc2/attach?rev=' + doc._rev, + path: '/medic/sample_doc2/attach', body: xml, + json: false, headers: { 'Content-Type': 'application/xml' }, - method: 'PUT' + method: 'PUT', + qs: { rev: doc._rev } }; return requestWrapper(options); }) .then(({body}) => { - const options = { uri: '/sample_doc2/attach?rev=' + body.rev}; + const options = { + path: '/medic/sample_doc2/attach', + qs: { rev: body.rev } + }; return requestWrapper(options); }) .then(({res, body}) => { - expect(res.headers['content-type']).to.equal('application/xml'); - expect(res.headers['content-encoding']).to.equal('gzip'); + expect(res.headers.get('content-type')).to.equal('application/xml'); + expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(body).to.equal(xml); }); }); @@ -170,16 +160,21 @@ describe('server', () => { 'Person 1.1.2.1'; const doc = await utils.getDoc('sample_doc2'); const options = { - uri: '/sample_doc2/attach?rev='+doc._rev, + path: '/medic/sample_doc2/attach', body: png, headers: { 'Content-Type': 'image/png' }, - method: 'PUT' + method: 'PUT', + qs: { rev: doc._rev }, }; const { body } = await requestWrapper(options); - const getAttachmentOptions = { uri: '/sample_doc2/attach?rev=' + body.rev }; + const getAttachmentOptions = { + path: '/medic/sample_doc2/attach', + qs: { rev: body.rev }, + json: false, + }; const { res, body: attachmentBody } = await requestWrapper(getAttachmentOptions); - expect(res.headers[ 'content-type' ]).to.equal('image/png'); - expect(res.headers[ 'content-encoding' ]).to.be.undefined; + expect(res.headers.get('content-type')).to.equal('image/png'); + expect(res.headers.get('content-encoding')).to.be.null; expect(attachmentBody).to.equal(png); }); }); diff --git a/tests/integration/infodocs/infodocs.spec.js b/tests/integration/infodocs/infodocs.spec.js index c6e7f2fef74..c49848bd0d2 100644 --- a/tests/integration/infodocs/infodocs.spec.js +++ b/tests/integration/infodocs/infodocs.spec.js @@ -58,7 +58,7 @@ describe('infodocs', () => { await utils.requestOnTestDb({ path, method, body: doc }); assert.fail('request should fail with conflict'); } catch (err) { - assert.equal(err.statusCode, 409); + assert.equal(err.status, 409); } const [newInfoDoc] = await delayedInfoDocsOf(doc._id); diff --git a/tests/integration/medic-collect/medic-collect.spec.js b/tests/integration/medic-collect/medic-collect.spec.js index d8e9c0d72df..e40bb3e40aa 100644 --- a/tests/integration/medic-collect/medic-collect.spec.js +++ b/tests/integration/medic-collect/medic-collect.spec.js @@ -1,6 +1,4 @@ const assert = require('chai').assert; -const constants = require('@constants'); -const request = require('request-promise-native'); const utils = require('@utils'); const host = 'localhost'; const db = utils.db; @@ -37,19 +35,15 @@ describe('medic-collect', () => { describe('without User-Agent header', () => { it('is prompted for auth details if not supplied', () => { return getForms({ auth: false, userAgent: false }) - .then(() => { - assert.fail('should fail the request'); - }) - .catch(err => { - assert.equal(err.statusCode, 401); - assert.equal(err.response.headers['www-authenticate'], 'Basic realm="Medic Web Services"'); + .then(err => { + assert.equal(err.status, 401); }); }); it('can fetch a list of forms', () => { return getForms({ auth: true, userAgent: false }) .then(res => { - assert.equal(res.statusCode, 200); + assert.equal(res.status, 200); assert.equal(res.body, MY_COLLECT_FORM_RESPONSE); }); }); @@ -58,19 +52,15 @@ describe('medic-collect', () => { describe('with User-Agent header', () => { it('is prompted for auth details if not supplied', () => { return getForms({ auth: false, userAgent: true }) - .then(() => { - assert.fail('should fail the request'); - }) - .catch(err => { - assert.equal(err.statusCode, 401); - assert.equal(err.response.headers['www-authenticate'], 'Basic realm="Medic Web Services"'); + .then(err => { + assert.equal(err.status, 401); }); }); it('can fetch a list of forms', () => { return getForms({ auth: true, userAgent: true }) .then(res => { - assert.equal(res.statusCode, 200); + assert.equal(res.status, 200); assert.equal(res.body, MY_COLLECT_FORM_RESPONSE); }); }); @@ -78,8 +68,6 @@ describe('medic-collect', () => { }); const getForms = ({ auth, userAgent }) => { - const url = auth ? constants.BASE_URL_AUTH : constants.BASE_URL; - const headers = { 'X-OpenRosa-Version': '1.0', Date: new Date().toISOString(), @@ -90,10 +78,11 @@ const getForms = ({ auth, userAgent }) => { 'org.medicmobile.collect.android/SNAPSHOT'; } - return request.get({ - url: `${url}/api/v1/forms`, + return utils.request({ + path: `/api/v1/forms`, headers, - resolveWithFullResponse: true + resolveWithFullResponse: true, + noAuth: !auth, }); }; diff --git a/tests/integration/nginx/nginx.spec.js b/tests/integration/nginx/nginx.spec.js index a661187fb6d..506640e93b7 100644 --- a/tests/integration/nginx/nginx.spec.js +++ b/tests/integration/nginx/nginx.spec.js @@ -5,14 +5,14 @@ const constants = require('@constants'); describe('HTTP request should redirect to HTTPS', () => { it('should return a 301 status code and redirect to HTTPS @docker', async () => { const [jsonResponse, htmlResponse] = await Promise.all([ - utils.request({ uri: `http://${constants.API_HOST}/`, followRedirect: false, json: true }).catch(err => err), - utils.request({ uri: `http://${constants.API_HOST}/`, followRedirect: false, json: false }).catch(err => err), + utils.request({ uri: `http://${constants.API_HOST}/`, redirect: 'manual', json: true, resolveWithFullResponse: true }).catch(err => err), + utils.request({ uri: `http://${constants.API_HOST}/`, redirect: 'manual', json: false, resolveWithFullResponse: true }).catch(err => err), ]); - expect(jsonResponse.statusCode).to.be.equal(301); - expect(htmlResponse.statusCode).to.be.equal(301); - expect(jsonResponse.responseBody.error).to.be.equal('301 Moved Permanently'); - expect(htmlResponse.responseBody).to.contain('301 Moved Permanently'); + expect(jsonResponse.status).to.be.equal(301); + expect(htmlResponse.status).to.be.equal(301); + expect(jsonResponse.body.error).to.be.equal('301 Moved Permanently'); + expect(htmlResponse.body).to.contain('301 Moved Permanently'); }); }); @@ -23,10 +23,10 @@ describe('HTTP acme-challenge should not redirect', () => { utils.request({ uri: `http://${constants.API_HOST}/.well-known/acme-challenge/`, json: false }).catch(err => err), ]); - expect(jsonResponse.statusCode).to.be.equal(404); - expect(htmlResponse.statusCode).to.be.equal(404); - expect(jsonResponse.responseBody.error).to.be.equal('404 Not Found'); - expect(htmlResponse.responseBody).to.contain('404 Not Found'); + expect(jsonResponse.status).to.be.equal(404); + expect(htmlResponse.status).to.be.equal(404); + expect(jsonResponse.body.error).to.be.equal('404 Not Found'); + expect(htmlResponse.body).to.contain('404 Not Found'); }); }); diff --git a/tests/integration/sentinel/transitions/create-user-for-contacts.spec.js b/tests/integration/sentinel/transitions/create-user-for-contacts.spec.js index d534d67137e..efbe2d564e7 100644 --- a/tests/integration/sentinel/transitions/create-user-for-contacts.spec.js +++ b/tests/integration/sentinel/transitions/create-user-for-contacts.spec.js @@ -47,10 +47,10 @@ const loginAsUser = ({ username, password }) => { const opts = { path: '/medic/login', method: 'POST', - simple: false, + resolveWithFullResponse: true, noAuth: true, body: { user: username, password }, - followRedirect: false, + redirect: 'manual', }; return utils.request(opts); }; @@ -124,7 +124,7 @@ describe('create_user_for_contacts', () => { await utils.createUsers([ORIGINAL_USER]); newUsers.push(ORIGINAL_USER.username); // Can log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 302 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 302 }); await utils.saveDoc(NEW_PERSON); // Write another contact that has a user being created and another user being replaced // (This is an approximation of behavior that could happen if Sentinel was down when the @@ -154,11 +154,11 @@ describe('create_user_for_contacts', () => { // Transition successful assert.isTrue(transitions.create_user_for_contacts.ok); // Can no longer log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 401 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 401 }); // User's password was automatically reset. Change it to something we know. await updateUserPassword(ORIGINAL_USER.username, 'n3wPassword!'); // Can still login as original user with new password - assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { statusCode: 302 }); + assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { status: 302 }); // New replacement user created const [newUserSettings, ...additionalUsers] = await utils.getUserSettings({ contactId: NEW_PERSON._id }); @@ -217,7 +217,7 @@ describe('create_user_for_contacts', () => { await utils.createUsers([ORIGINAL_USER]); newUsers.push(ORIGINAL_USER.username); // Can log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 302 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 302 }); await utils.saveDoc(NEW_PERSON); const originalContact = await utils.getDoc(ORIGINAL_PERSON._id); originalContact.user_for_contact = { @@ -235,11 +235,11 @@ describe('create_user_for_contacts', () => { // Transition successful assert.isTrue(transitions.create_user_for_contacts.ok); // Can no longer log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 401 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 401 }); // User's password was automatically reset. Change it to something we know. await updateUserPassword(ORIGINAL_USER.username, 'n3wPassword!'); // Can still login as original user with new password - assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { statusCode: 302 }); + assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { status: 302 }); // New user created const [newUserSettings, ...additionalUsers] = await utils.getUserSettings({ contactId: NEW_PERSON._id }); @@ -273,7 +273,7 @@ describe('create_user_for_contacts', () => { await utils.createUsers([otherUser]); newUsers.push(otherUser.username); // Can log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 302 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 302 }); await utils.saveDoc(NEW_PERSON); const originalContact = await utils.getDoc(ORIGINAL_PERSON._id); originalContact.user_for_contact = { @@ -291,11 +291,11 @@ describe('create_user_for_contacts', () => { // Transition successful assert.isTrue(transitions.create_user_for_contacts.ok); // Can no longer log in as user - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 401 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 401 }); // User's password was automatically reset. Change it to something we know. await updateUserPassword(ORIGINAL_USER.username, 'n3wPassword!'); // Can still login as original user with new password - assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { statusCode: 302 }); + assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { status: 302 }); // New user created const [newUserSettings, ...additionalUsers] = await utils.getUserSettings({ contactId: NEW_PERSON._id }); assert.isEmpty(additionalUsers); @@ -323,7 +323,7 @@ describe('create_user_for_contacts', () => { const [otherUserSettings] = await utils.getUserSettings({ name: otherUser.username }); assert.equal(otherUserSettings.contact_id, ORIGINAL_PERSON._id); // Can still log in as other user - assert.include(await loginAsUser(otherUser), { statusCode: 302 }); + assert.include(await loginAsUser(otherUser), { status: 302 }); }); it('replaces multiple users for a contact', async () => { @@ -334,8 +334,8 @@ describe('create_user_for_contacts', () => { await utils.createUsers([otherUser]); newUsers.push(otherUser.username); // Can log in as users - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 302 }); - assert.include(await loginAsUser(otherUser), { statusCode: 302 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 302 }); + assert.include(await loginAsUser(otherUser), { status: 302 }); await utils.saveDoc(NEW_PERSON); const originalContact = await utils.getDoc(ORIGINAL_PERSON._id); originalContact.user_for_contact = { @@ -357,14 +357,14 @@ describe('create_user_for_contacts', () => { // Transition successful assert.isTrue(transitions.create_user_for_contacts.ok); // Can no longer log in as users - assert.include(await loginAsUser(ORIGINAL_USER), { statusCode: 401 }); - assert.include(await loginAsUser(otherUser), { statusCode: 401 }); + assert.include(await loginAsUser(ORIGINAL_USER), { status: 401 }); + assert.include(await loginAsUser(otherUser), { status: 401 }); // User's password was automatically reset. Change it to something we know. await updateUserPassword(ORIGINAL_USER.username, 'n3wPassword!'); await updateUserPassword(otherUser.username, 'n3wPassword!'); // Can still login as original user with new password - assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { statusCode: 302 }); - assert.include(await loginAsUser({ ...otherUser, password: 'n3wPassword!' }), { statusCode: 302 }); + assert.include(await loginAsUser({ ...ORIGINAL_USER, password: 'n3wPassword!' }), { status: 302 }); + assert.include(await loginAsUser({ ...otherUser, password: 'n3wPassword!' }), { status: 302 }); // New users created const [newUserSettings0, newUserSettings1, ...additionalUsers] = await utils.getUserSettings({ contactId: NEW_PERSON._id }); diff --git a/tests/page-objects/default/login/login.wdio.page.js b/tests/page-objects/default/login/login.wdio.page.js index 766689b9588..e61b4fd5f82 100644 --- a/tests/page-objects/default/login/login.wdio.page.js +++ b/tests/page-objects/default/login/login.wdio.page.js @@ -59,10 +59,10 @@ const cookieLogin = async (options = {}) => { path: '/medic/login', body: { user: username, password: password, locale }, method: 'POST', - simple: false, + resolveWithFullResponse: true, }; const resp = await utils.request(opts); - const cookieArray = utils.parseCookieResponse(resp.headers['set-cookie']); + const cookieArray = utils.parseCookieResponse(resp.headers.getSetCookie()); await browser.url('/'); await browser.setCookies(cookieArray); diff --git a/tests/utils/index.js b/tests/utils/index.js index 4b69ee69461..ad846f714c7 100644 --- a/tests/utils/index.js +++ b/tests/utils/index.js @@ -2,7 +2,6 @@ const _ = require('lodash'); const constants = require('@constants'); -const rpn = require('request-promise-native'); const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -62,10 +61,6 @@ const logsDb = new PouchDB(`${constants.BASE_URL}/${constants.DB_NAME}-logs`, { const existingFeedbackDocIds = []; const MINIMUM_BROWSER_VERSION = '90'; const KUBECTL_CONTEXT = `-n ${PROJECT_NAME} --context k3d-${PROJECT_NAME}`; -const cookieJar = rpn.jar(); - -// Cookies from the jar will be included on Node `fetch` calls -global.fetch = require('fetch-cookie').default(global.fetch, cookieJar); const makeTempDir = (prefix) => fs.mkdtempSync(path.join(path.join(os.tmpdir(), prefix || 'ci-'))); const env = { @@ -77,7 +72,7 @@ const env = { const dockerPlatformName = () => { try { - return JSON.parse(execSync(`docker version --format '{{json .Server.Platform.Name}}'`)); + return JSON.parse(execSync(`docker version --format '{{json .Server.Platform.Name}}'`).toString()); } catch (error) { console.log('docker version failed. NOTE this error is not relevant if running outside of docker'); console.log(error.message); @@ -92,7 +87,7 @@ const isDockerDesktop = () => { const dockerGateway = () => { const network = isDocker() ? NETWORK : `k3d-${PROJECT_NAME}`; try { - return JSON.parse(execSync(`docker network inspect ${network} --format='{{json .IPAM.Config}}'`)); + return JSON.parse(execSync(`docker network inspect ${network} --format='{{json .IPAM.Config}}'`).toString()); } catch (error) { console.log('docker network inspect failed. NOTE this error is not relevant if running outside of docker'); console.log(error.message); @@ -138,81 +133,115 @@ const setupUserDoc = (userName = constants.USERNAME, userDoc = userSettings.buil }); }; -const getSession = async () => { - if (cookieJar.getCookies(constants.BASE_URL).length) { - return; +const randomIp = () => { + const section = () => (Math.floor(Math.random() * 255) + 1); + return `${section()}.${section()}.${section()}.${section()}`; +}; + +const getRequestUri = (options) => { + let uri = (options.uri || `${constants.BASE_URL}${options.path}`); + if (options.qs) { + Object.keys(options.qs).forEach((key) => { + if (Array.isArray(options.qs[key])) { + options.qs[key] = JSON.stringify(options.qs[key]); + } + }); + uri = `${uri}?${new URLSearchParams(options.qs).toString()}`; } - const options = { - method: 'POST', - uri: `${constants.BASE_URL}/_session`, - json: true, - body: { name: auth.username, password: auth.password }, - auth, - resolveWithFullResponse: true, - }; - const response = await rpn(options); - const setCookie = response.headers?.['set-cookie']; - const header = Array.isArray(setCookie) ? setCookie.find(header => header.startsWith('AuthSession')) : setCookie; - if (header) { - try { - cookieJar.setCookie(rpn.cookie(header), constants.BASE_URL); - } catch (err) { - console.error(err); - } + return uri; +}; + +const setRequestContentType = (options) => { + let sendJson = true; + if (options.json === false || + (options.headers['Content-Type'] && options.headers['Content-Type'] !== 'application/json') + ) { + sendJson = false; + } + + if (sendJson) { + options.headers.Accept = 'application/json'; + options.headers['Content-Type'] = 'application/json'; + options.body = JSON.stringify(options.body); } + + return sendJson; }; -const isLoginRequest = options => { - return options.path === '/medic/login' && options.body.user !== auth.username; +const setRequestEncoding = (options) => { + if (options.gzip) { + options.headers['Accept-Encoding'] = 'gzip'; + } + + if (options.gzip === false && !options.headers['Accept-Encoding']) { + options.headers['Accept-Encoding'] = 'identity'; + } }; -const randomIp = () => { - const section = () => (Math.floor(Math.random() * 255) + 1); - return `${section()}.${section()}.${section()}.${section()}`; +const setRequestAuth = (options) => { + if (options.noAuth) { + return; + } + + const auth = options.auth || { username: constants.USERNAME, password: constants.PASSWORD }; + const basicAuth = btoa(`${auth.username}:${auth.password}`); + options.headers.Authorization = `Basic ${basicAuth}`; +}; + +const getRequestOptions = (options) => { + options = typeof options === 'string' ? { path: options } : _.clone(options); + options.headers = options.headers || {}; + options.headers['X-Forwarded-For'] = randomIp(); + + const uri = getRequestUri(options); + const sendJson = setRequestContentType(options); + + setRequestAuth(options); + setRequestEncoding(options); + + return { uri, options, resolveWithFullResponse: options.resolveWithFullResponse, sendJson }; +}; + +const getResponseBody = async (response, sendJson) => { + const receiveJson = (!response.headers.get('content-type') && sendJson) || + response.headers.get('content-type')?.startsWith('application/json'); + return receiveJson ? await response.json() : await response.text(); }; // First Object is passed to http.request, second is for specific options / flags // for this wrapper -const request = async (options, { debug } = {}) => { //NOSONAR - options = typeof options === 'string' ? { path: options } : _.clone(options); - if (!options.noAuth && !options.auth && !isLoginRequest(options)) { - await getSession(); - options.jar = cookieJar; - } else { - options.headers = options.headers || {}; - options.headers['X-Forwarded-For'] = randomIp(); +const request = async (options, { debug } = {}) => { + const { uri, options: requestInit, resolveWithFullResponse, sendJson } = getRequestOptions(options); + if (debug) { + console.debug('SENDING REQUEST', JSON.stringify({ ...options, body: null }, null, 2)); } - options.uri = options.uri || `${constants.BASE_URL}${options.path}`; - options.json = options.json === undefined ? true : options.json; + + const response = await fetch(uri, requestInit); + const responseObj = { + ...response, + body: await getResponseBody(response, sendJson), + status: response.status, + ok: response.ok, + headers: response.headers + }; if (debug) { - console.log('SENDING REQUEST'); - console.log(JSON.stringify(options, null, 2)); + console.debug('RESPONSE', response.status, response.body); } - options.transform = (body, response, resolveWithFullResponse) => { - if (debug) { - console.log('RESPONSE'); - console.log(response.statusCode); - console.log(response.body); - } - // we might get a json response for a non-json request. - const contentType = response.headers['content-type']; - if (contentType?.startsWith('application/json') && !options.json) { - response.body = JSON.parse(response.body); - } - // return full response if `resolveWithFullResponse` or if non-2xx status code (so errors can be inspected) - return resolveWithFullResponse || !(/^2/.test('' + response.statusCode)) ? response : response.body; - }; + if (resolveWithFullResponse) { + return responseObj; + } - try { - return await rpn(options); - } catch (err) { - err.responseBody = err?.response?.body; - console.warn(`Error with request: ${options.method || 'GET'} ${options.uri} ${err.statusCode}`); - throw err; + if (response.ok || (response.status > 300 && response.status < 399)) { + return responseObj.body; } + + console.warn(`Error with request: ${options.method || 'GET'} ${uri} ${responseObj.status}`); + const err = new Error(response.error || `${response.status} - ${JSON.stringify(responseObj.body)}`); + Object.assign(err, responseObj); + throw err; }; const requestOnTestDb = (options, debug) => { diff --git a/tests/utils/sentinel.js b/tests/utils/sentinel.js index 59202270461..11b40ac2a13 100644 --- a/tests/utils/sentinel.js +++ b/tests/utils/sentinel.js @@ -19,7 +19,7 @@ const waitForSeq = (metadataId, docIds) => { return utils.sentinelDb .get(metadataId) .catch(err => { - if (err.statusCode === 404) { // maybe Sentinel hasn't started yet + if (err.status === 404) { // maybe Sentinel hasn't started yet return { value: 0 }; } throw err; diff --git a/tests/utils/sms.js b/tests/utils/sms.js index c2e5f5486a7..3ee179d1875 100644 --- a/tests/utils/sms.js +++ b/tests/utils/sms.js @@ -5,7 +5,7 @@ const sendSms = async (phone, message = 'testing sms') => { method: 'POST', path: '/api/v2/records', headers: { - 'Content-type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded' }, body: `message=${message}&from=${phone}`, });