diff --git a/jest.config.js b/jest.config.js index e4e86c096e..b96b198ffb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -50,8 +50,9 @@ module.exports = { '^LoginRouter$': `${ROOT}/src/LoginRouter`, }, setupFiles: [ - '/test/unit/jest/jest-setup-global.js' + '/test/unit/jest/jest-setup.js' ], + globalSetup: '/test/unit/jest/jest-global-setup.js', testMatch: [ '**/test/unit/spec/**/*.{js,ts}' ], diff --git a/packages/@okta/i18n/src/properties/login.properties b/packages/@okta/i18n/src/properties/login.properties index 78e6ceb877..f725133d36 100644 --- a/packages/@okta/i18n/src/properties/login.properties +++ b/packages/@okta/i18n/src/properties/login.properties @@ -1656,9 +1656,7 @@ idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.and # Device Assurance Grace Period idx.device_assurance.grace_period.title = Device assurance reminder -# {0} is the number of days until the grace period expires -idx.device_assurance.grace_period.warning.title.due_by_days = Your device doesn't meet the security requirements. Fix the issue within {0} days to prevent lockout. -# {0} is the date that the grace period expires +# {0} is the date that the grace period expires, in Okta short-with-timezone format (e.g. 11/29/2021, 4:15 PM EDT) idx.device_assurance.grace_period.warning.title.due_by_date = Your device doesn't meet the security requirements. Fix the issue by {0} to prevent lockout. idx.device_assurance.grace_period.continue_to_app = Continue to app diff --git a/packages/@okta/i18n/src/properties/login_ok_PL.properties b/packages/@okta/i18n/src/properties/login_ok_PL.properties index 989a426691..01047eca51 100644 --- a/packages/@okta/i18n/src/properties/login_ok_PL.properties +++ b/packages/@okta/i18n/src/properties/login_ok_PL.properties @@ -1248,7 +1248,6 @@ idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.uns idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required = 》Îñšţåļļ ţĥé Åñðŕöîð Ðéṽîçé Þöļîçý åþþ öñ ţĥîš ðéṽîçé €홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required_manual_install = 》Ĝö ţö <$1>Šéţţîñĝš îñ Öķţå Ṽéŕîƒý åñð ƒöļļöŵ ţĥé îñšţŕûçţîöñš ţö îñšţåļļ ţĥé Åñðŕöîð Ðéṽîçé Þöļîçý åþþ โ⾼ئ䀕ヸ€홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 idx.device_assurance.grace_period.title = 》Ðéṽîçé åššûŕåñçé ŕéɱîñðéŕ 홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 -idx.device_assurance.grace_period.warning.title.due_by_days = 》Ýöûŕ ðéṽîçé ðöéšñ´ţ ɱééţ ţĥé šéçûŕîţý ŕéǫûîŕéɱéñţš· Ƒîẋ ţĥé îššûé ŵîţĥîñ 한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ {0} ðåýš ţö þŕéṽéñţ ļöçķöûţ· 홝한Ӝฐโ《 idx.device_assurance.grace_period.warning.title.due_by_date = 》Ýöûŕ ðéṽîçé ðöéšñ´ţ ɱééţ ţĥé šéçûŕîţý ŕéǫûîŕéɱéñţš· Ƒîẋ ţĥé îššûé ƀý 한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ {0} ţö þŕéṽéñţ ļöçķöûţ· 한Ӝฐโ《 idx.device_assurance.grace_period.continue_to_app = 》Çöñţîñûé ţö åþþ ฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 api.policy.okta.account.management.insufficient.authenticators.unable.to.sign.in = 》Ûñåƀļé ţö šîĝñ îñ· Çöñţåçţ šûþþöŕţ ƒöŕ åššîšţåñçé· ⾼ئ䀕ヸ€홝한Ӝฐโ⾼ئ䀕ヸ€홝한Ӝฐโ《 diff --git a/packages/@okta/i18n/src/properties/login_ok_SK.properties b/packages/@okta/i18n/src/properties/login_ok_SK.properties index 34d280e534..5f6c8ff743 100644 --- a/packages/@okta/i18n/src/properties/login_ok_SK.properties +++ b/packages/@okta/i18n/src/properties/login_ok_SK.properties @@ -1248,7 +1248,6 @@ idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.uns idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required = [[okta-signin-widget:login: idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required]] idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required_manual_install = [[okta-signin-widget:login: idx.error.code.access_denied.device_assurance.remediation.android.zero.trust.android_device_policy_app_required_manual_install]] idx.device_assurance.grace_period.title = [[okta-signin-widget:login: idx.device_assurance.grace_period.title]] -idx.device_assurance.grace_period.warning.title.due_by_days = [[okta-signin-widget:login: idx.device_assurance.grace_period.warning.title.due_by_days]] idx.device_assurance.grace_period.warning.title.due_by_date = [[okta-signin-widget:login: idx.device_assurance.grace_period.warning.title.due_by_date]] idx.device_assurance.grace_period.continue_to_app = [[okta-signin-widget:login: idx.device_assurance.grace_period.continue_to_app]] api.policy.okta.account.management.insufficient.authenticators.unable.to.sign.in = [[okta-signin-widget:login: api.policy.okta.account.management.insufficient.authenticators.unable.to.sign.in]] diff --git a/playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-days.json b/playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-days.json deleted file mode 100644 index 03d80eaad7..0000000000 --- a/playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-days.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "version": "1.0.0", - "stateHandle": "01OCl7uyAUC4CUqHsObI9bvFiq01cRFgbnpJQ1bz82", - "expiresAt": "2024-07-31T22:56:22.000Z", - "intent": "LOGIN", - "remediation": { - "type": "array", - "value": [ - { - "rel": [ - "create-form" - ], - "name": "device-assurance-grace-period", - "href": "http://localhost:3000/idp/idx/skip", - "method": "POST", - "produces": "application/ion+json; okta-version=1.0.0", - "value": [ - { - "name": "stateHandle", - "required": true, - "value": "01OCl7uyAUC4CUqHsObI9bvFiq01cRFgbnpJQ1bz82", - "visible": false, - "mutable": false - } - ], - "accepts": "application/json; okta-version=1.0.0" - } - ] - }, - "messages": { - "type": "array", - "value": [ - { - "message": "Your device doesn't meet the security requirements. Fix the issue within 7 days to prevent lockout.", - "i18n": { - "key": "idx.device_assurance.grace_period.warning.title.due_by_days", - "params": [ - 7 - ] - }, - "class": "WARNING" - }, - { - "message": "Option 1:", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.option_index", - "params": ["1"] - }, - "class": "ERROR" - }, - { - "links": [ - {"url": "https://okta.com/android-upgrade-os"} - ], - "message": "Update to Android 100", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.android.upgrade_os_version", - "params": ["100"] - }, - "class": "ERROR" - }, - { - "links": [ - {"url": "https://okta.com/android-biometric-lock"} - ], - "message": "Enable lock screen and biometrics", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.android.use_biometric_lock_screen" - }, - "class": "ERROR" - }, - { - "message": "Option 2:", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.option_index", - "params": ["2"] - }, - "class": "ERROR" - }, - { - "links": [ - {"url": "https://okta.com/android-lock-screen"} - ], - "message": "Enable lock screen", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.android.use_lock_screen" - }, - "class": "ERROR" - }, - { - "links": [ - {"url": "https://okta.com/android-disk-encrypted"} - ], - "message": "Encrypt your device", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.android.device_disk_encrypted" - }, - "class": "ERROR" - }, - { - "links": [ - {"url": "https://okta.com/help"} - ], - "message": "For more information, follow the instructions on the help page or contact your administrator for help", - "i18n": { - "key": "idx.error.code.access_denied.device_assurance.remediation.additional_help_default" - }, - "class": "ERROR" - } - ] - }, - "cancel": { - "rel": [ - "create-form" - ], - "name": "cancel", - "href": "http://localhost:3000/idp/idx/cancel", - "method": "POST", - "produces": "application/ion+json; okta-version=1.0.0", - "value": [ - { - "name": "stateHandle", - "required": true, - "value": "01OCl7uyAUC4CUqHsObI9bvFiq01cRFgbnpJQ1bz82", - "visible": false, - "mutable": false - } - ], - "accepts": "application/json; okta-version=1.0.0" - }, - "app": { - "type": "object", - "value": { - "name": "okta_enduser", - "label": "Okta Dashboard", - "id": "0oa95jo1PTrdxHjaZ0g4" - } - } -} \ No newline at end of file diff --git a/playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-date.json b/playground/mocks/data/idp/idx/device-assurance-grace-period-multiple-options.json similarity index 97% rename from playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-date.json rename to playground/mocks/data/idp/idx/device-assurance-grace-period-multiple-options.json index f8fb7f8bf7..a597a47848 100644 --- a/playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-date.json +++ b/playground/mocks/data/idp/idx/device-assurance-grace-period-multiple-options.json @@ -31,11 +31,11 @@ "type": "array", "value": [ { - "message": "Your device doesn't meet the security requirements. Fix the issue by 08/01/2024 to prevent lockout.", + "message": "Your device doesn't meet the security requirements. Fix the issue by 09/05/2024, 12:00 AM UTC to prevent lockout.", "i18n": { "key": "idx.device_assurance.grace_period.warning.title.due_by_date", "params": [ - "08/01/2024" + "2024-09-05T00:00:00.000Z" ] }, "class": "WARNING" diff --git a/playground/mocks/data/idp/idx/device-assurance-grace-period-one-option.json b/playground/mocks/data/idp/idx/device-assurance-grace-period-one-option.json index 8a18b57e6c..2e64244e5a 100644 --- a/playground/mocks/data/idp/idx/device-assurance-grace-period-one-option.json +++ b/playground/mocks/data/idp/idx/device-assurance-grace-period-one-option.json @@ -31,11 +31,11 @@ "type": "array", "value": [ { - "message": "Your device doesn't meet the security requirements. Fix the issue within 7 days to prevent lockout.", + "message": "Your device doesn't meet the security requirements. Fix the issue by 09/05/2024, 12:00 AM UTC to prevent lockout.", "i18n": { - "key": "idx.device_assurance.grace_period.warning.title.due_by_days", + "key": "idx.device_assurance.grace_period.warning.title.due_by_date", "params": [ - 7 + "2024-09-05T00:00:00.000Z" ] }, "class": "WARNING" diff --git a/src/util/TimeUtil.js b/src/util/TimeUtil.js index 2f6b90822c..eddc08642f 100644 --- a/src/util/TimeUtil.js +++ b/src/util/TimeUtil.js @@ -93,4 +93,37 @@ export default { unit: convertMomentUnits(highestUnit), }; }, + + /** + * @method formatDateToDeviceAssuranceGracePeriodExpiryLocaleString + * Conversion from a Date object to a locale string that mimics Okta's `short-with-timezone` format + * but rounded down to the nearest hour + * e.g. new Date(2024-09-05T00:00:00.000Z) -> 09/05/2024, 8:00 PM EDT + * + * @param {Date} date The Date object for the grace period expiry + * @param {LanguageCode} languageCode The user's language code / locale + * @return {string} The formatted `short-with-timezone` local string + */ + formatDateToDeviceAssuranceGracePeriodExpiryLocaleString: (date, languageCode) => { + try { + // Invalid Date objects will return NaN for valueOf() + if (date && !isNaN(date.valueOf()) && languageCode !== null) { + // Round down the date to the nearest hour + date.setMinutes(0, 0, 0); + return date.toLocaleString(languageCode, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + }); + } else { + return null; + } + } catch (e) { + // If `languageCode` isn't in a valid format `toLocaleString()` will throw a `RangeError` + return null; + } + } }; diff --git a/src/v2/view-builder/views/device-assurance-grace-period/DeviceAssuranceGracePeriodView.js b/src/v2/view-builder/views/device-assurance-grace-period/DeviceAssuranceGracePeriodView.js index 3941193704..940bbb4382 100644 --- a/src/v2/view-builder/views/device-assurance-grace-period/DeviceAssuranceGracePeriodView.js +++ b/src/v2/view-builder/views/device-assurance-grace-period/DeviceAssuranceGracePeriodView.js @@ -14,9 +14,10 @@ const DeviceAssuranceGracePeriodView = BaseForm.extend( showMessages() { const messages = this.options.appState.get('messages').value; + const languageCode = this.settings.get('languageCode'); if (messages) { this.add(createCallout({ - content: new EndUserRemediationMessages({ messages }), + content: new EndUserRemediationMessages({ messages, languageCode }), type: 'warning', })); } diff --git a/src/v2/view-builder/views/end-user-remediation/EndUserRemediationMessages.js b/src/v2/view-builder/views/end-user-remediation/EndUserRemediationMessages.js index 83faee6b39..d9857c8588 100644 --- a/src/v2/view-builder/views/end-user-remediation/EndUserRemediationMessages.js +++ b/src/v2/view-builder/views/end-user-remediation/EndUserRemediationMessages.js @@ -1,6 +1,8 @@ import { View } from '@okta/courage'; import hbs from '@okta/handlebars-inline-precompile'; import { getMessage } from '../../../ion/i18nTransformer'; +import TimeUtil from 'util/TimeUtil'; +import { loc } from 'util/loc'; const I18N_ACCESS_DENIED_KEY_PREFIX = 'idx.error.code.access_denied.device_assurance.remediation'; const I18N_GRACE_PERIOD_KEY_PREFIX = 'idx.device_assurance.grace_period.warning'; @@ -72,18 +74,31 @@ export default View.extend({ let explanation = null; let useCustomHelpText = false; - messages.forEach((message) => { - if (message.i18n.key === ACCESS_DENIED_TITLE_KEY || message.i18n.key.startsWith(GRACE_PERIOD_TITLE_KEY)) { - title = getMessage(message); - } else if (message.i18n.key.startsWith(ACCESS_DENIED_EXPLANATION_KEY_PREFIX)) { - explanation = getMessage(message); - } else if (message.i18n.key.startsWith(HELP_AND_CONTACT_KEY_PREFIX)) { - useCustomHelpText = message.i18n.key === CUSTOM_URL_ADDITIONAL_HELP_KEY; - if (message.links && message.links[0] && message.links[0].url) { - this.additionalHelpUrl = message.links[0].url; + // eslint-disable-next-line complexity + messages.forEach((msg) => { + const { i18n: { key, params = [] }, links, message } = msg; + + if (key === ACCESS_DENIED_TITLE_KEY) { + title = getMessage(msg); + } else if (key.startsWith(GRACE_PERIOD_TITLE_KEY)) { + if (params.length > 0) { + const expiry = params[0]; + const expiryDate = new Date(expiry); + const localizedExpiry = TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString( + expiryDate, + this.options.languageCode + ); + title = localizedExpiry ? loc(key, 'login', [localizedExpiry]) : message; + } + } else if (key.startsWith(ACCESS_DENIED_EXPLANATION_KEY_PREFIX)) { + explanation = getMessage(msg); + } else if (key.startsWith(HELP_AND_CONTACT_KEY_PREFIX)) { + useCustomHelpText = key === CUSTOM_URL_ADDITIONAL_HELP_KEY; + if (links && links[0] && links[0].url) { + this.additionalHelpUrl = links[0].url; } } else { - remediationOptions.push(buildRemediationOptionBlockMessage(message)); + remediationOptions.push(buildRemediationOptionBlockMessage(msg)); } }); diff --git a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/__snapshots__/transformDeviceAssuranceGracePeriod.test.ts.snap b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/__snapshots__/transformDeviceAssuranceGracePeriod.test.ts.snap index 70ca34abf5..303f04073c 100644 --- a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/__snapshots__/transformDeviceAssuranceGracePeriod.test.ts.snap +++ b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/__snapshots__/transformDeviceAssuranceGracePeriod.test.ts.snap @@ -33,7 +33,7 @@ Object { }, ], "listStyleType": "disc", - "title": "idx.device_assurance.grace_period.warning.title.due_by_days", + "title": "idx.device_assurance.grace_period.warning.title.due_by_date", }, Object { "listStyleType": "disc", diff --git a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.test.ts b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.test.ts index 7202b6f400..a4d9a2bafd 100644 --- a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.test.ts +++ b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.test.ts @@ -29,11 +29,11 @@ describe('transformDeviceAssuranceGracePeriod Tests', () => { beforeEach(() => { transaction.messages = [ { - message: 'Your device doesn\'t meet the security requirements. Fix the issue within 7 days to prevent lockout.', + message: 'Your device doesn\'t meet the security requirements. Fix the issue by 09/05/2024, 12:00 AM UTC to prevent lockout.', i18n: { - key: 'idx.device_assurance.grace_period.warning.title.due_by_days', + key: 'idx.device_assurance.grace_period.warning.title.due_by_date', params: [ - 7, + '2024-09-05T00:00:00.000Z', ], }, class: 'ERROR', @@ -70,7 +70,7 @@ describe('transformDeviceAssuranceGracePeriod Tests', () => { formBag, widgetProps, }); - const remediationMessages = buildEndUserRemediationMessages(transaction.messages!); + const remediationMessages = buildEndUserRemediationMessages(transaction.messages!, 'en'); expect(updatedFormBag.uischema.elements.length).toBe(3); expect(updatedFormBag).toMatchSnapshot(); diff --git a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.ts b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.ts index 947be3d8d0..aa6a9e75a0 100644 --- a/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.ts +++ b/src/v3/src/transformer/layout/deviceAssuranceGracePeriod/transformDeviceAssuranceGracePeriod.ts @@ -19,20 +19,22 @@ import { InfoboxElement, TitleElement, } from '../../../types'; -import { buildEndUserRemediationMessages, loc } from '../../../util'; +import { buildEndUserRemediationMessages, getLanguageCode, loc } from '../../../util'; export const transformDeviceAssuranceGracePeriod: IdxStepTransformer = ({ formBag, transaction, + widgetProps, }) => { const { uischema } = formBag; const { messages = [] } = transaction; + const languageCode = getLanguageCode(widgetProps); // Normally, the transactionMessageTransformer runs after this transformer, but buildEndUserRemediationMessages() // expects localized transaction messages so we have to call this transformer here. transactionMessageTransformer(transaction); - const remediationMessages = buildEndUserRemediationMessages(messages); + const remediationMessages = buildEndUserRemediationMessages(messages, languageCode); const titleElement: TitleElement = { type: 'Title', diff --git a/src/v3/src/transformer/oktaVerify/transformOktaVerifyEnrollPoll.ts b/src/v3/src/transformer/oktaVerify/transformOktaVerifyEnrollPoll.ts index 9b78ce20a5..136b34c2bb 100644 --- a/src/v3/src/transformer/oktaVerify/transformOktaVerifyEnrollPoll.ts +++ b/src/v3/src/transformer/oktaVerify/transformOktaVerifyEnrollPoll.ts @@ -437,7 +437,7 @@ export const transformOktaVerifyEnrollPoll: IdxStepTransformer = ({ const stepper: StepperLayout = { type: UISchemaLayoutType.STEPPER, - key: 'stepper_' + channelType, + key: `stepper_${channelType}`, elements: [ // QR code { diff --git a/src/v3/src/util/formUtils.ts b/src/v3/src/util/formUtils.ts index 7bdfa96331..ab41556726 100644 --- a/src/v3/src/util/formUtils.ts +++ b/src/v3/src/util/formUtils.ts @@ -15,7 +15,9 @@ import { IdxMessage, IdxRemediation, IdxTransaction, NextStep, } from '@okta/okta-auth-js'; +import { LanguageCode } from '../../../types'; import IDP from '../../../util/IDP'; +import TimeUtil from '../../../util/TimeUtil'; import Util from '../../../util/Util'; import { CUSTOM_APP_UV_ENABLE_BIOMETRIC_SERVER_KEY, IDX_STEP, SOCIAL_IDP_TYPE_TO_I18KEY, TERMINAL_KEY, @@ -299,6 +301,7 @@ export const getBiometricsErrorMessageElement = ( export const buildEndUserRemediationMessages = ( messages: IdxMessage[], + languageCode?: LanguageCode, ) : WidgetMessage[] | undefined => { if (messages.length === 0) { return undefined; @@ -314,14 +317,24 @@ export const buildEndUserRemediationMessages = ( messages.forEach((msg) => { // @ts-expect-error OKTA-630508 links is missing from IdxMessage type - const { i18n: { key }, links, message } = msg; + const { i18n: { key, params = [] }, links, message } = msg; const widgetMsg = { listStyleType: 'disc' } as WidgetMessage; - if (key === ACCESS_DENIED_TITLE_KEY || key.startsWith(GRACE_PERIOD_TITLE_KEY) - || key === REMEDIATION_OPTION_INDEX_KEY) { + if (key === ACCESS_DENIED_TITLE_KEY || key === REMEDIATION_OPTION_INDEX_KEY) { // `messages` will already be localized at this point by transactionMessageTransformer, so we can directly set // widgetMsg.title equal to `message` widgetMsg.title = message; + } else if (key.startsWith(GRACE_PERIOD_TITLE_KEY)) { + // OKTA-798446 TODO: Migrate to i18next datetime localization after it is merged to gen3 + if (params.length > 0) { + // Should be an ISO8601 format string + const expiry = params[0]; + const expiryDate = new Date(expiry as string); + const localizedExpiry = TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString( + expiryDate, languageCode, + ); + widgetMsg.title = localizedExpiry ? loc(key, 'login', [localizedExpiry]) : message; + } } else if (key.startsWith(HELP_AND_CONTACT_KEY_PREFIX)) { widgetMsg.message = loc( key, diff --git a/test/testcafe/spec/DeviceAssuranceGracePeriod_spec.js b/test/testcafe/spec/DeviceAssuranceGracePeriod_spec.js index 714b1d1bb1..cea33e10e5 100644 --- a/test/testcafe/spec/DeviceAssuranceGracePeriod_spec.js +++ b/test/testcafe/spec/DeviceAssuranceGracePeriod_spec.js @@ -2,23 +2,15 @@ import SuccessPageObject from '../framework/page-objects/SuccessPageObject'; import { RequestMock } from 'testcafe'; import { checkA11y } from '../framework/a11y'; import success from '../../../playground/mocks/data/idp/idx/success'; -import deviceAssuranceGracePeriodDueByDateResponse from '../../../playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-date.json'; -import deviceAssuranceGracePeriodDueByDaysResponse from '../../../playground/mocks/data/idp/idx/device-assurance-grace-period-due-by-days.json'; +import deviceAssuranceGracePeriodMultipleOptionsResponse from '../../../playground/mocks/data/idp/idx/device-assurance-grace-period-multiple-options.json'; import deviceAssuranceGracePeriodOneOptionResponse from '../../../playground/mocks/data/idp/idx/device-assurance-grace-period-one-option.json'; import { oktaDashboardContent } from '../framework/shared'; import DeviceAssuranceGracePeriodPageObject from '../framework/page-objects/DeviceAssuranceGracePeriodPageObject'; +import TimeUtil from '../../../src/util/TimeUtil'; -const dueByDateMock = RequestMock() +const multipleOptionsMock = RequestMock() .onRequestTo('http://localhost:3000/idp/idx/introspect') - .respond(deviceAssuranceGracePeriodDueByDateResponse) - .onRequestTo('http://localhost:3000/idp/idx/skip') - .respond(success) - .onRequestTo(/^http:\/\/localhost:3000\/app\/UserHome.*/) - .respond(oktaDashboardContent); - -const dueByDaysMock = RequestMock() - .onRequestTo('http://localhost:3000/idp/idx/introspect') - .respond(deviceAssuranceGracePeriodDueByDaysResponse) + .respond(deviceAssuranceGracePeriodMultipleOptionsResponse) .onRequestTo('http://localhost:3000/idp/idx/skip') .respond(success) .onRequestTo(/^http:\/\/localhost:3000\/app\/UserHome.*/) @@ -37,43 +29,17 @@ async function setup(t) { return deviceAssuranceGracePeriodPage; } -test.requestHooks(dueByDateMock)('should render correct messaging and navigate to dashboard after clicking continue button for due by date grace period', async t => { +test.requestHooks(multipleOptionsMock)('should render correct messaging and navigate to dashboard after clicking continue button for grace period with multiple options', async t => { const deviceAssuranceGracePeriodPage = await setup(t); + const expiryLocaleString = TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00.000Z'), 'en'); await checkA11y(t); await t.expect(deviceAssuranceGracePeriodPage.form.getNthTitle(0)).eql('Device assurance reminder'); const warningBox = deviceAssuranceGracePeriodPage.getWarningBox(); await t.expect(warningBox.visible).ok(); - - await t.expect(deviceAssuranceGracePeriodPage.hasText('Your device doesn\'t meet the security requirements. Fix the issue by 08/01/2024 to prevent lockout.')).eql(true); - - await t.expect(deviceAssuranceGracePeriodPage.getOptionHeading(0)).eql('Option 1:'); - await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-upgrade-os').withExactText('Update to Android 100').exists).eql(true); - await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-biometric-lock').withExactText('Enable lock screen and biometrics').exists).eql(true); - - await t.expect(deviceAssuranceGracePeriodPage.getOptionHeading(1)).eql('Option 2:'); - await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-lock-screen').withExactText('Enable lock screen').exists).eql(true); - await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-disk-encrypted').withExactText('Encrypt your device').exists).eql(true); - - await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/help').withExactText('the help page').exists).eql(true); - await t.expect(deviceAssuranceGracePeriodPage.form.getAnchorsWithBlankTargetsWithoutRelevantAttributes().exists).eql(false); - await deviceAssuranceGracePeriodPage.clickContinueToAppButton(); - const successPage = new SuccessPageObject(t); - const pageUrl = await successPage.getPageUrl(); - await t.expect(pageUrl) - .eql('http://localhost:3000/app/UserHome?stateToken=mockedStateToken123'); -}); - -test.requestHooks(dueByDaysMock)('should render correct messaging and navigate to dashboard after clicking continue button for due by days grace period', async t => { - const deviceAssuranceGracePeriodPage = await setup(t); - await checkA11y(t); - - await t.expect(deviceAssuranceGracePeriodPage.form.getNthTitle(0)).eql('Device assurance reminder'); - const warningBox = deviceAssuranceGracePeriodPage.getWarningBox(); - await t.expect(warningBox.visible).ok(); - await t.expect(deviceAssuranceGracePeriodPage.hasText('Your device doesn\'t meet the security requirements. Fix the issue within 7 days to prevent lockout.')).eql(true); + await t.expect(deviceAssuranceGracePeriodPage.hasText(`Your device doesn't meet the security requirements. Fix the issue by ${expiryLocaleString} to prevent lockout.`)).eql(true); await t.expect(deviceAssuranceGracePeriodPage.getOptionHeading(0)).eql('Option 1:'); await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-upgrade-os').withExactText('Update to Android 100').exists).eql(true); @@ -85,7 +51,7 @@ test.requestHooks(dueByDaysMock)('should render correct messaging and navigate t await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/help').withExactText('the help page').exists).eql(true); await t.expect(deviceAssuranceGracePeriodPage.form.getAnchorsWithBlankTargetsWithoutRelevantAttributes().exists).eql(false); - + await deviceAssuranceGracePeriodPage.clickContinueToAppButton(); const successPage = new SuccessPageObject(t); const pageUrl = await successPage.getPageUrl(); @@ -95,13 +61,14 @@ test.requestHooks(dueByDaysMock)('should render correct messaging and navigate t test.requestHooks(oneOptionMock)('should render correct messaging for grace period with one option', async t => { const deviceAssuranceGracePeriodPage = await setup(t); + const expiryLocaleString = TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00.000Z'), 'en'); await checkA11y(t); await t.expect(deviceAssuranceGracePeriodPage.form.getNthTitle(0)).eql('Device assurance reminder'); const warningBox = deviceAssuranceGracePeriodPage.getWarningBox(); await t.expect(warningBox.visible).ok(); - await t.expect(deviceAssuranceGracePeriodPage.hasText('Your device doesn\'t meet the security requirements. Fix the issue within 7 days to prevent lockout.')).eql(true); + await t.expect(deviceAssuranceGracePeriodPage.hasText(`Your device doesn't meet the security requirements. Fix the issue by ${expiryLocaleString} to prevent lockout.`)).eql(true); await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-upgrade-os').withExactText('Update to Android 100').exists).eql(true); await t.expect(deviceAssuranceGracePeriodPage.getAnchor('https://okta.com/android-biometric-lock').withExactText('Enable lock screen and biometrics').exists).eql(true); diff --git a/test/unit/jest/jest-global-setup.js b/test/unit/jest/jest-global-setup.js new file mode 100644 index 0000000000..318cfd3864 --- /dev/null +++ b/test/unit/jest/jest-global-setup.js @@ -0,0 +1,5 @@ +module.exports = async () => { + // Timezone had to be hardcoded here in Jest's globalSetup configuration because the device's + // timezone was being cached before the setupFiles configuration occurred + process.env.TZ = 'UTC'; +}; \ No newline at end of file diff --git a/test/unit/jest/jest-setup-global.js b/test/unit/jest/jest-setup.js similarity index 100% rename from test/unit/jest/jest-setup-global.js rename to test/unit/jest/jest-setup.js diff --git a/test/unit/spec/TimeUtil_spec.js b/test/unit/spec/TimeUtil_spec.js index 1370f53888..5d24972991 100644 --- a/test/unit/spec/TimeUtil_spec.js +++ b/test/unit/spec/TimeUtil_spec.js @@ -104,4 +104,45 @@ describe('util/TimeUtil', function() { }); }); }); + + describe('formatDateToDeviceAssuranceGracePeriodExpiryLocaleString', () => { + const languageCode = 'en'; + + it('formats modern date', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), languageCode)).toEqual('09/05/2024, 12:00 AM UTC'); + }); + + it('rounds down to the nearest hour', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:01:01Z'), languageCode)).toEqual('09/05/2024, 12:00 AM UTC'); + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:30:30Z'), languageCode)).toEqual('09/05/2024, 12:00 AM UTC'); + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T23:59:59Z'), languageCode)).toEqual('09/05/2024, 11:00 PM UTC'); + }); + + + it('falls back to default locale if `languageCode` is undefined', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), undefined)).toEqual('09/05/2024, 12:00 AM UTC'); + }); + + it('returns null if Date object is invalid', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('invalid'), languageCode)).toBeNull(); + }); + + it('returns null if Date object is null', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(null, languageCode)).toBeNull(); + }); + + it('returns null if Date object is undefined', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(undefined, languageCode)).toBeNull(); + }); + + it('returns null if `languageCode` is null', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), null)).toBeNull(); + }); + + it('returns null if `languageCode` is syntactically invalid and throws RangeError', () => { + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), '')).toBeNull(); + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), 'a')).toBeNull(); + expect(TimeUtil.formatDateToDeviceAssuranceGracePeriodExpiryLocaleString(new Date('2024-09-05T00:00:00Z'), 'aaaaaaaaa')).toBeNull(); + }); + }); });