diff --git a/.gitignore b/.gitignore
index c3fe41ab2..c423abf15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ dist
**/src/formio
**/cypress/**/videos
screenshots
+downloads
node_modules
# Ignore only top-level package-lock.json
diff --git a/app/app.js b/app/app.js
index 7a787470c..c9f41e61a 100644
--- a/app/app.js
+++ b/app/app.js
@@ -8,6 +8,7 @@ const querystring = require('querystring');
const log = require('./src/components/log')(module.filename);
const httpLogger = require('./src/components/log').httpLogger;
const middleware = require('./src/forms/common/middleware');
+const rateLimiter = require('./src/forms/common/middleware').apiKeyRateLimiter;
const v1Router = require('./src/routes/v1');
const DataConnection = require('./src/db/dataConnection');
@@ -52,6 +53,8 @@ app.use((_req, res, next) => {
}
});
+app.use(config.get('server.basePath') + config.get('server.apiPath'), rateLimiter);
+
// Frontend configuration endpoint
apiRouter.use('/config', (_req, res, next) => {
try {
diff --git a/app/frontend/src/components/designer/FormDesigner.vue b/app/frontend/src/components/designer/FormDesigner.vue
index 4f459b20b..17df2cba2 100644
--- a/app/frontend/src/components/designer/FormDesigner.vue
+++ b/app/frontend/src/components/designer/FormDesigner.vue
@@ -405,6 +405,7 @@ async function schemaCreateNew() {
apiIntegration: form.value.apiIntegration,
useCase: form.value.useCase,
labels: form.value.labels,
+ formMetadata: form.value.formMetadata,
});
// update user labels with any new added labels
if (
diff --git a/app/frontend/src/components/designer/FormSettings.vue b/app/frontend/src/components/designer/FormSettings.vue
index deb8c434f..478b3bd7e 100644
--- a/app/frontend/src/components/designer/FormSettings.vue
+++ b/app/frontend/src/components/designer/FormSettings.vue
@@ -6,6 +6,7 @@ import FormAccessSettings from '~/components/designer/settings/FormAccessSetting
import FormFunctionalitySettings from '~/components/designer/settings/FormFunctionalitySettings.vue';
import FormSubmissionSettings from '~/components/designer/settings/FormSubmissionSettings.vue';
import FormScheduleSettings from '~/components/designer/settings/FormScheduleSettings.vue';
+import FormMetadataSettings from '~/components/designer/settings/FormMetadataSettings.vue';
import { useFormStore } from '~/store/form';
defineProps({
@@ -36,6 +37,9 @@ const { form, isFormPublished, isRTL } = storeToRefs(useFormStore());
+
+
+
diff --git a/app/frontend/src/components/designer/FormViewer.vue b/app/frontend/src/components/designer/FormViewer.vue
index 42c6714e2..5d1025209 100644
--- a/app/frontend/src/components/designer/FormViewer.vue
+++ b/app/frontend/src/components/designer/FormViewer.vue
@@ -128,7 +128,9 @@ const { authenticated, keycloak, tokenParsed, user } = storeToRefs(authStore);
const { downloadedFile, isRTL } = storeToRefs(formStore);
const formScheduleExpireMessage = computed(() =>
- t('trans.formViewer.formScheduleExpireMessage')
+ form?.value?.schedule?.message
+ ? form.value.schedule.message
+ : t('trans.formViewer.formScheduleExpireMessage')
);
const formUnauthorizedMessage = computed(() =>
diff --git a/app/frontend/src/components/designer/settings/FormMetadataSettings.vue b/app/frontend/src/components/designer/settings/FormMetadataSettings.vue
new file mode 100644
index 000000000..f10d91692
--- /dev/null
+++ b/app/frontend/src/components/designer/settings/FormMetadataSettings.vue
@@ -0,0 +1,122 @@
+
+
+
+
+ {{
+ $t('trans.formSettings.formMetadataTitle')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/frontend/src/internationalization/trans/chefs/ar/ar.json b/app/frontend/src/internationalization/trans/chefs/ar/ar.json
index f661cf1b6..2579af577 100644
--- a/app/frontend/src/internationalization/trans/chefs/ar/ar.json
+++ b/app/frontend/src/internationalization/trans/chefs/ar/ar.json
@@ -169,7 +169,10 @@
"eventSubscription": "اشتراك الحدث",
"validEndpointRequired": "الرجاء إدخال نقطة نهاية https://",
"validBearerTokenRequired": "89abddfb-2cff-4fda-83e6-13221f0c3d4f أدخل مثال رمز حامل صالح",
- "wideFormLayout": "تخطيط نموذج واسع"
+ "wideFormLayout": "تخطيط نموذج واسع",
+ "formMetadataTitle": "بيانات تعريف النموذج",
+ "formMetadataMessage": "معلومات منظمة لوصف أو شرح هذا النموذج للأنظمة الخارجية. ستتضمن الاستدعاءات للأنظمة الخارجية هذه البيانات التعريفية في حمولاتها.",
+ "formMetadataJsonError": "يجب أن تكون البيانات الوصفية للنموذج JSON صالحة. استخدم علامات الاقتباس المزدوجة حول السمات والقيم."
},
"formProfile": {
"message": "تقوم فرق CHEFS بجمع وتنظيم المعلومات لتكون مدخلات حاسمة لصياغة حالات أعمال شاملة. ستلعب هذه الحالات دورًا حيويًا في توجيه العمليات الاستراتيجية وتحسين CHEFS المستمر في السنوات القادمة. هذه المبادرة لجمع البيانات ضرورية لإعلام القرارات الحاسمة وتشكيل مسار CHEFS ، مما يضمن قابليتها للتكيف وفعاليتها في التعامل مع الاحتياجات والتحديات المتطورة.",
@@ -199,7 +202,7 @@
"selectDeploymentErr": "يرجى اختيار مستوى النشر",
"selectMinistryErr": "يرجى اختيار وزارتك",
"selectUseCaseErr": "يرجى إدخال حالة استخدام النموذج الخاص بك",
- "labelSizeErr":"لا يمكن أن تتجاوز التسميات 25 حرفًا"
+ "labelSizeErr": "لا يمكن أن تتجاوز التسميات 25 حرفًا"
},
"ministries": {
"AF": "الزراعة والغذاء (AF)",
@@ -266,7 +269,7 @@
"regenerate": "تجديد",
"generate": "يولد",
"secret": "سر",
- "filesAPIAccess":"السماح لمفتاح API هذا بالوصول إلى الملفات المرسلة"
+ "filesAPIAccess": "السماح لمفتاح API هذا بالوصول إلى الملفات المرسلة"
},
"manageVersions": {
"important": "مهم!",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "أرسل بريدًا إلكترونيًا غير مدعو إلى",
"updateUserErrMsg": "حدث خطأ أثناء محاولة تحديث المستخدمين لهذا الإرسال.",
"updateUserConsoleErrMsg": "خطأ في تعيين أذونات المستخدم. Sub: {submitId} المستخدم: {userId} خطأ: {error}",
- "searchInputLength": "يجب أن يكون إدخال البحث عن اسم المستخدم / البريد الإلكتروني BCeID أكبر من 6 أحرف.",
+ "searchInputLength": "يجب أن يكون إدخال البحث لاسم المستخدم/البريد الإلكتروني BCeID أكبر من 4 أحرف.",
"exactBCEIDSearch": "يجب أن تكون عمليات البحث في البريد الإلكتروني عن BCeID دقيقة.",
"getUsersErrMsg": "خطأ في الحصول على المستخدمين: {error}",
"exactEmailOrUsername": "أدخل البريد الإلكتروني أو اسم المستخدم بالضبط.",
@@ -1081,4 +1084,4 @@
"user": "مستخدم"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/de/de.json b/app/frontend/src/internationalization/trans/chefs/de/de.json
index c2115567f..4261135fd 100644
--- a/app/frontend/src/internationalization/trans/chefs/de/de.json
+++ b/app/frontend/src/internationalization/trans/chefs/de/de.json
@@ -169,7 +169,10 @@
"eventSubscription": "Ereignisabonnement",
"validEndpointRequired": "Bitte geben Sie einen gültigen Endpunkt ein, beginnend mit https://",
"validBearerTokenRequired": "Bitte geben Sie ein gültiges Token ein. Beispiel: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Breites Formularlayout"
+ "wideFormLayout": "Breites Formularlayout",
+ "formMetadataTitle": "Formularmetadaten",
+ "formMetadataMessage": "Strukturierte Informationen, um diese Form externen Systemen zu beschreiben oder zu erklären. Aufrufe an externe Systeme enthalten diese Metadaten in ihren Nutzdaten.",
+ "formMetadataJsonError": "Formularmetadaten müssen gültiges JSON sein. Verwenden Sie doppelte Anführungszeichen um Attribute und Werte."
},
"formProfile": {
"message": "Das CHEFS-Team sammelt und organisiert Informationen, um als entscheidende Grundlage für die Erstellung umfassender Geschäftsfälle zu dienen. Diese Fälle werden eine Schlüsselrolle dabei spielen, den strategischen Betrieb und die laufende Verbesserung von CHEFS in den kommenden Jahren zu leiten. Diese Initiative zur Datensammlung ist entscheidend, um kritische Entscheidungen zu informieren und die Trajektorie von CHEFS zu formen, um seine Anpassungsfähigkeit und Wirksamkeit bei der Bewältigung sich wandelnder Bedürfnisse und Herausforderungen zu gewährleisten.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Uneingeladene E-Mail an gesendet",
"updateUserErrMsg": "Beim Versuch, Benutzer für diese Übermittlung zu aktualisieren, ist ein Fehler aufgetreten.",
"updateUserConsoleErrMsg": "Fehler beim Festlegen der Benutzerberechtigungen. Sub: {submissionId} Benutzer: {userId} Fehler: {error}",
- "searchInputLength": "Die Sucheingabe für BCeID-Benutzername/E-Mail-Adresse muss mehr als 6 Zeichen umfassen.",
+ "searchInputLength": "Die Sucheingabe für BCeID-Benutzername/E-Mail-Adresse muss mehr als 4 Zeichen umfassen.",
"exactBCEIDSearch": "E-Mail-Suchen nach BCeID müssen genau sein.",
"getUsersErrMsg": "Fehler beim Abrufen von Benutzern: {error}",
"exactEmailOrUsername": "Geben Sie eine genaue E-Mail-Adresse oder einen Benutzernamen ein.",
@@ -605,7 +608,6 @@
"note": "Notiz",
"maxChars": "Maximal 4000 Zeichen",
"totalNotes": "Gesamtanzahl Notizen:"
-
},
"statusPanel": {
"currentStatus": "Aktueller Status:",
@@ -1082,4 +1084,4 @@
"user": "Benutzer"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/en/en.json b/app/frontend/src/internationalization/trans/chefs/en/en.json
index fe0991616..fc76f0b3a 100644
--- a/app/frontend/src/internationalization/trans/chefs/en/en.json
+++ b/app/frontend/src/internationalization/trans/chefs/en/en.json
@@ -199,7 +199,10 @@
"eventSubscription": "Event Subscription",
"validEndpointRequired": "Please enter a valid endpoint starting with https://",
"validBearerTokenRequired": "Enter a valid bearer token example: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Wide Form Layout"
+ "wideFormLayout": "Wide Form Layout",
+ "formMetadataTitle": "Form Metadata",
+ "formMetadataMessage": "Structured information to describe or explain this form to external systems. Calls to external systems will include this metadata in their payloads.",
+ "formMetadataJsonError": "Form metadata must be valid JSON. Use double-quotes around attributes and values."
},
"formProfile": {
"message": "The CHEFS team is collecting and organizing information to serve as crucial input for crafting comprehensive business cases. These cases will play a pivotal role in guiding the strategic operation and ongoing improvement of CHEFS in the coming years. This initiative to gather data is essential for informing critical decisions and molding the trajectory of CHEFS, ensuring its adaptability and effectiveness in addressing evolving needs and challenges.",
@@ -595,7 +598,7 @@
"sentUninvitedEmailTo": "Sent uninvited email to",
"updateUserErrMsg": "An error occurred while trying to update users for this submission.",
"updateUserConsoleErrMsg": "Error setting user permissions. Sub: {submissionId} User: {userId} Error: {error}",
- "searchInputLength": "Search input for BCeID username/email must be greater than 6 characters.",
+ "searchInputLength": "Search input for BCeID username/email must be greater than 4 characters.",
"exactBCEIDSearch": "Email searches for BCeID must be exact.",
"getUsersErrMsg": "Error getting users: {error}",
"exactEmailOrUsername": "Enter an exact email or username.",
diff --git a/app/frontend/src/internationalization/trans/chefs/es/es.json b/app/frontend/src/internationalization/trans/chefs/es/es.json
index 22ddb28aa..fa081d0e2 100644
--- a/app/frontend/src/internationalization/trans/chefs/es/es.json
+++ b/app/frontend/src/internationalization/trans/chefs/es/es.json
@@ -169,7 +169,10 @@
"eventSubscription": "Suscripción a eventos",
"validEndpointRequired": "Ingrese un punto final válido que comience con https://",
"validBearerTokenRequired": "Introduzca un ejemplo de token de portador válido: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Diseño de formulario amplio"
+ "wideFormLayout": "Diseño de formulario amplio",
+ "formMetadataTitle": "Metadatos del formulario",
+ "formMetadataMessage": "Información estructurada para describir o explicar este formulario a sistemas externos. Las llamadas a sistemas externos incluirán estos metadatos en sus cargas útiles.",
+ "formMetadataJsonError": "Los metadatos del formulario deben ser JSON válidos. Utilice comillas dobles alrededor de atributos y valores."
},
"formProfile": {
"message": "El equipo de CHEFS está recopilando y organizando información para servir como entrada crucial para la elaboración de casos de negocio integrales. Estos casos jugarán un papel fundamental en la guía de la operación estratégica y la mejora continua de CHEFS en los próximos años. Esta iniciativa de recopilación de datos es esencial para informar decisiones críticas y dar forma a la trayectoria de CHEFS, asegurando su adaptabilidad y efectividad para abordar necesidades y desafíos en constante evolución.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Enviado correo electrónico no invitado a",
"updateUserErrMsg": "Se produjo un error al intentar actualizar los usuarios para este envío.",
"updateUserConsoleErrMsg": "Error al establecer los permisos de usuario. Sub: {submissionId} Usuario: {userId} Error: {error}",
- "searchInputLength": "La entrada de búsqueda para el nombre de usuario/correo electrónico de BCeID debe tener más de 6 caracteres.",
+ "searchInputLength": "La entrada de búsqueda para el nombre de usuario/correo electrónico de BCeID debe tener más de 4 caracteres.",
"exactBCEIDSearch": "Las búsquedas de correo electrónico para BCeID deben ser exactas.",
"getUsersErrMsg": "Error al obtener usuarios: {error}",
"exactEmailOrUsername": "Ingrese un correo electrónico o nombre de usuario exacto.",
@@ -1081,4 +1084,4 @@
"user": "Usuario"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/fa/fa.json b/app/frontend/src/internationalization/trans/chefs/fa/fa.json
index be613f2e8..d849a4218 100644
--- a/app/frontend/src/internationalization/trans/chefs/fa/fa.json
+++ b/app/frontend/src/internationalization/trans/chefs/fa/fa.json
@@ -169,7 +169,10 @@
"eventSubscription": "اشتراک رویداد",
"validEndpointRequired": "لطفاً یک نقطه پایان معتبر که با شروع آن شروع می شود وارد کنید https://",
"validBearerTokenRequired": "89abddfb-2cff-4fda-83e6-13221f0c3d4f یک نمونه توکن حامل معتبر وارد کنید",
- "wideFormLayout": "طرح فرم گسترده"
+ "wideFormLayout": "طرح فرم گسترده",
+ "formMetadataTitle": "فراداده فرم",
+ "formMetadataMessage": "اطلاعات ساختاریافته برای توصیف یا توضیح این فرم برای سیستم های خارجی. تماسها با سیستمهای خارجی این ابرداده را در محمولههای خود شامل میشوند.",
+ "formMetadataJsonError": "فراداده فرم باید JSON معتبر باشد. از دو نقل قول در مورد ویژگی ها و مقادیر استفاده کنید."
},
"formProfile": {
"message": "تیم CHEFS اطلاعات را جمع آوری و سازماندهی میکند تا به عنوان ورودی حیاتی برای ساخت مورد کارهای تجاری جامع عمل کند. این موارد نقش کلیدی در راهنمایی عملیات استراتژیک و بهبود مستمر CHEFS در سالهای آینده خواهند داشت. این اقدام برای جمعآوری دادهها برای اطلاع از تصمیمات حیاتی و شکلدهی مسیر CHEFS جهت اطمینان از قابلیت تطبیق و کارایی آن در مواجهه با نیازها و چالشهای در حال تحول حیاتی است.",
@@ -266,7 +269,7 @@
"regenerate": "بازسازی کنید",
"generate": "تولید می کنند",
"secret": "راز",
- "filesAPIAccess":"به این کلید API اجازه دهید به فایل های ارسالی دسترسی داشته باشد"
+ "filesAPIAccess": "به این کلید API اجازه دهید به فایل های ارسالی دسترسی داشته باشد"
},
"manageVersions": {
"important": "مهم!",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "ایمیل ناخوانده به",
"updateUserErrMsg": "هنگام تلاش برای بهروزرسانی کاربران برای این ارسال، خطایی روی داد.",
"updateUserConsoleErrMsg": "خطا در تنظیم مجوزهای کاربر. فرعی: {submissionId} کاربر: {userId} خطا: {error}",
- "searchInputLength": "ورودی جستجو برای نام کاربری/ایمیل BCeID باید بیشتر از 6 کاراکتر باشد.",
+ "searchInputLength": "ورودی جستجو برای نام کاربری/ایمیل BCeID باید بیشتر از 4 کاراکتر باشد.",
"exactBCEIDSearch": "جستجوی ایمیل برای BCeID باید دقیق باشد.",
"getUsersErrMsg": "خطا در دریافت کاربران: {error}",
"exactEmailOrUsername": "یک ایمیل یا نام کاربری دقیق وارد کنید.",
@@ -1081,4 +1084,4 @@
"user": "کاربر"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/fr/fr.json b/app/frontend/src/internationalization/trans/chefs/fr/fr.json
index 11a37d431..234a30b0a 100644
--- a/app/frontend/src/internationalization/trans/chefs/fr/fr.json
+++ b/app/frontend/src/internationalization/trans/chefs/fr/fr.json
@@ -169,7 +169,10 @@
"eventSubscription": "Abonnement à l'événement",
"validEndpointRequired": "Veuillez saisir un point de terminaison valide commençant par https://",
"validBearerTokenRequired": "Saisissez un exemple de jeton de support valide : 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Disposition large du formulaire"
+ "wideFormLayout": "Disposition large du formulaire",
+ "formMetadataTitle": "Métadonnées du formulaire",
+ "formMetadataMessage": "Informations structurées pour décrire ou expliquer ce formulaire aux systèmes externes. Les appels vers des systèmes externes incluront ces métadonnées dans leurs charges utiles.",
+ "formMetadataJsonError": "Les métadonnées du formulaire doivent être du JSON valide. Utilisez des guillemets doubles autour des attributs et des valeurs."
},
"formProfile": {
"message": "L'équipe CHEFS collecte et organise des informations pour servir de contribution cruciale à l'élaboration de cas d'affaires complets. Ces cas joueront un rôle central dans la direction des opérations stratégiques et l'amélioration continue de CHEFS au cours des prochaines années. Cette initiative de collecte de données est essentielle pour éclairer les décisions critiques et façonner la trajectoire de CHEFS, assurant son adaptabilité et son efficacité face aux besoins et aux défis en constante évolution.",
@@ -266,7 +269,7 @@
"regenerate": "Régénérer",
"generate": "Générer",
"secret": "Secret",
- "filesAPIAccess":"Autoriser cette clé API à accéder aux fichiers soumis"
+ "filesAPIAccess": "Autoriser cette clé API à accéder aux fichiers soumis"
},
"manageVersions": {
"important": "IMPORTANT!",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Envoyé un e-mail non invité à",
"updateUserErrMsg": "Une erreur s'est produite lors de la tentative de mise à jour des utilisateurs pour cette soumission.",
"updateUserConsoleErrMsg": "Erreur lors de la définition des autorisations utilisateur. Sous : {submissionId} Utilisateur : {userId} Erreur : {error}",
- "searchInputLength": "L'entrée de recherche pour le nom d'utilisateur/e-mail BCeID doit comporter plus de 6 caractères.",
+ "searchInputLength": "L'entrée de recherche pour le nom d'utilisateur/e-mail BCeID doit comporter plus de 4 caractères.",
"exactBCEIDSearch": "Les recherches par e-mail pour BCeID doivent être exactes.",
"getUsersErrMsg": "Erreur lors de l'obtention des utilisateurs : {error}",
"exactEmailOrUsername": "Entrez un e-mail ou un nom d'utilisateur exact.",
@@ -1081,4 +1084,4 @@
"user": "Utilisateur"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/hi/hi.json b/app/frontend/src/internationalization/trans/chefs/hi/hi.json
index bf2b7c9c8..5bc902194 100644
--- a/app/frontend/src/internationalization/trans/chefs/hi/hi.json
+++ b/app/frontend/src/internationalization/trans/chefs/hi/hi.json
@@ -169,7 +169,10 @@
"eventSubscription": "इवेंट सदस्यता",
"validEndpointRequired": "कृपया प्रारंभ करने वाला एक वैध समापन बिंदु दर्ज करें https://",
"validBearerTokenRequired": "एक वैध वाहक टोकन उदाहरण दर्ज करें: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "वाइड फॉर्म लेआउट"
+ "wideFormLayout": "वाइड फॉर्म लेआउट",
+ "formMetadataTitle": "फॉर्म मेटाडेटा",
+ "formMetadataMessage": "बाहरी प्रणालियों को इस फॉर्म का वर्णन करने या समझाने के लिए संरचित जानकारी। बाहरी सिस्टम पर कॉल में यह मेटाडेटा उनके पेलोड में शामिल होगा.",
+ "formMetadataJsonError": "प्रपत्र मेटाडेटा वैध JSON होना चाहिए. विशेषताओं और मूल्यों के आसपास दोहरे उद्धरण चिह्नों का उपयोग करें।"
},
"formProfile": {
"message": "CHEFS टीम सूचना एकत्र कर रही है और उसे समृद्धिकारी व्यापक व्यापार मामलों के लिए महत्वपूर्ण इनपुट के रूप में सेवा करने के लिए। ये मामले CHEFS के आगामी वर्षों में रणनीतिक संचालन और उन्नति में महत्वपूर्ण भूमिका निभाएंगे। डेटा इकट्ठा करने का यह पहल सूचना को सूचित करने, महत्वपूर्ण निर्णयों को सूचित करने और CHEFS की यात्रा को मोल्डिंग के लिए आवश्यक है, इसे सुनिश्चित करना है कि यह आवश्यकताओं और चुनौतियों को समाप्त करने में अपनी योग्यता और प्रभावकारिता में बदलते समय का सामना कर सकता है।",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "को बिन बुलाए ईमेल भेजा गया",
"updateUserErrMsg": "इस सबमिशन के लिए उपयोगकर्ताओं को अपडेट करने का प्रयास करते समय एक त्रुटि उत्पन्न हुई।",
"updateUserConsoleErrMsg": "उपयोगकर्ता अनुमतियाँ सेट करने में त्रुटि. विषय: {submissionId} उपयोगकर्ता: {userId} त्रुटि: {error}",
- "searchInputLength": "बीसीईआईडी उपयोगकर्ता नाम/ईमेल के लिए खोज इनपुट 6 अक्षरों से अधिक होना चाहिए।",
+ "searchInputLength": "बीसीईआईडी उपयोगकर्ता नाम/ईमेल के लिए खोज इनपुट 4 अक्षरों से अधिक होना चाहिए।",
"exactBCEIDSearch": "बीसीईआईडी के लिए ईमेल खोजें सटीक होनी चाहिए।",
"getUsersErrMsg": "उपयोगकर्ता प्राप्त करने में त्रुटि: {error}",
"exactEmailOrUsername": "सटीक ई-मेल या उपयोगकर्ता नाम दर्ज करें.",
@@ -1081,4 +1084,4 @@
"user": "उपयोगकर्ता"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/it/it.json b/app/frontend/src/internationalization/trans/chefs/it/it.json
index 077d2f0cb..1dbc38bfb 100644
--- a/app/frontend/src/internationalization/trans/chefs/it/it.json
+++ b/app/frontend/src/internationalization/trans/chefs/it/it.json
@@ -169,7 +169,10 @@
"eventSubscription": "Abbonamento all'evento",
"validEndpointRequired": "Inserisci un endpoint valido che inizi con https://",
"validBearerTokenRequired": "Inserisci un esempio di token al portatore valido: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Layout di modulo ampio"
+ "wideFormLayout": "Layout di modulo ampio",
+ "formMetadataTitle": "Metadati del modulo",
+ "formMetadataMessage": "Informazioni strutturate per descrivere o spiegare questo modulo a sistemi esterni. Le chiamate ai sistemi esterni includeranno questi metadati nei loro payload.",
+ "formMetadataJsonError": "I metadati del modulo devono essere JSON validi. Utilizzare le virgolette doppie attorno ad attributi e valori."
},
"formProfile": {
"message": "Il team CHEFS sta raccogliendo e organizzando informazioni per servire come input cruciale per la creazione di casi aziendali completi. Questi casi avranno un ruolo cruciale nella guida dell'operazione strategica e nell'ulteriore miglioramento di CHEFS nei prossimi anni. Questa iniziativa di raccolta dati è essenziale per informare decisioni critiche e plasmare la traiettoria di CHEFS, garantendone adattabilità ed efficacia nel affrontare esigenze e sfide in evoluzione.",
@@ -225,7 +228,7 @@
"TACS": "Turismo, Arte, Cultura e Sport (TACS)",
"MOTI": "Trasporti e Infrastrutture (MOTI)",
"WLRS": "Gestione dell'Acqua, della Terra e delle Risorse (WLRS)"
-},
+ },
"subscribeEvent": {
"eventType": "Tipo di evento",
"endpointUrl": "URL dell'endpoint",
@@ -248,7 +251,7 @@
"infoA": "Assicurati che il segreto della tua chiave API sia archiviato in un luogo sicuro (ad es. Key Vault).",
"infoB": "La tua chiave API garantisce l'accesso illimitato al tuo modulo. Non dare la tua chiave API a nessuno.",
"infoC": "La chiave API deve essere utilizzata SOLO per le interazioni di sistema automatizzate. Non utilizzare la chiave API per l'accesso basato sull'utente",
- "infoD":"Se desideri accedere ai file inviati tramite la chiave API, abilita la seguente casella di controllo dopo aver generato la chiave.",
+ "infoD": "Se desideri accedere ai file inviati tramite la chiave API, abilita la seguente casella di controllo dopo aver generato la chiave.",
"deleteKey": "Elimina chiave",
"apiKey": "chiave API",
"hideSecret": "Nascondi segreto",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Email non invitata inviata a",
"updateUserErrMsg": "Si è verificato un errore durante il tentativo di aggiornare gli utenti per questo invio.",
"updateUserConsoleErrMsg": "Errore durante l'impostazione delle autorizzazioni utente. Sub: {submissionId} Utente: {userId} Errore: {error}",
- "searchInputLength": "L'input di ricerca per nome utente/e-mail BCeID deve contenere più di 6 caratteri.",
+ "searchInputLength": "L'input di ricerca per nome utente/e-mail BCeID deve contenere più di 4 caratteri.",
"exactBCEIDSearch": "Le ricerche e-mail per BCeID devono essere esatte.",
"getUsersErrMsg": "Errore durante il recupero degli utenti: {error}",
"exactEmailOrUsername": "Inserisci un'e-mail o un nome utente esatti.",
diff --git a/app/frontend/src/internationalization/trans/chefs/ja/ja.json b/app/frontend/src/internationalization/trans/chefs/ja/ja.json
index 0d0326376..c96262e4c 100644
--- a/app/frontend/src/internationalization/trans/chefs/ja/ja.json
+++ b/app/frontend/src/internationalization/trans/chefs/ja/ja.json
@@ -169,7 +169,10 @@
"eventSubscription": "イベントのサブスクリプション",
"validEndpointRequired": "で始まる有効なエンドポイントを入力してください https://",
"validBearerTokenRequired": "有効なベアラー トークンの例を入力してください: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "ワイドフォームレイアウト"
+ "wideFormLayout": "ワイドフォームレイアウト",
+ "formMetadataTitle": "フォームのメタデータ",
+ "formMetadataMessage": "このフォームを外部システムに記述または説明するための構造化情報。外部システムへの呼び出しには、このメタデータがペイロードに含まれます。",
+ "formMetadataJsonError": "フォームのメタデータは有効な JSON である必要があります。属性と値を二重引用符で囲みます。"
},
"formProfile": {
"message": "CHEFSチームは包括的なビジネスケースの作成に重要な入力となる情報を収集し、整理しています。これらのケースは、CHEFSの戦略的な運用と今後の改善を指南するうえで重要な役割を果たします。データを収集するこの取り組みは、重要な意思決定の情報提供やCHEFSの軌道を形作るために不可欠です。これにより、CHEFSが変化するニーズと課題に対応するための適応性と効果を確保します.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "招待されていないメールを次の宛先に送信しました",
"updateUserErrMsg": "この送信のユーザーを更新しようとしたときにエラーが発生しました。",
"updateUserConsoleErrMsg": "ユーザー権限の設定中にエラーが発生しました。サブ: {submissionId} ユーザー: {userId} エラー: {error}",
- "searchInputLength": "BCeID ユーザー名/電子メールの検索入力は 6 文字以上である必要があります。",
+ "searchInputLength": "BCeID ユーザー名/電子メールの検索入力は 4 文字以上である必要があります。",
"exactBCEIDSearch": "BCeID の電子メール検索は正確である必要があります。",
"getUsersErrMsg": "ユーザー取得エラー: {error}",
"exactEmailOrUsername": "正確な電子メールまたはユーザー名を入力します。",
@@ -1081,4 +1084,4 @@
"user": "ユーザー"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/ko/ko.json b/app/frontend/src/internationalization/trans/chefs/ko/ko.json
index 171bf2ed3..761f54dcc 100644
--- a/app/frontend/src/internationalization/trans/chefs/ko/ko.json
+++ b/app/frontend/src/internationalization/trans/chefs/ko/ko.json
@@ -169,7 +169,10 @@
"eventSubscription": "イベントのサブスクリプション",
"validEndpointRequired": "で始まる有効なエンドポイントを入力してください https://",
"validBearerTokenRequired": "有効なベアラー トークンの例を入力してください: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "와이드 폼 레이아웃"
+ "wideFormLayout": "와이드 폼 레이아웃",
+ "formMetadataTitle": "양식 메타데이터",
+ "formMetadataMessage": "이 양식을 외부 시스템에 설명하거나 설명하기 위한 구조화된 정보입니다. 외부 시스템에 대한 호출에는 페이로드에 이 메타데이터가 포함됩니다.",
+ "formMetadataJsonError": "양식 메타데이터는 유효한 JSON이어야 합니다. 속성과 값 주위에 큰따옴표를 사용하세요."
},
"formProfile": {
"message": "CHEFS 팀은 포괄적인 비즈니스 케이스를 작성하는 데 중요한 입력으로 사용될 정보를 수집하고 조직하고 있습니다. 이러한 케이스는 향후 몇 년 동안 CHEFS의 전략적 운영과 지속적인 개선을 안내하는 데 중추적인 역할을 할 것입니다. 데이터 수집을 위한 이 이니셔티브는 중요한 결정에 정보를 제공하고 CHEFS의 궤도를 형성하는 데 필수적입니다. 이를 통해 CHEFS가 변화하는 필요와 도전에 대응하여 적응성과 효과를 보장합니다.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "초대받지 않은 이메일을 보낸 사람",
"updateUserErrMsg": "이 제출에 대해 사용자를 업데이트하는 동안 오류가 발생했습니다.",
"updateUserConsoleErrMsg": "사용자 권한을 설정하는 중에 오류가 발생했습니다. 하위: {submissionId} 사용자: {userId} 오류: {error}",
- "searchInputLength": "BCeID 사용자 이름/이메일에 대한 검색 입력은 6자보다 커야 합니다.",
+ "searchInputLength": "BCeID 사용자 이름/이메일에 대한 검색 입력은 4자보다 커야 합니다.",
"exactBCEIDSearch": "BCeID에 대한 이메일 검색은 정확해야 합니다.",
"getUsersErrMsg": "사용자 가져오기 오류: {error}",
"exactEmailOrUsername": "정확한 이메일 또는 사용자 이름을 입력하세요.",
@@ -1081,4 +1084,4 @@
"user": "사용자"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/pa/pa.json b/app/frontend/src/internationalization/trans/chefs/pa/pa.json
index 4175c49ef..f62404cd3 100644
--- a/app/frontend/src/internationalization/trans/chefs/pa/pa.json
+++ b/app/frontend/src/internationalization/trans/chefs/pa/pa.json
@@ -169,7 +169,10 @@
"eventSubscription": "د پیښې ګډون",
"validEndpointRequired": "مهرباني وکړئ یو معتبر پای ټکی دننه کړئ چې پیل کیږي https://",
"validBearerTokenRequired": "89abddfb-2cff-4fda-83e6-13221f0c3d4f د اعتبار وړ وړونکي نښه مثال داخل کړئ",
- "wideFormLayout": "ਵਾਈਡ ਫਾਰਮ ਲੇਆਉਟ"
+ "wideFormLayout": "ਵਾਈਡ ਫਾਰਮ ਲੇਆਉਟ",
+ "formMetadataTitle": "ਫਾਰਮ ਮੈਟਾਡੇਟਾ",
+ "formMetadataMessage": "ਬਾਹਰੀ ਪ੍ਰਣਾਲੀਆਂ ਨੂੰ ਇਸ ਫਾਰਮ ਦਾ ਵਰਣਨ ਕਰਨ ਜਾਂ ਵਿਆਖਿਆ ਕਰਨ ਲਈ ਸਟ੍ਰਕਚਰਡ ਜਾਣਕਾਰੀ। ਬਾਹਰੀ ਸਿਸਟਮਾਂ ਲਈ ਕਾਲਾਂ ਵਿੱਚ ਇਹ ਮੈਟਾਡੇਟਾ ਉਹਨਾਂ ਦੇ ਪੇਲੋਡ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੇਗਾ।",
+ "formMetadataJsonError": "ਫਾਰਮ ਮੈਟਾਡੇਟਾ ਵੈਧ JSON ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ। ਗੁਣਾਂ ਅਤੇ ਮੁੱਲਾਂ ਦੇ ਆਲੇ-ਦੁਆਲੇ ਡਬਲ-ਕੋਟਸ ਦੀ ਵਰਤੋਂ ਕਰੋ।."
},
"formProfile": {
"message": "CHEFS ਟੀਮ ਜਾਣਕਾਰੀ ਇਕੱਠੀ ਕਰ ਰਹੀ ਹੈ ਅਤੇ ਵਿਗਿਆਨ ਕੇਸਾਂ ਲਈ ਮੁਦਾਵਿਆਂ ਬਣਾਉਣ ਲਈ ਮੁਹਿਮ ਵਰਤ ਰਹੀ ਹੈ। ਇਹ ਕੇਸ ਸੀਧੇ CHEFS ਦੀ ਰਣਨੀਤਿਕ ਓਪਰੇਸ਼ਨ ਅਤੇ ਚਲਦੇ ਸਾਲਾਂ ਵਿੱਚ ਚੋਣ ਦੀ ਮੁਖਮਾਨੇ ਵਿੱਚ ਏਕ ਕੀ ਬੰਦੋਬਸਤੀ ਰੋਲ ਪਵੇਗਾ। ਡਾਟਾ ਇਕੱਠਾ ਕਰਨ ਦੀ ਇਹ ਪ੍ਰਯਾਸ਼ਾ ਜਰੂਰੀ ਹੈ ਜਿਸ ਨਾਲ ਕ੍ਰਿਟਿਕਲ ਫੈਸਲਿਆਂ ਨੂੰ ਸੂਚਿਤ ਕਰਨ ਲਈ ਅਤੇ CHEFS ਦੇ ਤਰੱਕੀਪੂਰਤ ਵਿੱਚ ਮੁਕਾਬਲਾ ਕਰਨ ਲਈ ਲੋੜੀਦਾ ਹੈ। ਇਸ ਨਾਲ ਯਹ ਸੁਨਿਸ਼ਚਿਤ ਹੋ ਜਾਂਦਾ ਹੈ ਕਿ ਇਹ ਆਪਣੇ ਅਨੁਕੂਲਨ ਅਤੇ ਚੁਣੇ ਗਏ ਚੁਣੌਤੀਆਂ ਅਤੇ ਜ਼ਰੂਰਾਤਾਂ ਦਾ ਇੱਜ਼ਤੀਕਾਰ ਅਤੇ ਯੋਜਨਾ ਵਿੱਚ ਇੱਕ ਬਦਲਾਵ ਅਤੇ ਕਾਰਗੁਜ਼ਾਰੀ ਵਿਚ ਇਹ ਗੁਣਤੀ ਹੈ।",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "ਨੂੰ ਬਿਨਾਂ ਬੁਲਾਏ ਈਮੇਲ ਭੇਜੀ",
"updateUserErrMsg": "ਇਸ ਸਬਮਿਸ਼ਨ ਲਈ ਉਪਭੋਗਤਾਵਾਂ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰਦੇ ਸਮੇਂ ਇੱਕ ਤਰੁੱਟੀ ਉਤਪੰਨ ਹੋਈ।",
"updateUserConsoleErrMsg": "ਉਪਭੋਗਤਾ ਅਨੁਮਤੀਆਂ ਨੂੰ ਸੈੱਟ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ। ਉਪ: {submissionId} ਉਪਭੋਗਤਾ: {userId} ਤਰੁੱਟੀ: {error}",
- "searchInputLength": "BCeID ਉਪਭੋਗਤਾ ਨਾਮ/ਈਮੇਲ ਲਈ ਖੋਜ ਇਨਪੁਟ 6 ਅੱਖਰਾਂ ਤੋਂ ਵੱਧ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।",
+ "searchInputLength": "BCeID ਉਪਭੋਗਤਾ ਨਾਮ/ਈਮੇਲ ਲਈ ਖੋਜ ਇਨਪੁਟ 4 ਅੱਖਰਾਂ ਤੋਂ ਵੱਧ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।",
"exactBCEIDSearch": "BCeID ਲਈ ਈਮੇਲ ਖੋਜਾਂ ਸਟੀਕ ਹੋਣੀਆਂ ਚਾਹੀਦੀਆਂ ਹਨ।",
"getUsersErrMsg": "ਉਪਭੋਗਤਾਵਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ: {error}",
"exactEmailOrUsername": "ਇੱਕ ਸਹੀ ਈ-ਮੇਲ ਜਾਂ ਉਪਭੋਗਤਾ ਨਾਮ ਦਰਜ ਕਰੋ।",
@@ -1081,4 +1084,4 @@
"user": "ਉਪਭੋਗਤਾ"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/pt/pt.json b/app/frontend/src/internationalization/trans/chefs/pt/pt.json
index 2855f8a04..4ec6f0cf2 100644
--- a/app/frontend/src/internationalization/trans/chefs/pt/pt.json
+++ b/app/frontend/src/internationalization/trans/chefs/pt/pt.json
@@ -169,37 +169,40 @@
"eventSubscription": "Assinatura de evento",
"validEndpointRequired": "Insira um endpoint válido começando com https://",
"validBearerTokenRequired": "Insira um exemplo de token de portador válido: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Layout de Formulário Amplo"
+ "wideFormLayout": "Layout de Formulário Amplo",
+ "formMetadataTitle": "Metadados de formulário",
+ "formMetadataMessage": "Informações estruturadas para descrever ou explicar este formulário para sistemas externos. As chamadas para sistemas externos incluirão esses metadados em suas cargas.",
+ "formMetadataJsonError": "Os metadados do formulário devem ser JSON válidos. Use aspas duplas em torno de atributos e valores."
},
"formProfile": {
- "message": "A equipe CHEFS está coletando e organizando informações para servir como entrada crucial para a criação de casos de negócios abrangentes. Esses casos desempenharão um papel fundamental na orientação da operação estratégica e na melhoria contínua do CHEFS nos próximos anos. Essa iniciativa de coleta de dados é essencial para informar decisões críticas e moldar a trajetória do CHEFS, garantindo sua adaptabilidade e eficácia em lidar com necessidades e desafios em evolução.",
- "ministryPrompt": "Escolha o ministério ao qual você está associado",
- "useCasePrompt": "Escolha a finalidade do seu formulário abaixo",
- "deploymentPrompt": "Por favor, especifique o seu nível de implementação",
- "APIPrompt": "Especifique se pretende utilizar a API fornecida para integração em uma aplicação de terceiros",
- "labelPrompt": "As etiquetas servem como um meio de categorizar formulários semelhantes que podem pertencer a uma organização comum ou compartilhar um contexto relacionado.",
- "useCaseToolTip": "Se você não encontrar o seu caso de uso específico, entre em contato com a equipe CHEFS para discutir outras opções",
- "deploymentLevel": "Nível de Implementação",
- "ministryName": "Ministério",
- "label": "Etiquetas",
- "useCase": "Tipo de Caso de Uso",
- "development": "Desenvolvimento",
- "test": "Teste",
- "production": "Produção",
- "Y": "Sim",
- "N": "Não",
- "application": "Aplicações que serão avaliadas seguidas por uma decisão",
- "collection": "Coleta de Conjuntos de Dados, envio de dados",
- "feedback": "Formulário de Feedback para determinar satisfação, concordância, probabilidade ou outras perguntas qualitativas",
- "report": "Relatórios geralmente em uma programação repetitiva ou eventos programados como acompanhamentos",
- "registration": "Registro ou Inscrição - sem avaliação",
- "getLabelErr": "Ocorreu um erro ao tentar buscar rótulos de usuário.",
- "getLabelConsErr": "Erro ao buscar rótulos: ",
- "selectAPIErr": "Informe-nos sobre sua escolha em relação ao uso da Integração de API",
- "selectDeploymentErr": "Por favor, selecione um nível de implementação",
- "selectMinistryErr": "Por favor, selecione o seu ministério",
- "selectUseCaseErr": "Por favor, informe o caso de uso do seu formulário",
- "labelSizeErr": "Os rótulos não podem exceder 25 caracteres"
+ "message": "A equipe CHEFS está coletando e organizando informações para servir como entrada crucial para a criação de casos de negócios abrangentes. Esses casos desempenharão um papel fundamental na orientação da operação estratégica e na melhoria contínua do CHEFS nos próximos anos. Essa iniciativa de coleta de dados é essencial para informar decisões críticas e moldar a trajetória do CHEFS, garantindo sua adaptabilidade e eficácia em lidar com necessidades e desafios em evolução.",
+ "ministryPrompt": "Escolha o ministério ao qual você está associado",
+ "useCasePrompt": "Escolha a finalidade do seu formulário abaixo",
+ "deploymentPrompt": "Por favor, especifique o seu nível de implementação",
+ "APIPrompt": "Especifique se pretende utilizar a API fornecida para integração em uma aplicação de terceiros",
+ "labelPrompt": "As etiquetas servem como um meio de categorizar formulários semelhantes que podem pertencer a uma organização comum ou compartilhar um contexto relacionado.",
+ "useCaseToolTip": "Se você não encontrar o seu caso de uso específico, entre em contato com a equipe CHEFS para discutir outras opções",
+ "deploymentLevel": "Nível de Implementação",
+ "ministryName": "Ministério",
+ "label": "Etiquetas",
+ "useCase": "Tipo de Caso de Uso",
+ "development": "Desenvolvimento",
+ "test": "Teste",
+ "production": "Produção",
+ "Y": "Sim",
+ "N": "Não",
+ "application": "Aplicações que serão avaliadas seguidas por uma decisão",
+ "collection": "Coleta de Conjuntos de Dados, envio de dados",
+ "feedback": "Formulário de Feedback para determinar satisfação, concordância, probabilidade ou outras perguntas qualitativas",
+ "report": "Relatórios geralmente em uma programação repetitiva ou eventos programados como acompanhamentos",
+ "registration": "Registro ou Inscrição - sem avaliação",
+ "getLabelErr": "Ocorreu um erro ao tentar buscar rótulos de usuário.",
+ "getLabelConsErr": "Erro ao buscar rótulos: ",
+ "selectAPIErr": "Informe-nos sobre sua escolha em relação ao uso da Integração de API",
+ "selectDeploymentErr": "Por favor, selecione um nível de implementação",
+ "selectMinistryErr": "Por favor, selecione o seu ministério",
+ "selectUseCaseErr": "Por favor, informe o caso de uso do seu formulário",
+ "labelSizeErr": "Os rótulos não podem exceder 25 caracteres"
},
"ministries": {
"AF": "Agricultura e Alimentação (AF)",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Enviou e-mail não convidado para",
"updateUserErrMsg": "Ocorreu um erro ao tentar atualizar os usuários para este envio.",
"updateUserConsoleErrMsg": "Erro ao definir as permissões do usuário. Sub: {submissionId} Usuário: {userId} Erro: {error}",
- "searchInputLength": "A entrada de pesquisa para nome de usuário/e-mail BCeID deve ter mais de 6 caracteres.",
+ "searchInputLength": "A entrada de pesquisa para nome de usuário/e-mail BCeID deve ter mais de 4 caracteres.",
"exactBCEIDSearch": "As pesquisas de e-mail para BCeID devem ser exatas.",
"getUsersErrMsg": "Erro ao obter usuários: {error}",
"exactEmailOrUsername": "Digite um e-mail ou nome de usuário exato.",
@@ -1081,4 +1084,4 @@
"user": "Do utilizador"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/ru/ru.json b/app/frontend/src/internationalization/trans/chefs/ru/ru.json
index e54aaebbe..3fb7a82b3 100644
--- a/app/frontend/src/internationalization/trans/chefs/ru/ru.json
+++ b/app/frontend/src/internationalization/trans/chefs/ru/ru.json
@@ -169,7 +169,10 @@
"eventSubscription": "Подписка на события",
"validEndpointRequired": "Введите допустимый URL, начинающуюся с https://",
"validBearerTokenRequired": "Введите допустимый пример токена: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Широкая форма раскладки"
+ "wideFormLayout": "Широкая форма раскладки",
+ "formMetadataTitle": "Метаданные формы",
+ "formMetadataMessage": "Структурированная информация для описания или объяснения этой формы внешним системам. Вызовы внешних систем будут включать эти метаданные в свои полезные данные.",
+ "formMetadataJsonError": "Метаданные формы должны быть действительными в формате JSON. Используйте двойные кавычки вокруг атрибутов и значений."
},
"formProfile": {
"message": "Команда CHEFS собирает и систематизирует информацию для создания ключевых аргументов в пользу формирования всесторонних деловых кейсов. Эти кейсы будут играть решающую роль в направлении стратегической операции и постоянного совершенствования CHEFS в ближайшие годы. Эта инициатива по сбору данных является необходимой для принятия важных решений и формирования траектории CHEFS, обеспечивая его адаптивность и эффективность в решении изменяющихся потребностей и вызовов.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Отправлено письмо без приглашения на",
"updateUserErrMsg": "Произошла ошибка при попытке обновить пользователей для этой отправки.",
"updateUserConsoleErrMsg": "Ошибка установки разрешений пользователя. Sub: {submissionId} Пользователь: {userId} Ошибка: {error}",
- "searchInputLength": "Введите для поиска имя пользователя/адрес электронной почты BCeID, длина которого должна превышать 6 символов.",
+ "searchInputLength": "Введите для поиска имя пользователя/адрес электронной почты BCeID, длина которого должна превышать 4 символов.",
"exactBCEIDSearch": "Поиск по электронной почте для BCeID должен быть точным.",
"getUsersErrMsg": "Ошибка при получении пользователей: {error}",
"exactEmailOrUsername": "Введите точную электронную почту или имя пользователя.",
@@ -1081,4 +1084,4 @@
"user": "Пользователь"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/tl/tl.json b/app/frontend/src/internationalization/trans/chefs/tl/tl.json
index a6df3828c..56bf8856d 100644
--- a/app/frontend/src/internationalization/trans/chefs/tl/tl.json
+++ b/app/frontend/src/internationalization/trans/chefs/tl/tl.json
@@ -169,7 +169,10 @@
"eventSubscription": "Subscription sa Kaganapan",
"validEndpointRequired": "Mangyaring magpasok ng wastong endpoint na nagsisimula sa https://",
"validBearerTokenRequired": "Maglagay ng wastong halimbawa ng token ng maydala: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Malapad na Porma ng Layout"
+ "wideFormLayout": "Malapad na Porma ng Layout",
+ "formMetadataTitle": "Metadata ng Form",
+ "formMetadataMessage": "Nakabalangkas na impormasyon upang ilarawan o ipaliwanag ang form na ito sa mga panlabas na system. Isasama sa mga tawag sa mga external na system ang metadata na ito sa kanilang mga payload.",
+ "formMetadataJsonError": "Ang metadata ng form ay dapat na wastong JSON. Gumamit ng double-quotes sa paligid ng mga attribute at value."
},
"formProfile": {
"message": "Ang CHEFS team ay nagkokolekta at nag-oorganisa ng impormasyon upang magsilbing mahalagang input para sa pagbuo ng kapsulang kaso ng negosyo. Ang mga kaso na ito ay magiging pangunahing bahagi sa paggabay sa pangangasiwa at patuloy na pagpapabuti ng CHEFS sa mga darating na taon. Ang inisyatibang ito sa pagsasama ng datos ay mahalaga para sa pagbibigay impormasyon sa mga kritikal na desisyon at pagsanay sa takbo ng CHEFS, tiyak na nagiging adaptable at epektibo sa pagsasaad sa mga nagbabagong pangangailangan at hamon.",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Nagpadala ng hindi inanyayahang email kay",
"updateUserErrMsg": "May naganap na error habang sinusubukang i-update ang mga user para sa pagsusumiteng ito.",
"updateUserConsoleErrMsg": "Error sa pagtatakda ng mga pahintulot ng user. Sub: {submissionId} User: {userId} Error: {error}",
- "searchInputLength": "Ang input ng paghahanap para sa username/email ng BceID ay dapat na higit sa 6 na character.",
+ "searchInputLength": "Ang input ng paghahanap para sa username/email ng BceID ay dapat na higit sa 4 na character.",
"exactBCEIDSearch": "Dapat na eksakto ang mga paghahanap sa email para sa BceID.",
"getUsersErrMsg": "Error sa pagkuha ng mga user: {error}",
"exactEmailOrUsername": "Maglagay ng eksaktong e-mail o username.",
@@ -1079,4 +1082,4 @@
"user": "Gumagamit"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/uk/uk.json b/app/frontend/src/internationalization/trans/chefs/uk/uk.json
index 1ba0d44f5..35e574000 100644
--- a/app/frontend/src/internationalization/trans/chefs/uk/uk.json
+++ b/app/frontend/src/internationalization/trans/chefs/uk/uk.json
@@ -168,7 +168,10 @@
"allowEventSubscription": "Дозволити підписку на події",
"eventSubscription": "Підписка на подію",
"validEndpointRequired": "Введіть дійсну кінцеву точку, починаючи з https://",
- "wideFormLayout": "Широкий формат розкладки"
+ "wideFormLayout": "Широкий формат розкладки",
+ "formMetadataTitle": "Метадані форми",
+ "formMetadataMessage": "Структурована інформація для опису або пояснення цієї форми зовнішнім системам. Виклики зовнішніх систем включатимуть ці метадані у свої корисні навантаження.",
+ "formMetadataJsonError": "Метадані форми мають бути дійсними у форматі JSON. Використовуйте подвійні лапки навколо атрибутів і значень."
},
"formProfile": {
"message": "Команда CHEFS збирає та організовує інформацію для надання ключового внеску у створення всебічних бізнес-кейсів. Ці кейси відіграють вирішальну роль у напрямку стратегічної операції та постійного вдосконалення CHEFS у наступні роки. Ця ініціатива зібрати дані є важливою для інформування критичних рішень та формування траєкторії CHEFS, забезпечуючи його адаптивність та ефективність у вирішенні змінюючихся потреб і викликів.",
@@ -548,7 +551,7 @@
"sentUninvitedEmailTo": "Надіслано незапрошений електронний лист до",
"updateUserErrMsg": "Під час спроби оновити користувачів для цього подання сталася помилка.",
"updateUserConsoleErrMsg": "Помилка налаштування дозволів користувача. Sub: {submissionId} Користувач: {userId} Помилка: {error}",
- "searchInputLength": "Введення імені користувача/електронної адреси BCeID має містити більше 6 символів.",
+ "searchInputLength": "Введення імені користувача/електронної адреси BCeID має містити більше 4 символів.",
"exactBCEIDSearch": "Пошук електронної пошти для BCeID має бути точним.",
"getUsersErrMsg": "Помилка отримання користувачів: {error}",
"exactEmailOrUsername": "Введіть точний e-mail або ім'я користувача.",
@@ -1080,4 +1083,4 @@
"user": "Користувач"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/vi/vi.json b/app/frontend/src/internationalization/trans/chefs/vi/vi.json
index 430c073ad..c3522cfd6 100644
--- a/app/frontend/src/internationalization/trans/chefs/vi/vi.json
+++ b/app/frontend/src/internationalization/trans/chefs/vi/vi.json
@@ -169,7 +169,10 @@
"eventSubscription": "Đăng ký sự kiện",
"validEndpointRequired": "Vui lòng nhập một điểm cuối hợp lệ bắt đầu bằng https://",
"validBearerTokenRequired": "Nhập ví dụ về mã thông báo mang hợp lệ: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "Bố cục hình thức rộng"
+ "wideFormLayout": "Bố cục hình thức rộng",
+ "formMetadataTitle": "Siêu dữ liệu biểu mẫu",
+ "formMetadataMessage": "Thông tin có cấu trúc để mô tả hoặc giải thích biểu mẫu này cho các hệ thống bên ngoài. Các cuộc gọi đến hệ thống bên ngoài sẽ bao gồm siêu dữ liệu này trong tải trọng của chúng.",
+ "formMetadataJsonError": "Siêu dữ liệu biểu mẫu phải là JSON hợp lệ. Sử dụng dấu ngoặc kép xung quanh các thuộc tính và giá trị."
},
"formProfile": {
"message": "Đội ngũ CHEFS đang thu thập và tổ chức thông tin để phục vụ như một đầu vào quan trọng cho việc xây dựng các trường hợp kinh doanh toàn diện. Những trường hợp này sẽ đóng một vai trò quan trọng trong hướng dẫn vận hành chiến lược và cải tiến liên tục của CHEFS trong những năm sắp tới. Sáng kiến này để thu thập dữ liệu là quan trọng để thông tin quyết định quan trọng và định hình quỹ đạo của CHEFS, đảm bảo tính linh hoạt và hiệu quả trong đối mặt với những nhu cầu và thách thức đang thay đổi.",
@@ -225,7 +228,7 @@
"TACS": "Du lịch, Nghệ thuật, Văn hóa và Thể thao (TACS)",
"MOTI": "Giao thông vận tải và Cơ sở hạ tầng (MOTI)",
"WLRS": "Quản lý Nước, Đất và Tài nguyên (WLRS)"
- },
+ },
"subscribeEvent": {
"eventType": "Loại sự kiện",
"endpointUrl": "URL điểm cuối",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "Đã gửi email không mời đến",
"updateUserErrMsg": "Đã xảy ra lỗi khi cố gắng cập nhật người dùng cho lần gửi này.",
"updateUserConsoleErrMsg": "Lỗi đặt quyền của người dùng. Phụ: {submissionId} Người dùng: {userId} Lỗi: {error}",
- "searchInputLength": "Đầu vào tìm kiếm cho tên người dùng/email BCeID phải lớn hơn 6 ký tự.",
+ "searchInputLength": "Đầu vào tìm kiếm cho tên người dùng/email BCeID phải lớn hơn 4 ký tự.",
"exactBCEIDSearch": "Tìm kiếm email cho BCeID phải chính xác.",
"getUsersErrMsg": "Lỗi nhận người dùng: {error}",
"exactEmailOrUsername": "Nhập một e-mail hoặc tên người dùng chính xác.",
diff --git a/app/frontend/src/internationalization/trans/chefs/zh/zh.json b/app/frontend/src/internationalization/trans/chefs/zh/zh.json
index 531af0a66..22ea0ac6a 100644
--- a/app/frontend/src/internationalization/trans/chefs/zh/zh.json
+++ b/app/frontend/src/internationalization/trans/chefs/zh/zh.json
@@ -169,7 +169,10 @@
"eventSubscription": "E活动订阅",
"validEndpointRequired": "请输入以以下开头的有效端点 https://",
"validBearerTokenRequired": "输入有效的不记名令牌示例: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "宽表单布局"
+ "wideFormLayout": "宽表单布局",
+ "formMetadataTitle": "表单元数据",
+ "formMetadataMessage": "向外部系统描述或解释这种形式的结构化信息。对外部系统的调用将在其有效负载中包含此元数据。",
+ "formMetadataJsonError": "表单元数据必须是有效的 JSON。在属性和值周围使用双引号。"
},
"formProfile": {
"message": "CHEFS团队正在收集和组织信息,作为制定全面业务案例的关键输入。这些案例将在指导CHEFS未来几年的战略运作和持续改进中发挥关键作用。这一收集数据的倡议对于提供关键决策的信息和塑造CHEFS轨迹至关重要,确保其在应对不断变化的需求和挑战中的适应性和有效性。",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "发送了未经邀请的电子邮件至",
"updateUserErrMsg": "尝试更新此提交的用户时出错。",
"updateUserConsoleErrMsg": "设置用户权限时出错。子:{submissionId} 用户:{userId} 错误:{error}",
- "searchInputLength": "BCeID 用户名/电子邮件的搜索输入必须大于 6 个字符。",
+ "searchInputLength": "BCeID 用户名/电子邮件的搜索输入必须大于 4 个字符。",
"exactBCEIDSearch": "BCeID 的电子邮件搜索必须准确。",
"getUsersErrMsg": "获取用户时出错:{error}",
"exactEmailOrUsername": "输入准确的电子邮件或用户名。",
@@ -1081,4 +1084,4 @@
"user": "用户"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json
index 3761d8d46..6c7736429 100644
--- a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json
+++ b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json
@@ -169,7 +169,10 @@
"eventSubscription": "活動訂閱",
"validEndpointRequired": "請輸入以以下開頭的有效端點 https://",
"validBearerTokenRequired": "輸入有效的不記名令牌示例: 89abddfb-2cff-4fda-83e6-13221f0c3d4f",
- "wideFormLayout": "寬表單布局"
+ "wideFormLayout": "寬表單布局",
+ "formMetadataTitle": "表單元數據",
+ "formMetadataMessage": "向外部系統描述或解釋這種形式的結構化資訊。對外部系統的呼叫將在其有效負載中包含此元資料。",
+ "formMetadataJsonError": "表單元資料必須是有效的 JSON。在屬性和值周圍使用雙引號。"
},
"formProfile": {
"message": "CHEFS團隊正在收集和組織信息,作為制定全面業務案例的關鍵輸入。這些案例將在指導CHEFS未來幾年的戰略運作和持續改進中發揮關鍵作用。這一收集數據的倡議對於提供關鍵決策的信息和塑造CHEFS軌跡至關重要,確保其在應對不斷變化的需求和挑戰中的適應性和有效性。",
@@ -549,7 +552,7 @@
"sentUninvitedEmailTo": "發送了未經邀請的電子郵件至",
"updateUserErrMsg": "嘗試更新此提交的用戶時出錯。",
"updateUserConsoleErrMsg": "設置用戶權限時出錯。子:{submissionId} 用戶:{userId} 錯誤:{error}",
- "searchInputLength": "BCeID 用戶名/電子郵件的搜索輸入必須大於 6 個字符。",
+ "searchInputLength": "BCeID 用戶名/電子郵件的搜索輸入必須大於 4 個字符。",
"exactBCEIDSearch": "BCeID 的電子郵件搜索必須準確。",
"getUsersErrMsg": "獲取用戶時出錯:{error}",
"exactEmailOrUsername": "輸入準確的電子郵件或用戶名。",
@@ -1081,4 +1084,4 @@
"user": "用戶"
}
}
-}
\ No newline at end of file
+}
diff --git a/app/frontend/src/store/form.js b/app/frontend/src/store/form.js
index 2b6d9813c..3d8cf7d64 100644
--- a/app/frontend/src/store/form.js
+++ b/app/frontend/src/store/form.js
@@ -59,7 +59,11 @@ const genInitialSubscribeDetails = () => ({
endpointToken: null,
key: '',
});
-
+const genInitialFormMetadata = () => ({
+ id: null,
+ formId: null,
+ metadata: {},
+});
const genInitialForm = () => ({
description: '',
enableSubmitterDraft: false,
@@ -85,6 +89,7 @@ const genInitialForm = () => ({
apiIntegration: null,
useCase: null,
wideFormLayout: false,
+ formMetadata: genInitialFormMetadata(),
});
export const useFormStore = defineStore('form', {
@@ -328,7 +333,9 @@ export const useFormStore = defineStore('form', {
...genInitialSubscribe(),
...data.subscribe,
};
-
+ if (!data.formMetadata) {
+ data.formMetadata = genInitialFormMetadata();
+ }
this.form = data;
} catch (error) {
const notificationStore = useNotificationStore();
@@ -417,7 +424,7 @@ export const useFormStore = defineStore('form', {
const subscribe = this.form.subscribe.enabled
? this.form.subscribe
: {};
-
+ const formMetadata = this.form.formMetadata;
await formService.updateForm(this.form.id, {
name: this.form.name,
description: this.form.description,
@@ -445,6 +452,7 @@ export const useFormStore = defineStore('form', {
enableCopyExistingSubmission: this.form.enableCopyExistingSubmission
? this.form.enableCopyExistingSubmission
: false,
+ formMetadata: formMetadata,
});
// update user labels with any new added labels
diff --git a/app/frontend/src/utils/constants.js b/app/frontend/src/utils/constants.js
index 7d9bdcbc4..05bb2441f 100755
--- a/app/frontend/src/utils/constants.js
+++ b/app/frontend/src/utils/constants.js
@@ -16,6 +16,7 @@ export const ApiRoutes = Object.freeze({
FILES_API_ACCESS: '/filesApiAccess',
PROXY: '/proxy',
EXTERNAL_APIS: '/externalAPIs',
+ FORM_METADATA: '/formMetadata',
});
/** Roles a user can have on a form. These are defined in the DB and sent from the API */
diff --git a/app/frontend/tests/unit/components/designer/FormViewer.spec.js b/app/frontend/tests/unit/components/designer/FormViewer.spec.js
index b1bc4b02c..6f9cabb9f 100644
--- a/app/frontend/tests/unit/components/designer/FormViewer.spec.js
+++ b/app/frontend/tests/unit/components/designer/FormViewer.spec.js
@@ -170,7 +170,7 @@ describe('FormViewer.vue', () => {
getDispositionSpy.mockImplementation(() => {});
});
- it('formScheduleExpireMessage returns the formScheduleExpireMessage translation', async () => {
+ it('formScheduleExpireMessage returns the formScheduleExpireMessage translation or custom message', async () => {
const wrapper = shallowMount(FormViewer, {
props: {
formId: formId,
@@ -187,9 +187,19 @@ describe('FormViewer.vue', () => {
await flushPromises();
+ wrapper.vm.form = {};
+
expect(wrapper.vm.formScheduleExpireMessage).toEqual(
'trans.formViewer.formScheduleExpireMessage'
);
+
+ wrapper.vm.form = {
+ schedule: {
+ message: 'custom message',
+ },
+ };
+
+ expect(wrapper.vm.formScheduleExpireMessage).toEqual('custom message');
});
it('formUnauthorizedMessage returns the formUnauthorizedMessage translation', async () => {
diff --git a/app/frontend/tests/unit/components/designer/settings/FormMetadataSettings.spec.js b/app/frontend/tests/unit/components/designer/settings/FormMetadataSettings.spec.js
new file mode 100644
index 000000000..c0b88d5e3
--- /dev/null
+++ b/app/frontend/tests/unit/components/designer/settings/FormMetadataSettings.spec.js
@@ -0,0 +1,71 @@
+import { createTestingPinia } from '@pinia/testing';
+import { mount } from '@vue/test-utils';
+import { setActivePinia } from 'pinia';
+import { beforeEach, describe, expect, it } from 'vitest';
+import { nextTick, ref } from 'vue';
+
+import { useFormStore } from '~/store/form';
+import FormMetadataSettings from '~/components/designer/settings/FormMetadataSettings.vue';
+
+describe('FormMetadataSettings.vue', () => {
+ const pinia = createTestingPinia();
+ setActivePinia(pinia);
+
+ const formStore = useFormStore(pinia);
+
+ beforeEach(() => {
+ formStore.$reset();
+ });
+
+ it('sets JSON when valid JSON string', async () => {
+ formStore.form = ref({
+ formMetadata: {
+ metadata: {},
+ },
+ });
+ const wrapper = mount(FormMetadataSettings, {
+ global: {
+ plugins: [pinia],
+ stubs: {
+ BasePanel: {
+ name: 'BasePanel',
+ template: '
',
+ },
+ },
+ },
+ });
+
+ expect(formStore.form.formMetadata.metadata).toEqual({});
+ wrapper.vm.updateMetadata(JSON.stringify({ test: 'updated' }));
+ await nextTick();
+ expect(formStore.form.formMetadata.metadata).toEqual({ test: 'updated' });
+ });
+
+ it('does not set JSON when invalid JSON string', async () => {
+ formStore.form = ref({
+ formMetadata: {
+ metadata: { test: 'not updated' },
+ },
+ });
+ const wrapper = mount(FormMetadataSettings, {
+ global: {
+ plugins: [pinia],
+ stubs: {
+ BasePanel: {
+ name: 'BasePanel',
+ template: '
',
+ },
+ },
+ },
+ });
+
+ expect(formStore.form.formMetadata.metadata).toEqual({
+ test: 'not updated',
+ });
+ wrapper.vm.updateMetadata('this is not a valid JSON string.');
+ await nextTick();
+ expect(formStore.form.formMetadata.metadata).toEqual({
+ test: 'not updated',
+ });
+ });
+});
diff --git a/app/frontend/tests/unit/components/forms/manage/AddTeamMember.spec.js b/app/frontend/tests/unit/components/forms/manage/AddTeamMember.spec.js
index c44538518..1bf961a51 100644
--- a/app/frontend/tests/unit/components/forms/manage/AddTeamMember.spec.js
+++ b/app/frontend/tests/unit/components/forms/manage/AddTeamMember.spec.js
@@ -62,7 +62,7 @@ const BCEIDBASIC = {
},
text: {
message: 'trans.manageSubmissionUsers.searchInputLength',
- minLength: 6,
+ minLength: 4,
},
},
userSearch: {
@@ -144,7 +144,7 @@ const BCEIDBUSINESS = {
},
text: {
message: 'trans.manageSubmissionUsers.searchInputLength',
- minLength: 6,
+ minLength: 4,
},
},
formAccessSettings: 'idim',
diff --git a/app/frontend/tests/unit/components/forms/manage/TeamManagement.spec.js b/app/frontend/tests/unit/components/forms/manage/TeamManagement.spec.js
index 71e311524..d29fb4e25 100644
--- a/app/frontend/tests/unit/components/forms/manage/TeamManagement.spec.js
+++ b/app/frontend/tests/unit/components/forms/manage/TeamManagement.spec.js
@@ -87,7 +87,7 @@ const BCEIDBASIC = {
},
text: {
message: 'trans.manageSubmissionUsers.searchInputLength',
- minLength: 6,
+ minLength: 4,
},
},
userSearch: {
@@ -169,7 +169,7 @@ const BCEIDBUSINESS = {
},
text: {
message: 'trans.manageSubmissionUsers.searchInputLength',
- minLength: 6,
+ minLength: 4,
},
},
formAccessSettings: 'idim',
diff --git a/app/frontend/tests/unit/components/forms/submission/ManageSubmissionUsers.spec.js b/app/frontend/tests/unit/components/forms/submission/ManageSubmissionUsers.spec.js
index 66c8aede1..da414ba4c 100644
--- a/app/frontend/tests/unit/components/forms/submission/ManageSubmissionUsers.spec.js
+++ b/app/frontend/tests/unit/components/forms/submission/ManageSubmissionUsers.spec.js
@@ -161,7 +161,7 @@ describe('ManageSubmissionUsers.vue', () => {
wrapper.vm.selectedIdp = 'bceid-basic';
// should throw an error if search input is shorter than the min length specified by teamMebershipConfig
- wrapper.vm.onChangeUserSearchInput('john');
+ wrapper.vm.onChangeUserSearchInput('jon');
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
consoleErrorSpy.mockReset();
diff --git a/app/frontend/tests/unit/fixtures/identityProviders.json b/app/frontend/tests/unit/fixtures/identityProviders.json
index 67a31416d..08ba3e7e2 100644
--- a/app/frontend/tests/unit/fixtures/identityProviders.json
+++ b/app/frontend/tests/unit/fixtures/identityProviders.json
@@ -70,7 +70,7 @@
"addTeamMemberSearch": {
"text": {
"message": "trans.manageSubmissionUsers.searchInputLength",
- "minLength": 6
+ "minLength": 4
},
"email": {
"exact": true,
@@ -114,7 +114,7 @@
"addTeamMemberSearch": {
"text": {
"message": "trans.manageSubmissionUsers.searchInputLength",
- "minLength": 6
+ "minLength": 4
},
"email": {
"exact": true,
diff --git a/app/frontend/tests/unit/utils/constants.spec.js b/app/frontend/tests/unit/utils/constants.spec.js
index 17546e6f5..1e48ccd6e 100644
--- a/app/frontend/tests/unit/utils/constants.spec.js
+++ b/app/frontend/tests/unit/utils/constants.spec.js
@@ -16,6 +16,7 @@ describe('Constants', () => {
UTILS: '/utils',
FILES_API_ACCESS: '/filesApiAccess',
PROXY: '/proxy',
+ FORM_METADATA: '/formMetadata',
});
});
diff --git a/app/package-lock.json b/app/package-lock.json
index 9a3c48f0e..e0ebf463b 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -21,7 +21,7 @@
"config": "^3.3.9",
"cors": "^2.8.5",
"cryptr": "^6.3.0",
- "express": "^4.21.0",
+ "express": "^4.21.1",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.0",
"express-winston": "^4.2.0",
@@ -4831,9 +4831,9 @@
"dev": true
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@@ -7646,16 +7646,16 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -17037,9 +17037,9 @@
"dev": true
},
"cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -19179,16 +19179,16 @@
}
},
"express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
diff --git a/app/package.json b/app/package.json
index 164e997e8..d01ad2b55 100644
--- a/app/package.json
+++ b/app/package.json
@@ -59,7 +59,7 @@
"config": "^3.3.9",
"cors": "^2.8.5",
"cryptr": "^6.3.0",
- "express": "^4.21.0",
+ "express": "^4.21.1",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.0",
"express-winston": "^4.2.0",
diff --git a/app/src/db/migrations/20241010164117_049-update-idp-extra-length.js b/app/src/db/migrations/20241010164117_049-update-idp-extra-length.js
new file mode 100644
index 000000000..350e91ba1
--- /dev/null
+++ b/app/src/db/migrations/20241010164117_049-update-idp-extra-length.js
@@ -0,0 +1,84 @@
+const BCEID_EXTRAS = {
+ formAccessSettings: 'idim',
+ userSearch: {
+ filters: [
+ { name: 'filterIdpUserId', param: 'idpUserId', required: 0 },
+ { name: 'filterIdpCode', param: 'idpCode', required: 0 },
+ { name: 'filterUsername', param: 'username', required: 2, exact: true },
+ { name: 'filterFullName', param: 'fullName', required: 0 },
+ { name: 'filterFirstName', param: 'firstName', required: 0 },
+ { name: 'filterLastName', param: 'lastName', required: 0 },
+ { name: 'filterEmail', param: 'email', required: 2, exact: true },
+ { name: 'filterSearch', param: 'search', required: 0 },
+ ],
+ detail: 'Could not retrieve BCeID users. Invalid options provided.',
+ },
+};
+
+const BCEID_EXTRAS_OLD = {
+ ...BCEID_EXTRAS,
+ addTeamMemberSearch: {
+ text: {
+ minLength: 6,
+ message: 'trans.manageSubmissionUsers.searchInputLength',
+ },
+ email: {
+ exact: true,
+ message: 'trans.manageSubmissionUsers.exactBCEIDSearch',
+ },
+ },
+};
+
+const BCEID_EXTRAS_NEW = {
+ ...BCEID_EXTRAS,
+ addTeamMemberSearch: {
+ text: {
+ minLength: 4,
+ message: 'trans.manageSubmissionUsers.searchInputLength',
+ },
+ email: {
+ exact: true,
+ message: 'trans.manageSubmissionUsers.exactBCEIDSearch',
+ },
+ },
+};
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+exports.up = function (knex) {
+ return Promise.resolve().then(() =>
+ knex.schema
+ .then(() =>
+ knex('identity_provider').where({ code: 'bceid-business' }).update({
+ extra: BCEID_EXTRAS_NEW,
+ })
+ )
+ .then(() =>
+ knex('identity_provider').where({ code: 'bceid-basic' }).update({
+ extra: BCEID_EXTRAS_NEW,
+ })
+ )
+ );
+};
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+exports.down = function (knex) {
+ return Promise.resolve().then(() =>
+ knex.schema
+ .then(() =>
+ knex('identity_provider').where({ code: 'bceid-business' }).update({
+ extra: BCEID_EXTRAS_OLD,
+ })
+ )
+ .then(() =>
+ knex('identity_provider').where({ code: 'bceid-basic' }).update({
+ extra: BCEID_EXTRAS_OLD,
+ })
+ )
+ );
+};
diff --git a/app/src/db/migrations/20241016164117__050-form-metadata.js b/app/src/db/migrations/20241016164117__050-form-metadata.js
new file mode 100644
index 000000000..7da626a84
--- /dev/null
+++ b/app/src/db/migrations/20241016164117__050-form-metadata.js
@@ -0,0 +1,25 @@
+const stamps = require('../stamps');
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+
+exports.up = function (knex) {
+ return Promise.resolve().then(() =>
+ knex.schema.createTable('form_metadata', (table) => {
+ table.uuid('id').primary();
+ table.uuid('formId').references('id').inTable('form').notNullable().index();
+ table.jsonb('metadata');
+ stamps(knex, table);
+ })
+ );
+};
+
+/**
+ * @param { import("knex").Knex } knex
+ * @returns { Promise }
+ */
+exports.down = function (knex) {
+ return Promise.resolve().then(() => knex.schema.dropTableIfExists('form_metadata'));
+};
diff --git a/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js b/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js
new file mode 100644
index 000000000..047051eff
--- /dev/null
+++ b/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js
@@ -0,0 +1,89 @@
+exports.up = function (knex) {
+ return Promise.resolve()
+ .then(() => knex.schema.dropViewIfExists('submissions_data_vw'))
+ .then(() =>
+ knex.schema.raw(`CREATE OR REPLACE VIEW public.submissions_data_vw
+ AS SELECT s."confirmationId",
+ s."formName",
+ s.version,
+ s."createdAt",
+ CASE
+ WHEN u.id IS NULL THEN 'public'::character varying(255)
+ ELSE u."fullName"
+ END AS "fullName",
+ CASE
+ WHEN u.id IS NULL THEN 'public'::character varying(255)
+ ELSE u.username
+ END AS username,
+ u.email,
+ fs.submission -> 'data'::text AS submission,
+ fs."updatedAt",
+ fs."updatedBy",
+ s.deleted,
+ s.draft,
+ s."submissionId",
+ s."formId",
+ s."formVersionId",
+ u.id AS "userId",
+ u."idpUserId",
+ u."firstName",
+ u."lastName",
+ s."formSubmissionStatusCode" AS status,
+ s."formSubmissionAssignedToFullName" AS assignee,
+ s."formSubmissionAssignedToEmail" AS "assigneeEmail",
+ fss."createdAt" AS "submittedAt"
+ FROM submissions_vw s
+ JOIN form_submission fs ON s."submissionId" = fs.id
+ LEFT JOIN form_submission_user fsu ON s."submissionId" = fsu."formSubmissionId" AND fsu.permission::text = 'submission_create'::text
+ LEFT JOIN "user" u ON fsu."userId" = u.id
+ JOIN (
+ SELECT form_submission_status."submissionId", form_submission_status."createdAt", ROW_NUMBER() OVER (PARTITION BY form_submission_status."submissionId" ORDER BY form_submission_status."createdAt" DESC) AS rn
+ FROM form_submission_status where form_submission_status.code='SUBMITTED'
+) fss
+ON s."submissionId" = fss."submissionId" WHERE fss.rn = 1
+ ORDER BY s."createdAt", s."formName", s.version;`)
+ );
+};
+
+exports.down = function (knex) {
+ return Promise.resolve()
+ .then(() => knex.schema.dropViewIfExists('submissions_data_vw'))
+ .then(() =>
+ knex.schema.raw(
+ `CREATE OR REPLACE VIEW public.submissions_data_vw
+ AS SELECT s."confirmationId",
+ s."formName",
+ s.version,
+ s."createdAt",
+ CASE
+ WHEN u.id IS NULL THEN 'public'::character varying(255)
+ ELSE u."fullName"
+ END AS "fullName",
+ CASE
+ WHEN u.id IS NULL THEN 'public'::character varying(255)
+ ELSE u.username
+ END AS username,
+ u.email,
+ fs.submission -> 'data'::text AS submission,
+ fs."updatedAt",
+ fs."updatedBy",
+ s.deleted,
+ s.draft,
+ s."submissionId",
+ s."formId",
+ s."formVersionId",
+ u.id AS "userId",
+ u."idpUserId",
+ u."firstName",
+ u."lastName",
+ s."formSubmissionStatusCode" AS status,
+ s."formSubmissionAssignedToFullName" AS assignee,
+ s."formSubmissionAssignedToEmail" AS "assigneeEmail"
+ FROM submissions_vw s
+ JOIN form_submission fs ON s."submissionId" = fs.id
+ LEFT JOIN form_submission_user fsu ON s."submissionId" = fsu."formSubmissionId" AND fsu.permission::text = 'submission_create'::text
+ LEFT JOIN "user" u ON fsu."userId" = u.id
+ ORDER BY s."createdAt", s."formName", s.version;`
+ )
+ );
+};
diff --git a/app/src/docs/v1.api-spec.yaml b/app/src/docs/v1.api-spec.yaml
index 497ec73a2..867dd677b 100755
--- a/app/src/docs/v1.api-spec.yaml
+++ b/app/src/docs/v1.api-spec.yaml
@@ -3726,6 +3726,14 @@ components:
- $ref: '#/components/schemas/FormCore'
- type: object
properties:
+ formMetadata:
+ type: object
+ description: Contains Custom Form Metadata object
+ properties:
+ metadata:
+ type: object
+ description: JSON Object
+ example: { externalId: 'AB-0123456789' }
identityProviders:
type: array
items:
@@ -4044,6 +4052,10 @@ components:
createdAt:
type: string
example: '2020-06-04T18:49:20.672Z'
+ submittedAt:
+ type: string
+ description: Represents the timestamp indicating when a submission was last moved to the SUBMITTED state. If a submission is revised and resubmitted multiple times, submittedAt will update each time the state changes back to SUBMITTED, reflecting the most recent submission time.
+ example: '2020-06-04T18:49:20.672Z'
formFieldA:
type: string
description: A field in the submission object
diff --git a/app/src/forms/admin/routes.js b/app/src/forms/admin/routes.js
index 438202449..638075ac4 100644
--- a/app/src/forms/admin/routes.js
+++ b/app/src/forms/admin/routes.js
@@ -2,13 +2,10 @@ const routes = require('express').Router();
const jwtService = require('../../components/jwtService');
const currentUser = require('../auth/middleware/userAccess').currentUser;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const userController = require('../user/controller');
const controller = require('./controller');
-routes.use(rateLimiter);
-
// Routes under /admin fetch data without doing form permission checks. All
// routes in this file should remain under the "admin" role check, with the
// "admin" role only given to people who have permission to read all data.
diff --git a/app/src/forms/common/models/index.js b/app/src/forms/common/models/index.js
index fc313a9e2..ee96e239b 100644
--- a/app/src/forms/common/models/index.js
+++ b/app/src/forms/common/models/index.js
@@ -26,7 +26,7 @@ module.exports = {
FormSubscription: require('./tables/formSubscription'),
ExternalAPI: require('./tables/externalAPI'),
ExternalAPIStatusCode: require('./tables/externalAPIStatusCode'),
-
+ FormMetadata: require('./tables/formMetadata'),
// Views
FormSubmissionUserPermissions: require('./views/formSubmissionUserPermissions'),
PublicFormAccess: require('./views/publicFormAccess'),
diff --git a/app/src/forms/common/models/tables/form.js b/app/src/forms/common/models/tables/form.js
index 204c4c357..420b1d5be 100644
--- a/app/src/forms/common/models/tables/form.js
+++ b/app/src/forms/common/models/tables/form.js
@@ -34,6 +34,7 @@ class Form extends Timestamps(Model) {
const FormVersion = require('./formVersion');
const FormVersionDraft = require('./formVersionDraft');
const IdentityProvider = require('./identityProvider');
+ const FormMetadata = require('./formMetadata');
return {
drafts: {
relation: Model.HasManyRelation,
@@ -76,6 +77,14 @@ class Form extends Timestamps(Model) {
to: 'form_version.formId',
},
},
+ formMetadata: {
+ relation: Model.HasOneRelation,
+ modelClass: FormMetadata,
+ join: {
+ from: 'form.id',
+ to: 'form_metadata.formId',
+ },
+ },
};
}
diff --git a/app/src/forms/common/models/tables/formMetadata.js b/app/src/forms/common/models/tables/formMetadata.js
new file mode 100644
index 000000000..2caf1eee2
--- /dev/null
+++ b/app/src/forms/common/models/tables/formMetadata.js
@@ -0,0 +1,41 @@
+const { Model } = require('objection');
+const { Timestamps } = require('../mixins');
+const { Regex } = require('../../constants');
+const stamps = require('../jsonSchema').stamps;
+
+class FormMetadata extends Timestamps(Model) {
+ static get tableName() {
+ return 'form_metadata';
+ }
+
+ static get modifiers() {
+ return {
+ filterFormId(query, value) {
+ if (value) {
+ query.where('formId', value);
+ }
+ },
+ findByIdAndFormId(query, id, formId) {
+ if (id !== undefined && formId !== undefined) {
+ query.where('id', id).where('formId', formId);
+ }
+ },
+ };
+ }
+
+ static get jsonSchema() {
+ return {
+ type: 'object',
+ required: ['formId'],
+ properties: {
+ id: { type: 'string', pattern: Regex.UUID },
+ formId: { type: 'string', pattern: Regex.UUID },
+ metadata: { type: 'object' },
+ ...stamps,
+ },
+ additionalProperties: false,
+ };
+ }
+}
+
+module.exports = FormMetadata;
diff --git a/app/src/forms/common/utils.js b/app/src/forms/common/utils.js
index 456b8ab88..825a18350 100644
--- a/app/src/forms/common/utils.js
+++ b/app/src/forms/common/utils.js
@@ -3,6 +3,7 @@ const falsey = require('falsey');
const moment = require('moment');
const clone = require('lodash/clone');
const _ = require('lodash');
+const { ScheduleType } = require('./constants');
const setupMount = (type, app, routes) => {
const p = `/${type}`;
@@ -209,109 +210,98 @@ const checkIsFormExpired = (formSchedule = {}) => {
message: '',
};
- if (formSchedule && formSchedule.enabled) {
- //Check if Form open date is in past or Is form already started for submission
- if (formSchedule.openSubmissionDateTime) {
- let startDate = moment(formSchedule.openSubmissionDateTime).format('YYYY-MM-DD HH:MM:SS');
- let closingDate = null;
- if (formSchedule.scheduleType === 'closingDate' && formSchedule.closeSubmissionDateTime) {
- closingDate = moment(formSchedule.closeSubmissionDateTime).format('YYYY-MM-DD HH:MM:SS');
- }
- let isFormStartedAlready = moment().diff(startDate, 'seconds'); //If a positive number it means form get started
- if (isFormStartedAlready >= 0) {
- //Form have valid past open date for scheduling so lets check for the next conditions
- if (isFormStartedAlready && formSchedule.enabled && formSchedule.scheduleType !== 'manual') {
- if (formSchedule.closingMessageEnabled) {
- if (formSchedule.closingMessage) {
- result = { ...result, message: formSchedule.closingMessage };
- } else {
- result = { ...result, message: 'Something went wrong.' };
- }
- } else {
- result = { ...result, message: 'The form submission period has expired.' };
- }
+ if (formSchedule && formSchedule.enabled && formSchedule.openSubmissionDateTime) {
+ // The start date is the date that the form should be scheduled to be open to allow submissions
+ let startDate = moment(formSchedule.openSubmissionDateTime).startOf('day');
+ // The closing date is the date that the form should be scheduled to be closed
+ let closingDate = null;
+ if (formSchedule.scheduleType === ScheduleType.CLOSINGDATE && formSchedule.closeSubmissionDateTime) {
+ closingDate = moment(formSchedule.closeSubmissionDateTime).endOf('day');
+ }
- let closeDate =
- formSchedule.scheduleType === 'period'
- ? getCalculatedCloseSubmissionDate(
- startDate,
- formSchedule.keepOpenForTerm,
- formSchedule.keepOpenForInterval,
- formSchedule.allowLateSubmissions.enabled ? formSchedule.allowLateSubmissions.forNext.term : 0,
- formSchedule.allowLateSubmissions.forNext.intervalType,
- formSchedule.repeatSubmission.everyTerm,
- formSchedule.repeatSubmission.everyIntervalType,
- formSchedule.repeatSubmission.repeatUntil,
- formSchedule.scheduleType,
- formSchedule.closeSubmissionDateTime
- )
- : closingDate; //moment(formSchedule.closeSubmissionDateTime).format('YYYY-MM-DD HH:MM:SS');
- let isBetweenStartAndCloseDate = moment().isBetween(startDate, closeDate);
+ const currentMoment = moment();
- if (isBetweenStartAndCloseDate) {
- /** Check if form is Repeat enabled - start */
- /** Check if form is Repeat enabled and alow late submition - start */
- if (formSchedule.repeatSubmission.enabled) {
- let availableDates = getSubmissionPeriodDates(
+ let isFormStartedAlready = currentMoment.diff(startDate, 'seconds'); //If a positive number it means form get started
+
+ if (isFormStartedAlready >= 0) {
+ // The manual submission period does not have a custom closing message
+ if (formSchedule.scheduleType !== ScheduleType.MANUAL) {
+ if (formSchedule.closingMessageEnabled && formSchedule.closingMessage) {
+ result = { ...result, message: formSchedule.closingMessage };
+ }
+
+ let closeDate =
+ formSchedule.scheduleType === ScheduleType.PERIOD
+ ? getCalculatedCloseSubmissionDate(
+ startDate,
formSchedule.keepOpenForTerm,
formSchedule.keepOpenForInterval,
- startDate,
- formSchedule.repeatSubmission.everyTerm,
- formSchedule.repeatSubmission.everyIntervalType,
formSchedule.allowLateSubmissions.enabled ? formSchedule.allowLateSubmissions.forNext.term : 0,
formSchedule.allowLateSubmissions.forNext.intervalType,
- formSchedule.repeatSubmission.repeatUntil
- );
- for (let i = 0; i < availableDates.length; i++) {
- //Check if today is the day when a submitter can submit the form for given period of repeat submission
- let repeatIsBetweenStartAndCloseDate = moment().isBetween(availableDates[i].startDate, availableDates[i].closeDate);
-
- if (repeatIsBetweenStartAndCloseDate) {
- result = { ...result, expire: false }; //Form is available for given period to be submit.
+ formSchedule.repeatSubmission.everyTerm,
+ formSchedule.repeatSubmission.everyIntervalType,
+ formSchedule.repeatSubmission.repeatUntil,
+ formSchedule.scheduleType,
+ formSchedule.closeSubmissionDateTime
+ )
+ : closingDate;
+ let isBetweenStartAndCloseDate = currentMoment.isBetween(startDate, closeDate);
+ if (isBetweenStartAndCloseDate) {
+ if (formSchedule.repeatSubmission.enabled) {
+ // These are the available submission periods that a user can submit
+ let availableDates = getSubmissionPeriodDates(
+ formSchedule.keepOpenForTerm,
+ formSchedule.keepOpenForInterval,
+ startDate,
+ formSchedule.repeatSubmission.everyTerm,
+ formSchedule.repeatSubmission.everyIntervalType,
+ formSchedule.allowLateSubmissions.enabled ? formSchedule.allowLateSubmissions.forNext.term : 0,
+ formSchedule.allowLateSubmissions.forNext.intervalType,
+ formSchedule.repeatSubmission.repeatUntil
+ );
+ for (let i = 0; i < availableDates.length; i++) {
+ // Check if today is the day when a submitter can submit the form for given period of repeat submission
+ let repeatIsBetweenStartAndCloseDate = moment().isBetween(availableDates[i].startDate, availableDates[i].closeDate);
+
+ if (repeatIsBetweenStartAndCloseDate) {
+ result = { ...result, expire: false }; //Form is available for given period to be submit.
+ break;
+ } else if (formSchedule.allowLateSubmissions.enabled) {
+ result = { ...result, expire: true };
+ let isallowLateSubmissions = moment().isBetween(availableDates[i].startDate, availableDates[i].graceDate);
+ if (isallowLateSubmissions) {
+ //If late submission is allowed for the given repeat submission period then stop checking for other dates
+ result = {
+ ...result,
+ expire: true,
+ allowLateSubmissions: isallowLateSubmissions,
+ };
break;
- } else if (formSchedule.allowLateSubmissions.enabled) {
- result = { ...result, expire: true };
- /** Check if form is alow late submition - start */
- let isallowLateSubmissions = moment().isBetween(availableDates[i].startDate, availableDates[i].graceDate);
- if (isallowLateSubmissions) {
- //If late submission is allowed for the given repeat submission period then stop checking for other dates
- result = {
- ...result,
- expire: true,
- allowLateSubmissions: isallowLateSubmissions,
- };
- break;
- }
- /** Check if form is alow late submition - end */
- } else {
- result = { ...result, expire: true, allowLateSubmissions: false };
}
+ } else {
+ result = { ...result, expire: true, allowLateSubmissions: false };
}
}
- /** Check if form is Repeat enabled and alow late submition - end */
- /** Check if form is Repeat enabled - end */
+ }
+ } else {
+ // Block form submission but check if the designer allowed for late submissions
+ if (formSchedule.allowLateSubmissions.enabled) {
+ result = {
+ ...result,
+ expire: true,
+ allowLateSubmissions: isEligibleLateSubmission(closeDate, formSchedule.allowLateSubmissions.forNext.term, formSchedule.allowLateSubmissions.forNext.intervalType),
+ };
} else {
- //if close date not valid or not-in future OR close date not in between start and Today then block formSubmission but check the late submission if allowed
-
- if (formSchedule.allowLateSubmissions.enabled) {
- /** Check if form is alow late submition - start */
- result = {
- ...result,
- expire: true,
- allowLateSubmissions: isEligibleLateSubmission(closeDate, formSchedule.allowLateSubmissions.forNext.term, formSchedule.allowLateSubmissions.forNext.intervalType),
- };
- /** Check if form is alow late submition - end */
- } else {
- result = { ...result, expire: true, allowLateSubmissions: false };
- }
+ result = { ...result, expire: true, allowLateSubmissions: formSchedule.allowLateSubmissions.enabled };
}
}
- } else {
- //Form schedule open date is in the future so form will not be available for submission
- result = { ...result, expire: true, allowLateSubmissions: false, message: 'This form is not yet available for submission.' };
}
+ } else {
+ // The open submission date time is a future date time, so the form is not yet available for submission
+ result = { ...result, expire: true, allowLateSubmissions: formSchedule.allowLateSubmissions.enabled, message: 'This form is not yet available for submission.' };
}
}
+
return result;
};
@@ -361,12 +351,12 @@ const getSubmissionPeriodDates = (
let graceDate = null;
calculatedCloseDate.add(keepOpenForTerm, keepOpenForInterval);
- if (allowLateTerm && allowLateInterval) graceDate = calculatedCloseDate.clone().add(allowLateTerm, allowLateInterval).format('YYYY-MM-DD HH:MM:SS');
+ if (allowLateTerm && allowLateInterval) graceDate = calculatedCloseDate.clone().add(allowLateTerm, allowLateInterval);
// Always push through the first submission period
submissionPeriodDates.push({
- startDate: openSubmissionDate.clone().format('YYYY-MM-DD HH:MM:SS'),
- closeDate: calculatedCloseDate.format('YYYY-MM-DD HH:MM:SS'),
+ startDate: openSubmissionDate.clone(),
+ closeDate: calculatedCloseDate,
graceDate: graceDate,
});
@@ -383,12 +373,12 @@ const getSubmissionPeriodDates = (
calculatedCloseDate = openSubmissionDate.clone().add(keepOpenForTerm, keepOpenForInterval);
// If late submissions are enabled, set the grace period equal to the closing date
// with the addition of the late period
- if (allowLateTerm && allowLateInterval) graceDate = calculatedCloseDate.clone().add(allowLateTerm, allowLateInterval).format('YYYY-MM-DD HH:MM:SS');
+ if (allowLateTerm && allowLateInterval) graceDate = calculatedCloseDate.clone().add(allowLateTerm, allowLateInterval);
// Add the calculated dates to the submission period array
submissionPeriodDates.push({
- startDate: openSubmissionDate.clone().format('YYYY-MM-DD HH:MM:SS'),
- closeDate: calculatedCloseDate.format('YYYY-MM-DD HH:MM:SS'),
+ startDate: openSubmissionDate.clone(),
+ closeDate: calculatedCloseDate,
graceDate: graceDate,
});
@@ -432,13 +422,13 @@ const getCalculatedCloseSubmissionDate = (
repeatSubmissionUntil = moment(repeatSubmissionUntil);
if (!allowLateTerm && !repeatSubmissionTerm) {
- calculatedCloseDate = openDate.add(keepOpenForTerm, keepOpenForInterval).format('YYYY-MM-DD HH:MM:SS');
+ calculatedCloseDate = openDate.add(keepOpenForTerm, keepOpenForInterval);
} else {
if (repeatSubmissionTerm && repeatSubmissionInterval && repeatSubmissionUntil) {
calculatedCloseDate = repeatSubmissionUntil;
}
if (allowLateTerm && allowLateInterval) {
- calculatedCloseDate = calculatedCloseDate.add(keepOpenForTerm, keepOpenForInterval).add(allowLateTerm, allowLateInterval).format('YYYY-MM-DD HH:MM:SS');
+ calculatedCloseDate = calculatedCloseDate.add(keepOpenForTerm, keepOpenForInterval).add(allowLateTerm, allowLateInterval);
}
}
@@ -622,7 +612,7 @@ const validateScheduleObject = (schedule = {}) => {
let schType = schedule.scheduleType;
let openSubmissionDateTime = schedule.openSubmissionDateTime;
if (isDateValid(openSubmissionDateTime)) {
- if (schType === 'closingDate') {
+ if (schType === ScheduleType.CLOSINGDATE) {
if (!isDateValid(schedule.closeSubmissionDateTime)) {
result = {
message: 'Invalid closed submission date.',
@@ -646,7 +636,7 @@ const validateScheduleObject = (schedule = {}) => {
};
return result;
}
- } else if (schType === 'period') {
+ } else if (schType === ScheduleType.PERIOD) {
if (!isLateSubmissionObjValid(schedule)) {
result = {
message: 'Invalid late submission data.',
@@ -681,7 +671,7 @@ const validateScheduleObject = (schedule = {}) => {
return result;
}
} else {
- if (schType !== 'manual') {
+ if (schType !== ScheduleType.MANUAL) {
result = {
message: 'Invalid schedule type.',
status: 'error',
diff --git a/app/src/forms/event/eventService.js b/app/src/forms/event/eventService.js
index eeea7d144..74b8b862c 100644
--- a/app/src/forms/event/eventService.js
+++ b/app/src/forms/event/eventService.js
@@ -3,6 +3,7 @@ const { SubscriptionEvent } = require('../common/constants');
const { FormVersion, Form, FormSubscription } = require('../common/models');
const axios = require('axios');
const { queryUtils } = require('../common/utils');
+const formMetadataService = require('../form/formMetadata/service');
const service = {
/**
@@ -55,6 +56,8 @@ const service = {
jsonData['submissionId'] = submissionId;
}
+ await formMetadataService.addAttribute(formVersion.formId, jsonData);
+
axiosInstance.interceptors.request.use(
(cfg) => {
cfg.headers = { [subscribe.key]: `${subscribe.endpointToken}` };
diff --git a/app/src/forms/file/routes.js b/app/src/forms/file/routes.js
index ae65e0b19..95e21c232 100644
--- a/app/src/forms/file/routes.js
+++ b/app/src/forms/file/routes.js
@@ -3,13 +3,11 @@ const routes = require('express').Router();
const apiAccess = require('../auth/middleware/apiAccess');
const { currentUser } = require('../auth/middleware/userAccess');
const P = require('../common/constants').Permissions;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const controller = require('./controller');
const { currentFileRecord, hasFileCreate, hasFilePermissions } = require('./middleware/filePermissions');
const fileUpload = require('./middleware/upload').fileUpload;
-routes.use(rateLimiter);
routes.use(currentUser);
routes.param('fileId', validateParameter.validateFileId);
diff --git a/app/src/forms/form/exportService.js b/app/src/forms/form/exportService.js
index 29268cdb5..e353cb609 100644
--- a/app/src/forms/form/exportService.js
+++ b/app/src/forms/form/exportService.js
@@ -139,7 +139,7 @@ const service = {
_submissionsColumns: (form, params) => {
// Custom columns not defined - return default column selection behavior
- let columns = ['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email'];
+ let columns = ['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email', 'submittedAt'];
// if form has 'status updates' enabled in the form settings include these in export
if (form.enableStatusUpdates) {
columns = columns.concat(['status', 'assignee', 'assigneeEmail']);
diff --git a/app/src/forms/form/formMetadata/service.js b/app/src/forms/form/formMetadata/service.js
new file mode 100644
index 000000000..eac773916
--- /dev/null
+++ b/app/src/forms/form/formMetadata/service.js
@@ -0,0 +1,98 @@
+const Problem = require('api-problem');
+
+const { v4: uuidv4 } = require('uuid');
+
+const { FormMetadata } = require('../../common/models');
+const { typeUtils } = require('../../common/utils');
+
+const DEFAULT_HEADERNAME = 'X-FORM-METADATA';
+const DEFAULT_ATTRIBUTENAME = 'formMetadata';
+
+const service = {
+ validate: (data) => {
+ if (!data) {
+ throw new Problem(422, `'formMetadata record' cannot be empty.`);
+ }
+ },
+
+ initModel: (formId, data) => {
+ return {
+ id: uuidv4(),
+ formId: formId,
+ metadata: data.metadata ? data.metadata : {},
+ };
+ },
+
+ read: async (formId) => {
+ return FormMetadata.query().modify('filterFormId', formId).first();
+ },
+
+ hasMetadata: (formMetadata) => {
+ return formMetadata && formMetadata.metadata && Object.keys(formMetadata.metadata).length;
+ },
+
+ addMetadataToObject: async (formId, data, name, encode) => {
+ if (!formId || !data || !name) return;
+ if (![DEFAULT_ATTRIBUTENAME, DEFAULT_HEADERNAME].includes(name)) return;
+
+ if (data && typeUtils.isObject(data)) {
+ const o = await service.read(formId);
+ if (service.hasMetadata(o)) {
+ let value = o.metadata;
+ if (encode) {
+ let bufferObj = Buffer.from(JSON.stringify(o.metadata), 'utf8');
+ value = bufferObj.toString('base64');
+ }
+ data[name] = value;
+ }
+ }
+ },
+
+ addAttribute: async (formId, obj) => {
+ return await service.addMetadataToObject(formId, obj, DEFAULT_ATTRIBUTENAME);
+ },
+
+ addHeader: async (formId, headers) => {
+ return await service.addMetadataToObject(formId, headers, DEFAULT_HEADERNAME, true);
+ },
+
+ _insert: async (formId, data, currentUser, transaction) => {
+ const rec = service.initModel(formId, data);
+ await FormMetadata.query(transaction).insert({
+ ...rec,
+ createdBy: currentUser.usernameIdp,
+ });
+ },
+
+ _update: async (existing, data, currentUser, transaction) => {
+ data.id = existing.id; //make sure that id wasn't changed in transit
+ await FormMetadata.query(transaction)
+ .findById(existing.id)
+ .update({
+ ...data,
+ updatedBy: currentUser.usernameIdp,
+ });
+ },
+
+ upsert: async (formId, data, currentUser, transaction) => {
+ service.validate(data);
+ const externalTrx = transaction != undefined;
+ let trx;
+ try {
+ trx = externalTrx ? transaction : await FormMetadata.startTransaction();
+ const existing = await service.read(formId);
+ if (existing) {
+ await service._update(existing, data, currentUser, transaction);
+ } else {
+ await service._insert(formId, data, currentUser, transaction);
+ }
+ if (!externalTrx) trx.commit();
+ } catch (err) {
+ if (!externalTrx && trx) await trx.rollback();
+ throw err;
+ }
+ return service.read(formId);
+ },
+};
+
+module.exports = service;
diff --git a/app/src/forms/form/routes.js b/app/src/forms/form/routes.js
index d462e338b..10c3572bc 100644
--- a/app/src/forms/form/routes.js
+++ b/app/src/forms/form/routes.js
@@ -4,11 +4,9 @@ const jwtService = require('../../components/jwtService');
const apiAccess = require('../auth/middleware/apiAccess');
const { currentUser, hasFormPermissions } = require('../auth/middleware/userAccess');
const P = require('../common/constants').Permissions;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const controller = require('./controller');
-routes.use(rateLimiter);
routes.use(currentUser);
routes.param('documentTemplateId', validateParameter.validateDocumentTemplateId);
diff --git a/app/src/forms/form/service.js b/app/src/forms/form/service.js
index b65f3316d..44f6652f7 100644
--- a/app/src/forms/form/service.js
+++ b/app/src/forms/form/service.js
@@ -25,6 +25,7 @@ const {
} = require('../common/models');
const { falsey, queryUtils, checkIsFormExpired, validateScheduleObject, typeUtils } = require('../common/utils');
const { Permissions, Roles, Statuses } = require('../common/constants');
+const formMetadataService = require('./formMetadata/service');
const Rolenames = [Roles.OWNER, Roles.TEAM_MANAGER, Roles.FORM_DESIGNER, Roles.SUBMISSION_REVIEWER, Roles.FORM_SUBMITTER, Roles.SUBMISSION_APPROVER];
const service = {
@@ -133,6 +134,8 @@ const service = {
}));
await FormStatusCode.query(trx).insert(defaultStatuses);
+ await formMetadataService.upsert(obj.id, data.formMetadata, currentUser, trx);
+
await trx.commit();
const result = await service.readForm(obj.id);
result.draft = draft;
@@ -189,6 +192,8 @@ const service = {
}));
if (fIdps && fIdps.length) await FormIdentityProvider.query(trx).insert(fIdps);
+ await formMetadataService.upsert(obj.id, data.formMetadata, currentUser, trx);
+
await trx.commit();
const result = await service.readForm(obj.id);
return result;
@@ -224,7 +229,8 @@ const service = {
return Form.query()
.findById(formId)
.modify('filterActive', params.active)
- .allowGraph('[identityProviders,versions]')
+ .allowGraph('[formMetadata,identityProviders,versions]')
+ .withGraphFetched('formMetadata')
.withGraphFetched('identityProviders(orderDefault)')
.withGraphFetched('versions(selectWithoutSchema, orderVersionDescending)')
.throwIfNotFound();
diff --git a/app/src/forms/permission/routes.js b/app/src/forms/permission/routes.js
index fe6205497..02a43e39f 100644
--- a/app/src/forms/permission/routes.js
+++ b/app/src/forms/permission/routes.js
@@ -2,11 +2,9 @@ const routes = require('express').Router();
const jwtService = require('../../components/jwtService');
const currentUser = require('../auth/middleware/userAccess').currentUser;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const controller = require('./controller');
-routes.use(rateLimiter);
routes.use(jwtService.protect('admin'));
routes.use(currentUser);
diff --git a/app/src/forms/proxy/service.js b/app/src/forms/proxy/service.js
index c624fc738..fdf1f8605 100644
--- a/app/src/forms/proxy/service.js
+++ b/app/src/forms/proxy/service.js
@@ -2,6 +2,7 @@ const { encryptionService } = require('../../components/encryptionService');
const jwtService = require('../../components/jwtService');
const ProxyServiceError = require('./error');
const { ExternalAPI, SubmissionMetadata } = require('../../forms/common/models');
+const formMetadataService = require('../../forms/form/formMetadata/service');
const headerValue = (headers, key) => {
if (headers && key) {
@@ -99,6 +100,9 @@ const service = {
},
createExternalAPIHeaders: async (externalAPI, proxyHeaderInfo) => {
const result = {};
+ // form metadata, add if specified and there are attributes in the metadata.
+ await formMetadataService.addHeader(proxyHeaderInfo['formId'], result);
+
if (externalAPI.sendApiKey) {
result[externalAPI.apiKeyHeader] = externalAPI.apiKey;
}
diff --git a/app/src/forms/role/routes.js b/app/src/forms/role/routes.js
index 56262bbb3..86540f0b2 100644
--- a/app/src/forms/role/routes.js
+++ b/app/src/forms/role/routes.js
@@ -2,11 +2,9 @@ const routes = require('express').Router();
const jwtService = require('../../components/jwtService');
const currentUser = require('../auth/middleware/userAccess').currentUser;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const controller = require('./controller');
-routes.use(rateLimiter);
routes.use(currentUser);
routes.param('code', validateParameter.validateRoleCode);
diff --git a/app/src/forms/submission/routes.js b/app/src/forms/submission/routes.js
index 17d973105..b4c3e3f34 100644
--- a/app/src/forms/submission/routes.js
+++ b/app/src/forms/submission/routes.js
@@ -3,11 +3,9 @@ const routes = require('express').Router();
const apiAccess = require('../auth/middleware/apiAccess');
const { currentUser, hasSubmissionPermissions, filterMultipleSubmissions } = require('../auth/middleware/userAccess');
const P = require('../common/constants').Permissions;
-const rateLimiter = require('../common/middleware').apiKeyRateLimiter;
const validateParameter = require('../common/middleware/validateParameter');
const controller = require('./controller');
-routes.use(rateLimiter);
routes.use(currentUser);
routes.param('documentTemplateId', validateParameter.validateDocumentTemplateId);
diff --git a/app/src/forms/submission/service.js b/app/src/forms/submission/service.js
index dec60ea73..ae4f56301 100644
--- a/app/src/forms/submission/service.js
+++ b/app/src/forms/submission/service.js
@@ -20,7 +20,12 @@ const service = {
return await Promise.all([
FormSubmission.query().findById(meta.submissionId).throwIfNotFound(),
FormVersion.query().findById(meta.formVersionId).throwIfNotFound(),
- Form.query().findById(meta.formId).allowGraph('identityProviders').withGraphFetched('identityProviders(orderDefault)').throwIfNotFound(),
+ Form.query()
+ .findById(meta.formId)
+ .allowGraph('[formMetadata,identityProviders]')
+ .withGraphFetched('formMetadata')
+ .withGraphFetched('identityProviders(orderDefault)')
+ .throwIfNotFound(),
]).then((data) => {
return {
submission: data[0],
@@ -43,7 +48,12 @@ const service = {
return await Promise.all([
FormSubmission.query().findByIds(submissionIds).throwIfNotFound(),
FormVersion.query().findByIds(formVersionId).throwIfNotFound(),
- Form.query().findByIds(formId).allowGraph('identityProviders').withGraphFetched('identityProviders(orderDefault)').throwIfNotFound(),
+ Form.query()
+ .findByIds(formId)
+ .allowGraph('[formMetadata,identityProviders]')
+ .withGraphFetched('formMetadata')
+ .withGraphFetched('identityProviders(orderDefault)')
+ .throwIfNotFound(),
]).then((data) => {
return {
submission: data[0],
diff --git a/app/tests/fixtures/form/identity_providers.json b/app/tests/fixtures/form/identity_providers.json
index cf1a59615..cbc42aa71 100644
--- a/app/tests/fixtures/form/identity_providers.json
+++ b/app/tests/fixtures/form/identity_providers.json
@@ -113,7 +113,7 @@
"addTeamMemberSearch": {
"text": {
"message": "trans.manageSubmissionUsers.searchInputLength",
- "minLength": 6
+ "minLength": 4
},
"email": {
"exact": true,
@@ -199,7 +199,7 @@
"addTeamMemberSearch": {
"text": {
"message": "trans.manageSubmissionUsers.searchInputLength",
- "minLength": 6
+ "minLength": 4
},
"email": {
"exact": true,
diff --git a/app/tests/unit/forms/common/utils.spec.js b/app/tests/unit/forms/common/utils.spec.js
index 47c32ddb7..8b5e310b5 100644
--- a/app/tests/unit/forms/common/utils.spec.js
+++ b/app/tests/unit/forms/common/utils.spec.js
@@ -1,6 +1,8 @@
const config = require('config');
+const moment = require('moment');
-const { getBaseUrl, queryUtils, typeUtils, validateScheduleObject } = require('../../../../src/forms/common/utils');
+const { checkIsFormExpired, getBaseUrl, queryUtils, typeUtils, validateScheduleObject } = require('../../../../src/forms/common/utils');
+const { ScheduleType } = require('../../../../src/forms/common/constants');
jest.mock('config');
@@ -258,4 +260,210 @@ describe('Test Schedule object validation Utils functions', () => {
expect(result).toBeDefined();
expect(result).toHaveProperty('status', 'success');
});
+
+ it('checkIsFormExpired should return the default result object { allowLateSubmission: false, expire: false, message: "" }', () => {
+ expect(checkIsFormExpired()).toEqual({ allowLateSubmissions: false, expire: false, message: '' });
+ });
+
+ it('checkIsFormExpired should return a message that the form is not available yet if the open time is a future date { ...result, expire: true, allowLateSubmissions: false, message: "This form is not yet available for submission."', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ allowLateSubmissions: {
+ enabled: false,
+ },
+ openSubmissionDateTime: moment().add(1, 'days').format('YYYY-MM-DD'),
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: true,
+ message: 'This form is not yet available for submission.',
+ });
+ });
+
+ it('checkIsFormExpired should return a valid object for a manual schedule with a valid schedule ', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: ScheduleType.MANUAL,
+ allowLateSubmissions: {
+ enabled: false,
+ },
+ openSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: false,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired should append a closing message if it is enabled and a valid object for a valid schedule', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: ScheduleType.CLOSINGDATE,
+ closingMessageEnabled: true,
+ closingMessage: 'closing message',
+ allowLateSubmissions: {
+ enabled: false,
+ },
+ repeatSubmission: {
+ enabled: false,
+ },
+ openSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: moment().add(1, 'days').format('YYYY-MM-DD'),
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: false,
+ message: 'closing message',
+ });
+ });
+
+ it('checkIsFormExpired should return an expired object for a late schedule with no late submissions', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: ScheduleType.CLOSINGDATE,
+ allowLateSubmissions: {
+ enabled: false,
+ },
+ repeatSubmission: {
+ enabled: false,
+ },
+ openSubmissionDateTime: moment().subtract(2, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: true,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired should return an expired object but have allowLateSubmissions to be true for a late schedule with late submissions', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: ScheduleType.CLOSINGDATE,
+ allowLateSubmissions: {
+ enabled: true,
+ forNext: {
+ term: '1',
+ intervalType: 'days',
+ },
+ },
+ repeatSubmission: {
+ enabled: false,
+ },
+ openSubmissionDateTime: moment().subtract(2, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ })
+ ).toEqual({
+ allowLateSubmissions: true,
+ expire: true,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired for a period of 1 days should return an unexpired object in a valid schedule', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: 'period',
+ keepOpenForTerm: '1',
+ repeatSubmission: {
+ enabled: true,
+ everyTerm: '1',
+ repeatUntil: moment().add(1, 'month').format('YYYY-MM-DD'),
+ everyIntervalType: 'weeks',
+ },
+ keepOpenForInterval: 'days',
+ allowLateSubmissions: { enabled: true, forNext: { term: '1', intervalType: 'days' } },
+ closingMessageEnabled: false,
+ openSubmissionDateTime: moment().format('YYYY-MM-DD'),
+ closeSubmissionDateTime: null,
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: false,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired for a period on an expired day with late submissions should allow it', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: 'period',
+ keepOpenForTerm: '1',
+ repeatSubmission: {
+ enabled: true,
+ everyTerm: '1',
+ repeatUntil: moment().add(1, 'month').format('YYYY-MM-DD'),
+ everyIntervalType: 'weeks',
+ },
+ keepOpenForInterval: 'days',
+ allowLateSubmissions: { enabled: true, forNext: { term: '1', intervalType: 'days' } },
+ closingMessageEnabled: false,
+ openSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: null,
+ })
+ ).toEqual({
+ allowLateSubmissions: true,
+ expire: true,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired for a period on an expired day with no late submissions should disallow it', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: 'period',
+ keepOpenForTerm: '1',
+ repeatSubmission: {
+ enabled: true,
+ everyTerm: '1',
+ repeatUntil: moment().add(1, 'month').format('YYYY-MM-DD'),
+ everyIntervalType: 'weeks',
+ },
+ keepOpenForInterval: 'days',
+ allowLateSubmissions: { enabled: false, forNext: { term: '1', intervalType: 'days' } },
+ closingMessageEnabled: false,
+ openSubmissionDateTime: moment().subtract(1, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: null,
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: true,
+ message: '',
+ });
+ });
+
+ it('checkIsFormExpired for a period of 1 days should return an expired object in an invalid schedule', () => {
+ expect(
+ checkIsFormExpired({
+ enabled: true,
+ scheduleType: 'period',
+ keepOpenForTerm: '1',
+ repeatSubmission: {
+ enabled: false,
+ everyTerm: null,
+ repeatUntil: null,
+ everyIntervalType: null,
+ },
+ keepOpenForInterval: 'days',
+ allowLateSubmissions: { enabled: false, forNext: { term: '1', intervalType: 'days' } },
+ closingMessageEnabled: false,
+ openSubmissionDateTime: moment().subtract(5, 'days').format('YYYY-MM-DD'),
+ closeSubmissionDateTime: null,
+ })
+ ).toEqual({
+ allowLateSubmissions: false,
+ expire: true,
+ message: '',
+ });
+ });
});
diff --git a/app/tests/unit/forms/file/routes.spec.js b/app/tests/unit/forms/file/routes.spec.js
index 19a00ed35..59c5a3b4c 100644
--- a/app/tests/unit/forms/file/routes.spec.js
+++ b/app/tests/unit/forms/file/routes.spec.js
@@ -5,7 +5,6 @@ const { expressHelper } = require('../../../common/helper');
const apiAccess = require('../../../../src/forms/auth/middleware/apiAccess');
const userAccess = require('../../../../src/forms/auth/middleware/userAccess');
-const rateLimiter = require('../../../../src/forms/common/middleware/rateLimiter');
const validateParameter = require('../../../../src/forms/common/middleware/validateParameter');
const controller = require('../../../../src/forms/file/controller');
const filePermissions = require('../../../../src/forms/file/middleware/filePermissions');
@@ -36,10 +35,6 @@ filePermissions.hasFilePermissions = jest.fn(() => {
return hasFilePermissionsMock;
});
-rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => {
- next();
-});
-
upload.fileUpload.upload = jest.fn((_req, _res, next) => {
next();
});
@@ -80,7 +75,6 @@ describe(`${basePath}`, () => {
expect(filePermissions.currentFileRecord).toBeCalledTimes(0);
expect(filePermissions.hasFileCreate).toBeCalledTimes(1);
expect(hasFilePermissionsMock).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(upload.fileUpload.upload).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateFileId).toBeCalledTimes(0);
@@ -103,7 +97,6 @@ describe(`${basePath}/:id`, () => {
expect(filePermissions.currentFileRecord).toBeCalledTimes(1);
expect(filePermissions.hasFileCreate).toBeCalledTimes(0);
expect(hasFilePermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(upload.fileUpload.upload).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateFileId).toBeCalledTimes(1);
@@ -121,7 +114,6 @@ describe(`${basePath}/:id`, () => {
expect(filePermissions.currentFileRecord).toBeCalledTimes(1);
expect(filePermissions.hasFileCreate).toBeCalledTimes(0);
expect(hasFilePermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(upload.fileUpload.upload).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateFileId).toBeCalledTimes(1);
diff --git a/app/tests/unit/forms/form/exportService.spec.js b/app/tests/unit/forms/form/exportService.spec.js
index b57895ee2..404576902 100644
--- a/app/tests/unit/forms/form/exportService.spec.js
+++ b/app/tests/unit/forms/form/exportService.spec.js
@@ -81,7 +81,18 @@ describe('export', () => {
describe('type 1 / multiRowEmptySpacesCSVExport', () => {
const params = {
emailExport: false,
- fields: ['form.submissionId', 'form.confirmationId', 'form.formName', 'form.version', 'form.createdAt', 'form.fullName', 'form.username', 'form.email', 'simpletextfield'],
+ fields: [
+ 'form.submissionId',
+ 'form.confirmationId',
+ 'form.formName',
+ 'form.version',
+ 'form.createdAt',
+ 'form.fullName',
+ 'form.username',
+ 'form.email',
+ 'form.submittedAt',
+ 'simpletextfield',
+ ],
template: 'multiRowEmptySpacesCSVExport',
};
@@ -93,6 +104,7 @@ describe('export', () => {
formName: 'form',
version: 1,
createdAt: '2024-05-03T20:56:31.270Z',
+ submittedAt: '2024-05-03T20:56:31.270Z',
fullName: 'Pat Test',
username: 'PAT_TEST',
email: 'pat.test@gov.bc.ca',
@@ -125,6 +137,7 @@ describe('export', () => {
'form.fullName',
'form.username',
'form.email',
+ 'form.submittedAt',
'dataGrid',
'dataGrid.0.simpletextfield',
'dataGrid.1.simpletextfield',
@@ -143,6 +156,7 @@ describe('export', () => {
formName: 'form',
version: 1,
createdAt: '2024-05-03T20:56:31.270Z',
+ submittedAt: '2024-05-03T20:56:31.270Z',
fullName: 'Pat Test',
username: 'PAT_TEST',
email: 'pat.test@gov.bc.ca',
@@ -652,6 +666,7 @@ describe('', () => {
'form.fullName',
'form.username',
'form.email',
+ 'form.submittedAt',
'fishermansName',
'email',
'forWhichBcLakeRegionAreYouCompletingTheseQuestions',
@@ -682,7 +697,7 @@ describe('', () => {
expect(exportService._getData).toBeCalledTimes(1);
expect(exportService._buildCsvHeaders).toBeCalledTimes(1);
// test cases
- expect(fields.length).toEqual(19);
+ expect(fields.length).toEqual(20);
});
});
@@ -716,7 +731,7 @@ describe('_submissionsColumns', () => {
};
const submissions = exportService._submissionsColumns(form, params);
- expect(submissions.length).toEqual(9);
+ expect(submissions.length).toEqual(10);
expect(submissions).toEqual(expect.arrayContaining(['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email', 'submission']));
});
@@ -731,7 +746,7 @@ describe('_submissionsColumns', () => {
};
const submissions = exportService._submissionsColumns(form, params);
- expect(submissions.length).toEqual(10);
+ expect(submissions.length).toEqual(11);
});
it('should return right number of columns, when 1 prefered column (draft) passed as params.', async () => {
@@ -745,7 +760,7 @@ describe('_submissionsColumns', () => {
};
const submissions = exportService._submissionsColumns(form, params);
- expect(submissions.length).toEqual(10);
+ expect(submissions.length).toEqual(11);
});
it('should return right number of columns, when 2 prefered column (draft & deleted) passed as params.', async () => {
@@ -760,7 +775,7 @@ describe('_submissionsColumns', () => {
const submissions = exportService._submissionsColumns(form, params);
- expect(submissions.length).toEqual(11);
+ expect(submissions.length).toEqual(12);
});
it('should return right number of columns, when a garbage or NON-allowed column (testCol1 & testCol2) passed as params.', async () => {
@@ -774,7 +789,7 @@ describe('_submissionsColumns', () => {
};
const submissions = exportService._submissionsColumns(form, params);
- expect(submissions.length).toEqual(9);
+ expect(submissions.length).toEqual(10);
});
});
diff --git a/app/tests/unit/forms/form/externalApi/routes.spec.js b/app/tests/unit/forms/form/externalApi/routes.spec.js
index d31ac02ef..f7878945e 100644
--- a/app/tests/unit/forms/form/externalApi/routes.spec.js
+++ b/app/tests/unit/forms/form/externalApi/routes.spec.js
@@ -6,7 +6,6 @@ const { expressHelper } = require('../../../../common/helper');
const jwtService = require('../../../../../src/components/jwtService');
const apiAccess = require('../../../../../src/forms/auth/middleware/apiAccess');
const userAccess = require('../../../../../src/forms/auth/middleware/userAccess');
-const rateLimiter = require('../../../../../src/forms/common/middleware/rateLimiter');
const validateParameter = require('../../../../../src/forms/common/middleware/validateParameter');
const controller = require('../../../../../src/forms/form/externalApi/controller');
@@ -28,10 +27,6 @@ jwtService.protect = jest.fn(() => {
});
});
-rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => {
- next();
-});
-
const hasFormPermissionsMock = jest.fn((_req, _res, next) => {
next();
});
@@ -82,7 +77,6 @@ describe(`${basePath}/:formId/externalAPIs`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.listExternalAPIs).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -98,7 +92,6 @@ describe(`${basePath}/:formId/externalAPIs`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.createExternalAPI).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -126,7 +119,6 @@ describe(`${basePath}/:formId/externalAPIs/:externalAPIId`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.deleteExternalAPI).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(1);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -154,7 +146,6 @@ describe(`${basePath}/:formId/externalAPIs/:externalAPIId`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.updateExternalAPI).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(1);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -182,7 +173,6 @@ describe(`${basePath}/:formId/externalAPIs/algorithms`, () => {
expect(validateParameter.validateFormId).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(0);
expect(apiAccess).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(controller.listExternalAPIAlgorithms).toBeCalledTimes(1);
});
@@ -222,7 +212,6 @@ describe(`${basePath}/:formId/externalAPIs/statusCodes`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.listExternalAPIStatusCodes).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateExternalAPIId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
diff --git a/app/tests/unit/forms/form/formMetadata/service.spec.js b/app/tests/unit/forms/form/formMetadata/service.spec.js
new file mode 100644
index 000000000..ca331a100
--- /dev/null
+++ b/app/tests/unit/forms/form/formMetadata/service.spec.js
@@ -0,0 +1,470 @@
+const { MockModel, MockTransaction } = require('../../../../common/dbHelper');
+
+const uuid = require('uuid');
+
+const service = require('../../../../../src/forms/form/formMetadata/service');
+const { FormMetadata } = require('../../../../../src/forms/common/models');
+
+jest.mock('../../../../../src/forms/common/models/tables/formMetadata', () => MockModel);
+
+beforeEach(() => {
+ MockModel.mockReset();
+ MockTransaction.mockReset();
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+});
+
+describe('validate', () => {
+ let validData = null;
+ beforeEach(() => {
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+ it('should not throw errors with valid data', () => {
+ service.validate(validData);
+ });
+
+ it('should throw 422 with no data', () => {
+ expect(() => service.validate(undefined)).toThrow();
+ });
+});
+
+describe('initModel', () => {
+ it('should init with defaults', () => {
+ const res = service.initModel('123', {});
+ expect(res.id).toBeTruthy();
+ expect(res.formId).toEqual('123');
+ expect(res.metadata).toEqual({});
+ });
+
+ it('should init with existing values 422 with no data', () => {
+ const res = service.initModel('123', {
+ metadata: { externalId: '456' },
+ });
+ expect(res.id).toBeTruthy();
+ expect(res.formId).toEqual('123');
+ expect(res.metadata).toEqual({ externalId: '456' });
+ });
+});
+
+describe('upsert', () => {
+ const user = { usernameIdp: 'username' };
+ let validData = null;
+
+ beforeEach(() => {
+ MockModel.mockClear();
+ MockModel.mockReset();
+ MockTransaction.mockReset();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should update valid data', async () => {
+ MockModel.first = jest.fn().mockResolvedValue(Object.assign({}, validData));
+
+ await service.upsert(validData.formId, validData, user);
+ expect(MockModel.update).toBeCalledTimes(1);
+ expect(MockModel.update).toBeCalledWith({
+ updatedBy: user.usernameIdp,
+ ...validData,
+ });
+ expect(MockModel.startTransaction).toBeCalledTimes(1);
+ expect(MockTransaction.rollback).toBeCalledTimes(0);
+ expect(MockTransaction.commit).toBeCalledTimes(1);
+ });
+
+ it('should create when not found', async () => {
+ MockModel.first = jest.fn().mockResolvedValueOnce(null);
+ service.initModel = jest.fn().mockReturnValue(validData);
+ await service.upsert(validData.formId, validData, user);
+ expect(MockModel.insert).toBeCalledTimes(1);
+ expect(MockModel.insert).toBeCalledWith({
+ createdBy: user.usernameIdp,
+ ...validData,
+ });
+ expect(MockModel.startTransaction).toBeCalledTimes(1);
+ expect(MockTransaction.rollback).toBeCalledTimes(0);
+ expect(MockTransaction.commit).toBeCalledTimes(1);
+ });
+
+ it('should raise errors on failed update', async () => {
+ MockModel.first = jest.fn().mockResolvedValue(Object.assign({}, validData));
+ MockModel.update = jest.fn().mockRejectedValueOnce(new Error('SQL Error'));
+ await expect(service.upsert(validData.formId, validData, user)).rejects.toThrow();
+ expect(MockModel.startTransaction).toBeCalledTimes(1);
+ expect(MockTransaction.rollback).toBeCalledTimes(1);
+ expect(MockTransaction.commit).toBeCalledTimes(0);
+ });
+
+ it('should raise errors on failed insert', async () => {
+ MockModel.first = jest.fn().mockResolvedValueOnce(null);
+ MockModel.insert = jest.fn().mockRejectedValueOnce(new Error('SQL Error'));
+ await expect(service.upsert(validData.formId, validData, user)).rejects.toThrow();
+ expect(MockModel.startTransaction).toBeCalledTimes(1);
+ expect(MockTransaction.rollback).toBeCalledTimes(1);
+ expect(MockTransaction.commit).toBeCalledTimes(0);
+ });
+
+ it('should use provided transaction', async () => {
+ MockModel.first = jest.fn().mockResolvedValueOnce(null);
+ service.initModel = jest.fn().mockReturnValue(validData);
+ const xact = jest.fn().mockResolvedValue(MockTransaction);
+ await service.upsert(validData.formId, validData, user, xact);
+ expect(MockModel.insert).toBeCalledTimes(1);
+ expect(MockModel.insert).toBeCalledWith({
+ createdBy: user.usernameIdp,
+ ...validData,
+ });
+ expect(MockModel.startTransaction).toBeCalledTimes(0);
+ expect(MockTransaction.rollback).toBeCalledTimes(0);
+ expect(MockTransaction.commit).toBeCalledTimes(0);
+ });
+});
+
+describe('hasMetadata', () => {
+ it('return true when metadata has keys', async () => {
+ const result = await service.hasMetadata({ metadata: { a: 'b' } });
+ expect(result).toBeTruthy();
+ });
+
+ it('return false when no object', async () => {
+ const result = await service.hasMetadata();
+ expect(result).toBeFalsy();
+ });
+
+ it('return false when no metadata', async () => {
+ const result = await service.hasMetadata({});
+ expect(result).toBeFalsy();
+ });
+
+ it('return false when metadata has no keys', async () => {
+ const result = await service.hasMetadata({ metadata: {} });
+ expect(result).toBeFalsy();
+ });
+});
+
+describe('addMetadataToObject', () => {
+ let validData;
+ beforeEach(() => {
+ // no idea why MockModel wasn't working in this test, so just use the model directly
+ FormMetadata.query = jest.fn().mockReturnThis();
+ FormMetadata.where = jest.fn().mockReturnThis();
+ FormMetadata.modify = jest.fn().mockReturnThis();
+ FormMetadata.first = jest.fn().mockReturnThis();
+ FormMetadata.deleteById = jest.fn();
+ FormMetadata.throwIfNotFound = jest.fn().mockReturnThis();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should do nothing if no formid', async () => {
+ let obj = {};
+ await service.addMetadataToObject(null, obj, 'name');
+ expect(FormMetadata.query).not.toBeCalled();
+ expect(obj).toEqual({});
+ });
+
+ it('should do nothing if no data', async () => {
+ await service.addMetadataToObject('123', null, 'name');
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if no name', async () => {
+ let obj = {};
+ await service.addMetadataToObject('formId', obj, null);
+ expect(FormMetadata.query).not.toBeCalled();
+ expect(obj).toEqual({});
+ });
+
+ it('should do nothing if invalid name', async () => {
+ let obj = {};
+ await service.addMetadataToObject('formId', obj, 'badname');
+ expect(FormMetadata.query).not.toBeCalled();
+ expect(obj).toEqual({});
+ });
+
+ it('should do nothing if no data is not an object', async () => {
+ await service.addMetadataToObject('123', 123, 'formMetadata');
+ expect(FormMetadata.query).not.toBeCalled();
+ await service.addMetadataToObject('123', [], 'formMetadata');
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if formMetadata is not found', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(null);
+ let obj = {};
+ await service.addMetadataToObject('123', obj, 'formMetadata');
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({});
+ });
+
+ it('should add metadata as attributeName', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(validData);
+ let obj = {};
+ await service.addMetadataToObject('123', obj, 'formMetadata');
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({ formMetadata: { externalId: '456' } });
+ });
+
+ it('should add metadata as headerName', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(validData);
+ let obj = {};
+ await service.addMetadataToObject('123', obj, 'X-FORM-METADATA');
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({ 'X-FORM-METADATA': { externalId: '456' } });
+ });
+
+ it('should add encoded metadata as headerName', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(validData);
+ let bufferObj = Buffer.from(JSON.stringify(validData.metadata), 'utf8');
+ let encodedValue = bufferObj.toString('base64');
+
+ let obj = {};
+ await service.addMetadataToObject('123', obj, 'X-FORM-METADATA', true);
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj['X-FORM-METADATA']).toMatch(encodedValue);
+ });
+});
+
+describe('addAttribute', () => {
+ let validData;
+ beforeEach(() => {
+ // no idea why MockModel wasn't working in this test, so just use the model directly
+ FormMetadata.query = jest.fn().mockReturnThis();
+ FormMetadata.where = jest.fn().mockReturnThis();
+ FormMetadata.modify = jest.fn().mockReturnThis();
+ FormMetadata.first = jest.fn().mockReturnThis();
+ FormMetadata.deleteById = jest.fn();
+ FormMetadata.throwIfNotFound = jest.fn().mockReturnThis();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should do nothing if no formid', async () => {
+ let obj = {};
+ await service.addAttribute(null, obj);
+ expect(FormMetadata.query).not.toBeCalled();
+ expect(obj).toEqual({});
+ });
+
+ it('should do nothing if no data', async () => {
+ await service.addAttribute('123', null);
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if no data is not an object', async () => {
+ await service.addAttribute('123', 123);
+ expect(FormMetadata.query).not.toBeCalled();
+ await service.addAttribute('123', []);
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if formMetadata is not found', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(null);
+ let obj = {};
+ await service.addAttribute('123', obj);
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({});
+ });
+
+ it('should add metadata as attributeName', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(validData);
+ let obj = {};
+ await service.addAttribute('123', obj);
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({ formMetadata: { externalId: '456' } });
+ });
+});
+
+describe('addHeader', () => {
+ let validData;
+ beforeEach(() => {
+ // no idea why MockModel wasn't working in this test, so just use the model directly
+ FormMetadata.query = jest.fn().mockReturnThis();
+ FormMetadata.where = jest.fn().mockReturnThis();
+ FormMetadata.modify = jest.fn().mockReturnThis();
+ FormMetadata.first = jest.fn().mockReturnThis();
+ FormMetadata.deleteById = jest.fn();
+ FormMetadata.throwIfNotFound = jest.fn().mockReturnThis();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should do nothing if no formid', async () => {
+ let obj = {};
+ await service.addHeader(null, obj);
+ expect(FormMetadata.query).not.toBeCalled();
+ expect(obj).toEqual({});
+ });
+
+ it('should do nothing if no data', async () => {
+ await service.addHeader('123', null);
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if no data is not an object', async () => {
+ await service.addHeader('123', 123);
+ expect(FormMetadata.query).not.toBeCalled();
+ await service.addHeader('123', []);
+ expect(FormMetadata.query).not.toBeCalled();
+ });
+
+ it('should do nothing if formMetadata is not found', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(null);
+ let obj = {};
+ await service.addHeader('123', obj);
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj).toEqual({});
+ });
+
+ it('should add encoded metadata as headerName', async () => {
+ FormMetadata.first = jest.fn().mockResolvedValueOnce(validData);
+ let bufferObj = Buffer.from(JSON.stringify(validData.metadata), 'utf8');
+ let encodedValue = bufferObj.toString('base64');
+
+ let obj = {};
+ await service.addHeader('123', obj);
+ expect(FormMetadata.query).toBeCalledTimes(1);
+ expect(FormMetadata.first).toBeCalledTimes(1);
+ expect(obj['X-FORM-METADATA']).toMatch(encodedValue);
+ });
+});
+
+describe('read', () => {
+ const user = { usernameIdp: 'username' };
+ let validData = null;
+
+ beforeEach(() => {
+ MockModel.mockReset();
+ MockTransaction.mockReset();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should return valid data', async () => {
+ MockModel.first = jest.fn().mockResolvedValueOnce(validData);
+ const res = await service.read(validData.formId, validData, user);
+ expect(MockModel.first).toBeCalledTimes(1);
+ expect(res).toEqual(validData);
+ });
+});
+
+describe('_update', () => {
+ const user = { usernameIdp: 'username' };
+ let validData = null;
+
+ beforeEach(() => {
+ MockModel.mockClear();
+ MockModel.mockReset();
+ MockTransaction.mockReset();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should update valid data', async () => {
+ const xact = jest.fn().mockResolvedValue(MockTransaction);
+ await service._update(validData, validData, user, xact);
+ expect(MockModel.update).toBeCalledTimes(1);
+ expect(MockModel.update).toBeCalledWith({
+ updatedBy: user.usernameIdp,
+ ...validData,
+ });
+ });
+
+ it('should raise errors on failed update', async () => {
+ MockModel.update = jest.fn().mockRejectedValueOnce(new Error('SQL Error'));
+ const xact = jest.fn().mockResolvedValue(MockTransaction);
+ await expect(service._update(validData, validData, user, xact)).rejects.toThrow();
+ });
+});
+
+describe('_insert', () => {
+ const user = { usernameIdp: 'username' };
+ let validData = null;
+
+ beforeEach(() => {
+ MockModel.mockClear();
+ MockModel.mockReset();
+ MockTransaction.mockReset();
+ validData = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ metadata: { externalId: '456' },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should create with good data', async () => {
+ const xact = jest.fn().mockResolvedValue(MockTransaction);
+ service.initModel = jest.fn().mockReturnValue(validData);
+ await service._insert(validData.formId, validData, user, xact);
+ expect(MockModel.insert).toBeCalledTimes(1);
+ expect(MockModel.insert).toBeCalledWith({
+ createdBy: user.usernameIdp,
+ ...validData,
+ });
+ });
+
+ it('should raise errors on failed insert', async () => {
+ MockModel.insert = jest.fn().mockRejectedValueOnce(new Error('SQL Error'));
+ const xact = jest.fn().mockResolvedValue(MockTransaction);
+ await expect(service._insert(validData.formId, validData, user, xact)).rejects.toThrow();
+ });
+});
diff --git a/app/tests/unit/forms/form/routes.spec.js b/app/tests/unit/forms/form/routes.spec.js
index 74eb678d3..91cd650d7 100644
--- a/app/tests/unit/forms/form/routes.spec.js
+++ b/app/tests/unit/forms/form/routes.spec.js
@@ -6,7 +6,6 @@ const { expressHelper } = require('../../../common/helper');
const jwtService = require('../../../../src/components/jwtService');
const apiAccess = require('../../../../src/forms/auth/middleware/apiAccess');
const userAccess = require('../../../../src/forms/auth/middleware/userAccess');
-const rateLimiter = require('../../../../src/forms/common/middleware/rateLimiter');
const validateParameter = require('../../../../src/forms/common/middleware/validateParameter');
const controller = require('../../../../src/forms/form/controller');
@@ -29,10 +28,6 @@ jwtService.protect = jest.fn(() => {
return mockJwtServiceProtect;
});
-rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => {
- next();
-});
-
const hasFormPermissionsMock = jest.fn((_req, _res, next) => {
next();
});
@@ -83,7 +78,6 @@ describe(`${basePath}`, () => {
expect(controller.listForms).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(0);
expect(mockJwtServiceProtect).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(0);
@@ -102,7 +96,6 @@ describe(`${basePath}`, () => {
expect(controller.createForm).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(0);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(0);
@@ -126,7 +119,6 @@ describe(`${basePath}/:formId`, () => {
expect(controller.deleteForm).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -145,7 +137,6 @@ describe(`${basePath}/:formId`, () => {
expect(controller.readForm).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -164,7 +155,6 @@ describe(`${basePath}/:formId`, () => {
expect(controller.updateForm).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -188,7 +178,6 @@ describe(`${basePath}/:formId/apiKey`, () => {
expect(controller.deleteApiKey).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -207,7 +196,6 @@ describe(`${basePath}/:formId/apiKey`, () => {
expect(controller.readApiKey).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -226,7 +214,6 @@ describe(`${basePath}/:formId/apiKey`, () => {
expect(controller.createOrReplaceApiKey).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -250,7 +237,6 @@ describe(`${basePath}/:formId/apiKey/filesApiAccess`, () => {
expect(controller.filesApiKeyAccess).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -274,7 +260,6 @@ describe(`${basePath}/:formId/csvexport/fields`, () => {
expect(controller.readFieldsForCSVExport).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -298,7 +283,6 @@ describe(`${basePath}/:formId/documentTemplates`, () => {
expect(controller.documentTemplateList).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -317,7 +301,6 @@ describe(`${basePath}/:formId/documentTemplates`, () => {
expect(controller.documentTemplateCreate).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -342,7 +325,6 @@ describe(`${basePath}/:formId/documentTemplates/:documentTemplateId`, () => {
expect(controller.documentTemplateDelete).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -361,7 +343,6 @@ describe(`${basePath}/:formId/documentTemplates/:documentTemplateId`, () => {
expect(controller.documentTemplateRead).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -385,7 +366,6 @@ describe(`${basePath}/:formId/drafts`, () => {
expect(controller.listDrafts).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -404,7 +384,6 @@ describe(`${basePath}/:formId/drafts`, () => {
expect(controller.createDraft).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -429,7 +408,6 @@ describe(`${basePath}/:formId/drafts/:formVersionDraftId`, () => {
expect(controller.deleteDraft).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -448,7 +426,6 @@ describe(`${basePath}/:formId/drafts/:formVersionDraftId`, () => {
expect(controller.readDraft).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -467,7 +444,6 @@ describe(`${basePath}/:formId/drafts/:formVersionDraftId`, () => {
expect(controller.updateDraft).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -492,7 +468,6 @@ describe(`${basePath}/:formId/drafts/:formVersionDraftId/publish`, () => {
expect(controller.publishDraft).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -516,7 +491,6 @@ describe(`${basePath}/:formId/emailTemplate`, () => {
expect(controller.createOrUpdateEmailTemplate).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -540,7 +514,6 @@ describe(`${basePath}/:formId/emailTemplates`, () => {
expect(controller.readEmailTemplates).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -564,7 +537,6 @@ describe(`${basePath}/:formId/export`, () => {
expect(controller.export).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -588,7 +560,6 @@ describe(`${basePath}/:formId/export/fields`, () => {
expect(controller.exportWithFields).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -612,7 +583,6 @@ describe(`${basePath}/:formId/options`, () => {
expect(controller.readFormOptions).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(0);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -636,7 +606,6 @@ describe(`${basePath}/:formId/statusCodes`, () => {
expect(controller.getStatusCodes).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -660,7 +629,6 @@ describe(`${basePath}/:formId/submissions`, () => {
expect(controller.listFormSubmissions).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -684,7 +652,6 @@ describe(`${basePath}/:formId/subscriptions`, () => {
expect(controller.readFormSubscriptionDetails).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -703,7 +670,6 @@ describe(`${basePath}/:formId/subscriptions`, () => {
expect(controller.createOrUpdateSubscriptionDetails).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -727,7 +693,6 @@ describe(`${basePath}/:formId/version`, () => {
expect(controller.readPublishedForm).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -752,7 +717,6 @@ describe(`${basePath}/:formId/versions/:formVersionId`, () => {
expect(controller.readVersion).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -777,7 +741,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/fields`, () => {
expect(controller.readVersionFields).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -802,7 +765,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/multiSubmission`, () => {
expect(controller.createMultiSubmission).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -827,7 +789,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/publish`, () => {
expect(controller.publishVersion).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -852,7 +813,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions`, () => {
expect(controller.listSubmissions).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -871,7 +831,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions`, () => {
expect(controller.createSubmission).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -896,7 +855,6 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions/discover`, ()
expect(controller.listSubmissionFields).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(1);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(1);
@@ -920,7 +878,6 @@ describe(`${basePath}/formcomponents/proactivehelp/imageUrl/:componentId`, () =>
expect(controller.getFCProactiveHelpImageUrl).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(0);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(0);
@@ -943,7 +900,6 @@ describe(`${basePath}/formcomponents/proactivehelp/list`, () => {
expect(controller.listFormComponentsProactiveHelp).toBeCalledTimes(1);
expect(hasFormPermissionsMock).toBeCalledTimes(0);
expect(mockJwtServiceProtect).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
expect(validateParameter.validateFormId).toBeCalledTimes(0);
diff --git a/app/tests/unit/forms/proxy/routes.spec.js b/app/tests/unit/forms/proxy/routes.spec.js
index 1d65140e1..bc4e7fa4c 100644
--- a/app/tests/unit/forms/proxy/routes.spec.js
+++ b/app/tests/unit/forms/proxy/routes.spec.js
@@ -13,7 +13,6 @@ const { expressHelper } = require('../../../common/helper');
const apiAccess = require('../../../../src/forms/auth/middleware/apiAccess');
const userAccess = require('../../../../src/forms/auth/middleware/userAccess');
-const rateLimiter = require('../../../../src/forms/common/middleware/rateLimiter');
//
// Mock out all the middleware - we're testing that the routes are set up
@@ -27,10 +26,6 @@ apiAccess.mockImplementation(
})
);
-rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => {
- next();
-});
-
userAccess.currentUser = jest.fn((_req, _res, next) => {
next();
});
diff --git a/app/tests/unit/forms/proxy/service.spec.js b/app/tests/unit/forms/proxy/service.spec.js
index 266966c53..61e9e5352 100644
--- a/app/tests/unit/forms/proxy/service.spec.js
+++ b/app/tests/unit/forms/proxy/service.spec.js
@@ -7,6 +7,7 @@ const service = require('../../../../src/forms/proxy/service');
const { ExternalAPI } = require('../../../../src/forms/common/models');
jest.mock('../../../../src/forms/common/models/views/submissionMetadata', () => MockModel);
+jest.mock('../../../../src/forms/common/models/tables/formMetadata', () => MockModel);
const goodPayload = {
formId: '123',
@@ -52,6 +53,14 @@ const goodExternalApi = {
userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM,
};
+const goodFormMetadata = {
+ id: uuid.v4(),
+ formId: uuid.v4(),
+ headerName: 'X-FORM-METADATA',
+ attributeName: 'formMetadata',
+ metadata: { externalId: '789' },
+};
+
beforeEach(() => {
MockModel.mockReset();
MockTransaction.mockReset();
@@ -256,6 +265,13 @@ describe('Proxy Service', () => {
});
});
describe('createExternalAPIHeaders', () => {
+ beforeEach(() => {
+ MockModel.mockReset();
+ MockModel.first = jest.fn().mockResolvedValueOnce(goodFormMetadata);
+ });
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
it('should throw error with no headers', async () => {
const externalAPI = undefined;
const proxyHeaderInfo = goodProxyHeaderInfo;
@@ -275,7 +291,7 @@ describe('Proxy Service', () => {
const externalAPI = Object.assign({}, goodExternalApi);
externalAPI.sendUserToken = false;
externalAPI.sendUserInfo = false;
- const proxyHeaderInfo = undefined;
+ const proxyHeaderInfo = { formId: uuid.v4() };
const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
expect(result).toBeTruthy();
});
@@ -352,5 +368,27 @@ describe('Proxy Service', () => {
expect(result['X-CHEFS-USER-EMAIL']).toBe(userInfo.email);
expect(result['X-CHEFS-FORM-FORMID']).toBe(userInfo.formId);
});
+ it('should add base64 encoded form metadata header', async () => {
+ const externalAPI = Object.assign({}, goodExternalApi);
+ const proxyHeaderInfo = Object.assign({}, goodProxyHeaderInfo);
+ const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
+ expect(result).toBeTruthy();
+ expect(result['X-FORM-METADATA']).toBeTruthy();
+ // check for encoding
+ let bufferObj = Buffer.from(result['X-FORM-METADATA'], 'base64');
+ let decodedString = bufferObj.toString('utf8');
+ let o = JSON.parse(decodedString);
+ expect(o).toMatchObject(goodFormMetadata.metadata);
+ });
+ it('should not add base64 encoded form metadata header when no metadata', async () => {
+ const externalAPI = Object.assign({}, goodExternalApi);
+ const proxyHeaderInfo = Object.assign({}, goodProxyHeaderInfo);
+ let fd = goodFormMetadata;
+ fd.metadata = {}; //no attributes
+ MockModel.first = jest.fn().mockResolvedValueOnce(fd);
+ const result = await service.createExternalAPIHeaders(externalAPI, proxyHeaderInfo);
+ expect(result).toBeTruthy();
+ expect(result['X-FORM-METADATA']).toBeFalsy();
+ });
});
});
diff --git a/app/tests/unit/forms/submission/routes.spec.js b/app/tests/unit/forms/submission/routes.spec.js
index 3e3daf62c..9da3c359c 100644
--- a/app/tests/unit/forms/submission/routes.spec.js
+++ b/app/tests/unit/forms/submission/routes.spec.js
@@ -6,7 +6,6 @@ const { expressHelper } = require('../../../common/helper');
const jwtService = require('../../../../src/components/jwtService');
const apiAccess = require('../../../../src/forms/auth/middleware/apiAccess');
const userAccess = require('../../../../src/forms/auth/middleware/userAccess');
-const rateLimiter = require('../../../../src/forms/common/middleware/rateLimiter');
const validateParameter = require('../../../../src/forms/common/middleware/validateParameter');
const controller = require('../../../../src/forms/submission/controller');
@@ -28,10 +27,6 @@ jwtService.protect = jest.fn(() => {
});
});
-rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => {
- next();
-});
-
const hasSubmissionPermissionsMock = jest.fn((_req, _res, next) => {
next();
});
@@ -82,7 +77,6 @@ describe(`${basePath}/:formSubmissionId`, () => {
expect(apiAccess).toBeCalledTimes(1);
expect(controller.delete).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -100,7 +94,6 @@ describe(`${basePath}/:formSubmissionId`, () => {
expect(apiAccess).toBeCalledTimes(1);
expect(controller.read).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -118,7 +111,6 @@ describe(`${basePath}/:formSubmissionId`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.update).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -142,7 +134,6 @@ describe(`${basePath}/:formSubmissionId/:formId/submissions`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.deleteMultipleSubmissions).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -166,7 +157,6 @@ describe(`${basePath}/:formSubmissionId/:formId/submissions/restore`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.restoreMultipleSubmissions).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(1);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -189,7 +179,6 @@ describe(`${basePath}/:formSubmissionId/edits`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.listEdits).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -212,7 +201,6 @@ describe(`${basePath}/:formSubmissionId/email`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.email).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -235,7 +223,6 @@ describe(`${basePath}/:formSubmissionId/notes`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.getNotes).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -253,7 +240,6 @@ describe(`${basePath}/:formSubmissionId/notes`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.addNote).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -276,7 +262,6 @@ describe(`${basePath}/:formSubmissionId/options`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.readOptions).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(0);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -299,7 +284,6 @@ describe(`${basePath}/:formSubmissionId/restore`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.restore).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -322,7 +306,6 @@ describe(`${basePath}/:formSubmissionId/status`, () => {
expect(apiAccess).toBeCalledTimes(1);
expect(controller.getStatus).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -340,7 +323,6 @@ describe(`${basePath}/:formSubmissionId/status`, () => {
expect(apiAccess).toBeCalledTimes(0);
expect(controller.addStatus).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
@@ -364,7 +346,6 @@ describe(`${basePath}/:formSubmissionId/template/:documentTemplateId/render`, ()
expect(apiAccess).toBeCalledTimes(1);
expect(controller.templateRender).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1);
@@ -387,7 +368,6 @@ describe(`${basePath}/:formSubmissionId/template/render`, () => {
expect(apiAccess).toBeCalledTimes(1);
expect(controller.templateUploadAndRender).toBeCalledTimes(1);
expect(hasSubmissionPermissionsMock).toBeCalledTimes(1);
- expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1);
expect(userAccess.currentUser).toBeCalledTimes(1);
expect(userAccess.filterMultipleSubmissions).toBeCalledTimes(0);
expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(0);
diff --git a/docs/chefs-identity-provider-changes.md b/docs/chefs-identity-provider-changes.md
index 670ff2028..81af98601 100644
--- a/docs/chefs-identity-provider-changes.md
+++ b/docs/chefs-identity-provider-changes.md
@@ -71,7 +71,7 @@ Currently, `IDIR` has no data in `extra`.
formAccessSettings: 'idim',
addTeamMemberSearch: {
text: {
- minLength: 6,
+ minLength: 4,
message: 'trans.manageSubmissionUsers.searchInputLength',
},
email: {
diff --git a/tests/functional/cypress/e2e/form-design-advanceddata.cy.js b/tests/functional/cypress/e2e/form-design-advanceddata.cy.js
index 522e32b29..6aacaf0b2 100644
--- a/tests/functional/cypress/e2e/form-design-advanceddata.cy.js
+++ b/tests/functional/cypress/e2e/form-design-advanceddata.cy.js
@@ -87,22 +87,8 @@ it('Checks the Container component', () => {
"key": "dataGrid",
"type": "datagrid",
"input": true,
- "components": [
- {
- "label": "Children",
- "key": "children",
- "type": "datagrid",
- "input": true,
-
-
+
"components": [
- {
- "label": "First Name",
- "key": "firstName",
- "type": "textfield",
- "input": true,
- "tableView": true
- },
{
"label": "Gender",
"key": "gender",
@@ -123,10 +109,7 @@ it('Checks the Container component', () => {
}
]
- }
- ]
-
-})
+ })
cy.get('div.ace_content').type(pretty,{ parseSpecialCharSequences: false });
cy.get('button').contains('Save').click();
diff --git a/tests/functional/cypress/e2e/form-submission-export.cy.js b/tests/functional/cypress/e2e/form-submission-export.cy.js
new file mode 100644
index 000000000..a50219a20
--- /dev/null
+++ b/tests/functional/cypress/e2e/form-submission-export.cy.js
@@ -0,0 +1,251 @@
+import "cypress-keycloak-commands";
+import "cypress-drag-drop";
+import { formsettings } from "../support/login.js";
+
+const depEnv = Cypress.env("depEnv");
+
+Cypress.Commands.add("waitForLoad", () => {
+ const loaderTimeout = 60000;
+
+ cy.get(".nprogress-busy", { timeout: loaderTimeout }).should("not.exist");
+});
+describe("Form Designer", () => {
+ beforeEach(() => {
+ cy.on("uncaught:exception", (err, runnable) => {
+ // Form.io throws an uncaught exception for missing projectid
+ // Cypress catches it as undefined: undefined so we can't get the text
+ console.log(err);
+ return false;
+ });
+ });
+ it("Visits the form settings page", () => {
+ cy.viewport(1000, 1100);
+ cy.waitForLoad();
+ formsettings();
+ });
+ it("Add some fields for submission", () => {
+ cy.viewport(1000, 1800);
+ cy.waitForLoad();
+ cy.get("button").contains("Basic Fields").click();
+ cy.get("div.formio-builder-form").then(($el) => {
+ const coords = $el[0].getBoundingClientRect();
+ cy.get("span.btn")
+ .contains("Text Field")
+
+ .trigger("mousedown", { which: 1 }, { force: true })
+ .trigger("mousemove", coords.x, -50, { force: true })
+ .trigger("mouseup", { force: true });
+ cy.get("button").contains("Save").click();
+ });
+ // Form saving
+ });
+ it("Form Submission and Updation", () => {
+ cy.viewport(1000, 1100);
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.intercept("GET", `/${depEnv}/api/v1/forms/*`).as("getForm");
+ // Form saving
+ let savedButton = cy.get("[data-cy=saveButton]");
+ expect(savedButton).to.not.be.null;
+ savedButton.trigger("click");
+ cy.waitForLoad();
+
+ // Go to My forms
+ cy.wait("@getForm").then(() => {
+ let userFormsLinks = cy.get("[data-cy=userFormsLinks]");
+ expect(userFormsLinks).to.not.be.null;
+ userFormsLinks.trigger("click");
+ });
+ // Filter the newly created form
+ cy.location("search").then((search) => {
+ let arr = search.split("=");
+ let arrayValues = arr[1].split("&");
+ cy.log(arrayValues[0]);
+ cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`);
+ cy.waitForLoad();
+ //Publish the form
+ cy.get(".v-label > span").click();
+
+ cy.get("span").contains("Publish Version 1");
+
+ cy.contains("Continue").should("be.visible");
+ cy.contains("Continue").trigger("click");
+ //Submit the form
+ cy.visit(`/${depEnv}/form/submit?f=${arrayValues[0]}`);
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.get("button").contains("Submit").should("be.visible");
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.contains("Text Field").click();
+ cy.contains("Text Field").type("Alex");
+ //form submission
+ cy.get("button").contains("Submit").click();
+ cy.waitForLoad();
+ cy.get('[data-test="continue-btn-continue"]').click({ force: true });
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.get("label").contains("Text Field").should("be.visible");
+ cy.get("label").contains("Text Field").should("be.visible");
+ cy.location("pathname").should("eq", `/${depEnv}/form/success`);
+ cy.contains("h1", "Your form has been submitted successfully");
+ cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`);
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.waitForLoad();
+ cy.visit(`/${depEnv}/form/submit?f=${arrayValues[0]}`);
+ cy.wait(2000);
+ cy.get("button").contains("Submit").should("be.visible");
+ cy.waitForLoad();
+ cy.contains("Text Field").click();
+ cy.contains("Text Field").type("Alex");
+ cy.get("button").contains("Submit").click();
+ cy.waitForLoad();
+ cy.get('[data-test="continue-btn-continue"]').should("be.visible");
+ cy.get('[data-test="continue-btn-continue"]').should("exist");
+ cy.get('[data-test="continue-btn-continue"]').click({ force: true });
+ cy.location("pathname").should("eq", `/${depEnv}/form/success`);
+ cy.contains("h1", "Your form has been submitted successfully");
+ cy.waitForLoad();
+ //view submission
+ cy.visit(`/${depEnv}/form/manage?f=${arrayValues[0]}`);
+ cy.wait(2000);
+ });
+ });
+ it("Verify export submission", () => {
+ cy.viewport(1000, 1100);
+ cy.waitForLoad();
+ cy.get(".mdi-list-box-outline").click();
+ cy.waitForLoad();
+ //Export submission files
+ cy.get(".mdi-download").click();
+ //Verify submission file name
+ cy.get("h3").then(($elem) => {
+ const rem = $elem.text();
+ cy.log(rem);
+ const remname = rem + "_submissions.json";
+ cy.get(".ml-1").contains(remname);
+ });
+ cy.get(':nth-child(2) > .v-col > .v-input > .v-input__control > .v-selection-control-group > :nth-child(2) > .v-label > .radioboxLabelStyle').click();
+ cy.get('.v-col > .v-input > .v-input__control > .v-field > .v-field__field > .v-field__input').contains('1');
+ cy.contains('form.submissionId').should('be.visible');
+ cy.contains('form.confirmationId').should('be.visible');
+ cy.contains('form.formName').should('be.visible');
+ cy.contains('form.version').should('be.visible');
+ cy.contains('form.createdAt').should('be.visible');
+ cy.contains('form.fullName').should('exist');
+ cy.contains('form.username').should('exist');
+ cy.contains('form.email').should('exist');
+
+ cy.get('.v-input.mt-3 > .v-input__control > .v-field').type('status');
+ cy.contains('form.status').should('be.visible');
+ cy.contains('form.submissionId').should('not.exist');
+ cy.contains('form.confirmationId').should('not.exist');
+ cy.contains('form.formName').should('not.exist');
+ cy.contains('form.version').should('not.exist');
+ cy.contains('form.createdAt').should('not.exist');
+ //Close filter option
+ cy.get('.mdi-close-circle').click();
+ cy.contains('form.submissionId').should('be.visible');
+ cy.contains('form.confirmationId').should('be.visible');
+ cy.contains('form.formName').should('be.visible');
+ cy.contains('form.version').should('be.visible');
+ cy.contains('form.createdAt').should('be.visible');
+ //Select Date range
+ cy.get('input[type="radio"]').then($el => {
+
+ const rem=$el[3];
+ const rem1=$el[4];
+ cy.get(rem).click();
+ //Default first Radio button for csv format is enabled
+ cy.get(rem1).should('be.checked');
+
+ });
+ //verify csv format options
+ cy.get('input[type="date"]').should('have.length',2);
+ cy.get('span').contains('1 - Multiple rows per submission with indentation spaces');
+ cy.get('span').contains('2 - Multiple rows per submission');
+ cy.get('span').contains('3 - Single row per submission');
+ cy.get('span').contains('4 - Unformatted');
+ cy.get("h3").then(($elem) => {
+ const rem = $elem.text();
+ cy.log(rem);
+ const remname = rem + "_submissions.csv";
+ cy.get(".ml-1").contains(remname);
+ });
+ //verify export button is enabled
+ cy.get('.mb-5').should('be.enabled');
+ });
+
+ it("Verify print template functionality", () => {
+ cy.viewport(1000, 1100);
+ cy.waitForLoad();
+ cy.get(".mdi-list-box-outline").click();
+
+
+ cy.get(':nth-child(1) > :nth-child(6) > a > .v-btn').click();
+ //print option
+ cy.get('.mdi-printer').click();
+ cy.get('.flex-container > .v-btn--elevated').should('be.enabled');
+ cy.get('.text-textLink').should('be.enabled');
+ cy.get('.text-textLink').click();
+ cy.get('.v-overlay__content > .v-card').should('not.exist');
+ cy.get('.mdi-printer').click();
+ cy.get('.v-label > span').contains('Expand text fields');
+ cy.get('.v-slide-group-item--active > .v-btn__content').contains('Browser Print');
+ cy.get('[tabindex="-1"] > .v-btn__content').contains('Template Print');
+ cy.get('input[type="checkbox"]').should('not.be.checked');
+ cy.get('input[type="checkbox"]').click();
+ cy.get('input[type="checkbox"]').should('be.checked');
+
+ cy.get('.v-window-item--active > .flex-container > .more-info-link').should('exist');
+
+ //print option for submission files
+ cy.get('[tabindex="-1"] > .v-btn__content').click();
+ let fileUploadInputField = cy.get('input[type=file]');
+ cy.get('input[type=file]').should('not.to.be.null');
+ fileUploadInputField.attachFile('add1.png');
+ cy.waitForLoad();
+ //cy.get('label').contains('Upload template file').click({multiple:true,force:true});
+ //cy.get('.v-messages__message').contains('The template must use one of the following extentions: .txt, .docx, .html, .odt, .pptx, .xlsx');
+ cy.get('#file-input-submit').should('not.be.enabled');
+ //
+ cy.waitForLoad();
+ //Upload print template
+ cy.get('[tabindex="-1"] > .v-btn__content').click();
+ cy.get('.v-slide-group__content > [tabindex="-1"]').click();
+ cy.waitForLoad();
+ cy.get('.mdi-close-circle').click();
+ cy.get('input[type=file]').attachFile('test.docx');
+ cy.waitForLoad();
+ //cy.get('.v-selection-control-group > .v-input--dirty > .v-input__control > .v-field > .v-field__append-inner > .mdi-menu-down').click();
+ cy.get('.v-selection-control-group > .v-text-field > .v-input__control > .v-field').click();
+ cy.contains('pdf').should('be.visible');
+ //cy.get('span').contains('docx').should('exist');
+ cy.contains('pdf').click();
+ cy.get('#file-input-submit').should('be.enabled');
+ cy.get('.v-card-actions > .flex-container > .text-textLink').should('be.enabled');
+ cy.get('.v-card-actions > .flex-container > .text-textLink').click();
+ cy.get('.mdi-printer').click();
+ cy.get('#file-input-submit').click({force: true});
+ // Verify cdogs template uplaod success message
+ cy.get('body').click(0,0);
+ cy.wait(2000);
+ cy.get('.v-alert__content').contains('Document generated successfully').should('be.visible');
+
+ //Delete form after test run
+ cy.get('.mdi-list-box-outline').click();
+ cy.location("search").then((search) => {
+ let arr = search.split("=");
+ cy.visit(`/${depEnv}/form/manage?f=${arr[1]}`);
+ cy.waitForLoad();
+ cy.get('.mdi-delete').click();
+ cy.get('[data-test="continue-btn-continue"]').click();
+ cy.get('#logoutButton > .v-btn__content > span').click();
+ })
+
+
+ });
+});
diff --git a/tests/functional/cypress/support/login.js b/tests/functional/cypress/support/login.js
index 66ab288da..6bf2015fb 100644
--- a/tests/functional/cypress/support/login.js
+++ b/tests/functional/cypress/support/login.js
@@ -25,6 +25,12 @@ export function formsettings(){
cy.get('#user').type(username);
cy.get('#password').type(password);
cy.get('.btn').click();
+ cy.get('[data-cy="help"]')
+ .should("have.attr", "href", "https://developer.gov.bc.ca/docs/default/component/chefs-techdocs")
+ .should("have.text", "Help");
+ cy.get('[data-cy="feedback"]')
+ .should("have.attr", "href", "https://chefs-fider.apps.silver.devops.gov.bc.ca/")
+ .should("have.text", "Feedback");
cy.get('[data-cy="createNewForm"]').click();
cy.get('.v-row > :nth-child(1) > .v-card > .v-card-title > span').contains('Form Title');