diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 515515879..880c7d2da 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -13,151 +13,21 @@ + -
- - - -
- -
- - - {{ messages.nextcloudHubProvider }} - -
- -
- - {{ messages.externalOIDCProvider }} - -
- -
- - -

-

- -
- -

- {{ messages.tokenExchangeHintText }} -

- - {{ messages.enableTokenExchange }} - -
-
- -
- -
-
-
-
- - - {{ t('integration_openproject', 'Edit authentication settings') }} - - - {{ t('integration_openproject', 'Cancel') }} - - - - {{ t('integration_openproject', 'Save') }} - -
-
${linkText}` return t('integration_openproject', 'Server-side encryption is active, but encryption for Team Folders is not yet enabled. To ensure secure storage of files in project folders, please follow the configuration steps in the {htmlLink}.', { htmlLink }, null, { escape: false, sanitize: false }) }, - getConfigureOIDCHintText() { - const linkText = t('integration_openproject', 'OpenID Connect settings') - const settingsUrl = this.appLinks.user_oidc.settingsLink - const htmlLink = `${linkText}` - return this.messagesFmt.configureOIDCProviders(htmlLink) - }, - getUserOidcMinimumVersion() { - return this.state.user_oidc_minimum_version - }, isIntegrationCompleteWithOauth2() { return (this.isServerHostFormComplete && this.isAuthorizationMethodFormComplete @@ -756,114 +585,34 @@ export default { return this.state.encryption_info.server_side_encryption_enabled && !this.state.encryption_info.encryption_enabled_for_groupfolders }, - disableSaveSSOSettings() { - const { currentOIDCProviderSelected, SSOProviderType, enableTokenExchange } = this.authorizationSetting - if (SSOProviderType === this.SSO_PROVIDER_TYPE.nextcloudHub) { - const typeChanged = SSOProviderType !== this.state.authorization_settings.sso_provider_type - const hasClientId = !!this.authorizationSetting.currentTargetedAudienceClientIdSelected || !!this.getCurrentSelectedTargetedClientId - const clientIdChanged = this.authorizationSetting.currentTargetedAudienceClientIdSelected !== this.getCurrentSelectedTargetedClientId - if (hasClientId) { - return !typeChanged && !clientIdChanged - } - return !hasClientId - } - - const formValueChanged = currentOIDCProviderSelected !== this.state.authorization_settings.oidc_provider - || enableTokenExchange !== this.state.authorization_settings.token_exchange - - if (!enableTokenExchange) { - return currentOIDCProviderSelected === null || !formValueChanged - } - - const clientIdChanged = this.authorizationSetting.currentTargetedAudienceClientIdSelected !== this.getCurrentSelectedTargetedClientId - return this.authorizationSetting.currentTargetedAudienceClientIdSelected === null - || !this.authorizationSetting.currentTargetedAudienceClientIdSelected - || (!formValueChanged && !clientIdChanged) - }, - getCurrentSelectedOIDCProvider() { - return this.authorizationSetting.currentOIDCProviderSelected - }, - getCurrentSelectedTargetedClientId() { - return this.state.authorization_settings.targeted_audience_client_id - }, - getSSOProviderType() { - return this.authorizationSetting.SSOProviderType - }, - getUserOidcAppName() { - return this.state.apps.user_oidc.name - }, - getOidcAppName() { - return this.state.apps.oidc.name - }, getGroupfoldersAppName() { return this.state.apps.groupfolders.name }, getAdminAuditAppName() { return this.state.admin_audit_app_name }, - hasEnabledSupportedUserOidcApp() { - return this.state.apps.user_oidc.enabled && this.state.apps.user_oidc.supported - }, - getMinSupportedUserOidcVersion() { - return this.state.apps.user_oidc.minimum_version - }, - hasEnabledSupportedOIDCApp() { - return this.state.apps.oidc.enabled && this.state.apps.oidc.supported - }, - getMinSupportedOidcVersion() { - return this.state.apps.oidc.minimum_version - }, hasEnabledSupportedGroupfoldersApp() { return this.state.apps.groupfolders.enabled && this.state.apps.groupfolders.supported }, getMinSupportedGroupfoldersVersion() { return this.state.apps.groupfolders.minimum_version }, - isExternalSSOProvider() { - return this.authorizationSetting.SSOProviderType === SSO_PROVIDER_TYPE.external - }, - hasOidcAppErrorWithNextcloudHub() { - return !this.hasEnabledSupportedOIDCApp && this.authorizationSetting.SSOProviderType === SSO_PROVIDER_TYPE.nextcloudHub - }, showGroupfoldersAppError() { - return this.isProjectFolderSwitchEnabled && !this.hasEnabledSupportedGroupfoldersApp && !this.isProjectFolderSetupFormInDisableMode - }, - disableNCHubUnsupportedHint() { - if (!this.hasEnabledSupportedOIDCApp) { - if (this.formMode.SSOSettings === F_MODES.DISABLE || this.formMode.SSOSettings === F_MODES.NEW) { - return true - } else if (this.isExternalSSOProvider) { - return true - } - } - return false - }, - showClientIDField() { - if (this.authorizationSetting.SSOProviderType === SSO_PROVIDER_TYPE.nextcloudHub) { - return true - } - return this.authorizationSetting.enableTokenExchange + return this.isProjectFolderSwitchEnabled && !this.hasEnabledSupportedGroupfoldersApp && !this.isProjectFolderFormInDisableMode }, }, watch: { - 'authorizationSetting.SSOProviderType'() { - if (this.isExternalSSOProvider && this.state.authorization_settings.sso_provider_type !== this.SSO_PROVIDER_TYPE.external) { - this.authorizationSetting.currentOIDCProviderSelected = null - } - }, - 'form.authenticationMethod.complete'() { - if (this.form.authenticationMethod.complete && this.formMode.authorizationSetting === F_MODES.DISABLE) { - this.formMode.authorizationSetting = F_MODES.EDIT + 'form.ssoSettings.complete'() { + if (this.form.ssoSettings.complete && (!this.state.authorization_settings.sso_provider_type || this.formMode.projectFolderSetUp === F_MODES.DISABLE)) { + this.formMode.projectFolderSetUp = F_MODES.EDIT + this.showDefaultManagedProjectFolders = true + this.isProjectFolderSwitchEnabled = true + this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup } }, }, created() { - this.currentSetting = this.settingsStepper.next().value - this.init() - if (!this.hasEnabledSupportedOIDCApp && (this.formMode.SSOSettings === F_MODES.DISABLE || this.formMode.SSOSettings === F_MODES.NEW)) { - this.authorizationSetting.SSOProviderType = SSO_PROVIDER_TYPE.external - } }, mounted() { this.isDarkTheme = window.getComputedStyle(this.$el).getPropertyValue('--background-invert-if-dark') === 'invert(100%)' @@ -880,7 +629,7 @@ export default { this.isProjectFolderAlreadySetup = true } } - if (this.state.fresh_project_folder_setup === true && this.formMode.projectFolderSetUp === F_MODES.DISABLE) { + if (this.state.fresh_project_folder_setup === true && this.isProjectFolderFormInDisableMode) { this.currentProjectFolderState = true this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup } else { @@ -894,66 +643,13 @@ export default { ) { this.showDefaultManagedProjectFolders = true } - // for oidc authorization - if (this.state.authorization_method === AUTH_METHOD.OIDC - && this.state.openproject_instance_url - && this.state.authorization_settings.oidc_provider - && this.state.authorization_settings.targeted_audience_client_id - ) { - this.showDefaultManagedProjectFolders = true - } if (this.state.fresh_project_folder_setup === false) { this.showDefaultManagedProjectFolders = true } - if (this.state.authorization_method) { - this.formMode.authorizationMethod = F_MODES.VIEW - this.isFormCompleted.authorizationMethod = true - } - if (this.state.openproject_instance_url && this.state.authorization_method) { - if (this.state.authorization_method === AUTH_METHOD.OAUTH2) { - if (!this.state.openproject_client_id || !this.state.openproject_client_secret) { - this.formMode.authorizationSetting = F_MODES.EDIT - } - } - if (this.state.authorization_method === AUTH_METHOD.OIDC) { - if (!this.state.authorization_settings.oidc_provider || !this.state.authorization_settings.targeted_audience_client_id) { - this.formMode.authorizationSetting = F_MODES.EDIT - this.formMode.SSOSettings = F_MODES.NEW - } - } - } - if (this.state.authorization_method === AUTH_METHOD.OIDC && this.state.authorization_settings.sso_provider_type) { - if (this.state.authorization_settings.sso_provider_type === SSO_PROVIDER_TYPE.nextcloudHub) { - if (this.state.authorization_settings.targeted_audience_client_id) { - this.formMode.authorizationSetting = F_MODES.VIEW - this.formMode.SSOSettings = F_MODES.VIEW - this.isFormCompleted.authorizationSetting = true - } - } else if (this.state.authorization_settings.oidc_provider) { - if (this.state.authorization_settings.token_exchange) { - if (this.state.authorization_settings.targeted_audience_client_id) { - this.formMode.authorizationSetting = F_MODES.VIEW - this.formMode.SSOSettings = F_MODES.VIEW - this.isFormCompleted.authorizationSetting = true - } - } else { - this.formMode.authorizationSetting = F_MODES.VIEW - this.formMode.SSOSettings = F_MODES.VIEW - this.isFormCompleted.authorizationSetting = true - } - } - this.authorizationSetting.oidcProviderSet = this.authorizationSetting.currentOIDCProviderSelected = this.state.authorization_settings.oidc_provider - this.authorizationSetting.currentTargetedAudienceClientIdSelected = this.state.authorization_settings.targeted_audience_client_id - this.authorizationSetting.SSOProviderType = this.state.authorization_settings.sso_provider_type - this.authorizationSetting.enableTokenExchange = this.state.authorization_settings.token_exchange - } if (!!this.state.openproject_client_id && !!this.state.openproject_client_secret) { this.formMode.opOauth = F_MODES.VIEW this.isFormCompleted.opOauth = true } - if (!this.state.authorization_method) { - this.formMode.authorizationMethod = F_MODES.EDIT - } if (this.state.authorization_method) { if (!this.state.openproject_client_id && !this.state.openproject_client_secret) { this.formMode.opOauth = F_MODES.EDIT @@ -973,7 +669,7 @@ export default { this.formMode.projectFolderSetUp = F_MODES.VIEW this.isFormCompleted.projectFolderSetUp = true } - if (this.formMode.ncOauth === F_MODES.VIEW || this.formMode.authorizationSetting === F_MODES.VIEW) { + if (this.formMode.ncOauth === F_MODES.VIEW || this.form.ssoSettings.complete) { this.showDefaultManagedProjectFolders = true } if (this.showDefaultManagedProjectFolders) { @@ -988,35 +684,10 @@ export default { this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange } this.isProjectFolderSwitchEnabled = this.currentProjectFolderState === true - - if (this.state.oidc_providers) { - this.registeredOidcProviders = this.state.oidc_providers - } } }, markFormComplete(formFn) { formFn(this.form) - this.nextSettings() - }, - nextSettings() { - this.currentSetting = this.settingsStepper.next().value - }, - closeRequestModal() { - this.show = false - }, - setAuthorizationSettingToViewMode() { - this.formMode.authorizationSetting = F_MODES.VIEW - this.formMode.SSOSettings = F_MODES.VIEW - this.isFormCompleted.authorizationSetting = true - this.authorizationSetting.SSOProviderType = this.state.authorization_settings.sso_provider_type - this.authorizationSetting.currentOIDCProviderSelected = this.state.authorization_settings.oidc_provider - this.authorizationSetting.enableTokenExchange = this.state.authorization_settings.token_exchange - this.authorizationSetting.currentTargetedAudienceClientIdSelected = this.state.authorization_settings.targeted_audience_client_id - }, - setAuthorizationSettingInEditMode() { - this.formMode.authorizationSetting = F_MODES.EDIT - this.formMode.SSOSettings = F_MODES.EDIT - this.isFormCompleted.authorizationSetting = false }, setProjectFolderSetUpToEditMode() { this.formMode.projectFolderSetUp = F_MODES.EDIT @@ -1103,35 +774,6 @@ export default { } } }, - async saveOIDCAuthSetting() { - this.isFormStep = FORM.AUTHORIZATION_SETTING - this.loadingAuthorizationMethodForm = true - - if (this.authorizationSetting.SSOProviderType === this.SSO_PROVIDER_TYPE.nextcloudHub) { - this.authorizationSetting.oidcProviderSet = this.SSO_PROVIDER_LABEL.nextcloudHub - this.authorizationSetting.currentOIDCProviderSelected = this.SSO_PROVIDER_LABEL.nextcloudHub - } else { - this.authorizationSetting.oidcProviderSet = this.getCurrentSelectedOIDCProvider - } - - const success = await this.saveOPOptions() - if (success) { - this.formMode.authorizationSetting = F_MODES.VIEW - this.formMode.SSOSettings = F_MODES.VIEW - this.isFormCompleted.authorizationSetting = true - if (!this.isIntegrationCompleteWithOIDC && this.formMode.projectFolderSetUp !== F_MODES.EDIT && this.formMode.opUserAppPassword !== F_MODES.EDIT) { - this.formMode.projectFolderSetUp = F_MODES.EDIT - this.showDefaultManagedProjectFolders = true - this.isProjectFolderSwitchEnabled = true - this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup - } - this.state.authorization_settings.sso_provider_type = this.authorizationSetting.SSOProviderType - this.state.authorization_settings.oidc_provider = this.authorizationSetting.currentOIDCProviderSelected - this.state.authorization_settings.token_exchange = this.authorizationSetting.enableTokenExchange - this.state.authorization_settings.targeted_audience_client_id = this.authorizationSetting.currentTargetedAudienceClientIdSelected - } - this.loadingAuthorizationMethodForm = false - }, resetOPOAuthClientValues() { OC.dialogs.confirmDestructive( t('integration_openproject', 'If you proceed you will need to update these settings with the new OpenProject OAuth credentials. Also, all users will need to reauthorize access to their OpenProject account.'), @@ -1187,9 +829,7 @@ export default { // reset form states to default this.isFormCompleted.opOauth = false - this.isFormCompleted.server = false this.formMode.opOauth = F_MODES.EDIT - this.formMode.SSOSettings = F_MODES.NEW this.state.default_enable_navigation = false this.state.default_enable_unified_search = false @@ -1201,7 +841,6 @@ export default { // if the authorization method is "oidc" if (authMethod === AUTH_METHOD.OIDC) { this.state.authorization_settings.targeted_audience_client_id = null - this.authorizationSetting.currentOIDCProviderSelected = null } await this.saveOPOptions() window.location.reload() @@ -1229,22 +868,6 @@ export default { token_exchange: null, } - } else if (this.isFormStep === FORM.AUTHORIZATION_SETTING) { - values = { - oidc_provider: this.getCurrentSelectedOIDCProvider, - targeted_audience_client_id: this.authorizationSetting.currentTargetedAudienceClientIdSelected, - sso_provider_type: this.authorizationSetting.SSOProviderType, - token_exchange: this.authorizationSetting.enableTokenExchange, - } - } else if (this.isFormStep === FORM.AUTHORIZATION_METHOD) { - values = { - ...values, - authorization_method: this.state.authorization_method, - oidc_provider: this.isIntegrationCompleteWithOIDC ? this.getCurrentSelectedOIDCProvider : null, - targeted_audience_client_id: this.isIntegrationCompleteWithOIDC ? this.authorizationSetting.currentTargetedAudienceClientIdSelected : null, - sso_provider_type: this.authorizationSetting.SSOProviderType, - token_exchange: this.authorizationSetting.enableTokenExchange, - } } else if (this.isFormStep === FORM.GROUP_FOLDER) { if (!this.isProjectFolderSwitchEnabled) { values = { @@ -1422,9 +1045,6 @@ export default { ) }) }, - onSelectOIDCProvider(selectedOption) { - this.authorizationSetting.currentOIDCProviderSelected = selectedOption - }, }, } @@ -1506,54 +1126,5 @@ export default { color: #1a67a3 !important; font-style: normal; } - .authorization-method { - &--description { - font-size: 14px; - .title { - font-weight: 700; - } - .description { - margin-top: 0.1rem; - } - } - &--options { - margin-top: 1rem; - .radio-check { - font-weight: 500; - } - } - } - .authorization-settings { - &--content { - max-width: 550px; - &--label { - font-weight: 700; - font-size: .875rem; - color: var(--color-primary-text) - } - &--section { - margin-top: 0.7rem; - } - } - .description { - margin-top: 0.1rem; - } - } - .error-container { - margin-left: 2.4rem; - font-size: 14px; - } -} - -[data-theme-light] { - #openproject_prefs { - .authorization-settings { - &--content { - &--label { - color: var(--color-main-text) - } - } - } - } } diff --git a/src/components/admin/FormAuthMethod.vue b/src/components/admin/FormAuthMethod.vue index 535291baf..9f7b414f3 100644 --- a/src/components/admin/FormAuthMethod.vue +++ b/src/components/admin/FormAuthMethod.vue @@ -118,8 +118,8 @@ export default { type: Object, required: true, }, - currentSetting: { - type: String, + formState: { + type: Object, required: true, }, isDarkTheme: { @@ -150,7 +150,7 @@ export default { }, computed: { showSettings() { - return this.currentSetting === this.formId || !!this.isFormComplete + return this.formState.serverHost.complete || !!this.isFormComplete }, isFormComplete() { return !!this.savedAuthMethod diff --git a/src/components/admin/FormOpenProjectHost.vue b/src/components/admin/FormOpenProjectHost.vue index 4246e5356..26858b837 100644 --- a/src/components/admin/FormOpenProjectHost.vue +++ b/src/components/admin/FormOpenProjectHost.vue @@ -10,9 +10,8 @@ :index="formOrder" :title="t('integration_openproject', 'OpenProject server')" :is-complete="isFormComplete" - :is-disabled="!showSettings" :is-dark-theme="isDarkTheme" /> -
+
+ + + + + diff --git a/src/utils.js b/src/utils.js index ed04e4646..f712d91b1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -129,15 +129,9 @@ export const ADMIN_SETTINGS_FORM = { complete: false, value: {}, }, + // order: 4 or 5 depending on auth method projectFolder: { id: 'project-folder', complete: false, }, } - -export function * settingsFlowGenerator() { - const settings = Object.values(ADMIN_SETTINGS_FORM).map(({ id }) => id) - for (const setting of settings) { - yield setting - } -} diff --git a/tests/jest/components/AdminSettings.spec.js b/tests/jest/components/AdminSettings.spec.js index 1ae30d2e0..4e81f908e 100644 --- a/tests/jest/components/AdminSettings.spec.js +++ b/tests/jest/components/AdminSettings.spec.js @@ -11,7 +11,7 @@ import * as dialogs from '@nextcloud/dialogs' import { createLocalVue, shallowMount, mount } from '@vue/test-utils' import flushPromises from 'flush-promises' // eslint-disable-line n/no-unpublished-import import AdminSettings from '../../../src/components/AdminSettings.vue' -import { F_MODES, AUTH_METHOD, ADMIN_SETTINGS_FORM } from '../../../src/utils.js' +import { F_MODES, AUTH_METHOD } from '../../../src/utils.js' import { appLinks } from '../../../src/constants/links.js' import { messagesFmt, messages } from '../../../src/constants/messages.js' @@ -125,15 +125,6 @@ const completeOAUTH2IntegrationState = { }, } -const completeOIDCIntegrationState = { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - }, -} - const appState = { apps: { oidc: { @@ -175,7 +166,6 @@ describe('AdminSettings.vue', () => { nc_oauth_client: null, }, { - authorizationMethod: F_MODES.EDIT, opOauth: F_MODES.DISABLE, ncOauth: F_MODES.DISABLE, projectFolderSetUp: F_MODES.DISABLE, @@ -318,14 +308,11 @@ describe('AdminSettings.vue', () => { ], ])('when the form is loaded %s', (name, state, expectedFormMode, expectedFormState) => { const wrapper = getWrapper({ state }) - expect(wrapper.vm.currentSetting).toBe(ADMIN_SETTINGS_FORM.serverHost.id) - expect(wrapper.vm.formMode.authorizationMethod).toBe(expectedFormMode.authorizationMethod) expect(wrapper.vm.formMode.opOauth).toBe(expectedFormMode.opOauth) expect(wrapper.vm.formMode.ncOauth).toBe(expectedFormMode.ncOauth) expect(wrapper.vm.formMode.projectFolderSetUp).toBe(expectedFormMode.projectFolderSetUp) expect(wrapper.vm.formMode.opUserAppPassword).toBe(expectedFormMode.opUserAppPassword) - expect(wrapper.vm.isFormCompleted.authorizationMethod).toBe(expectedFormState.authorizationMethod) expect(wrapper.vm.isFormCompleted.opOauth).toBe(expectedFormState.opOauth) expect(wrapper.vm.isFormCompleted.ncOauth).toBe(expectedFormState.ncOauth) expect(wrapper.vm.isFormCompleted.projectFolderSetUp).toBe(expectedFormState.projectFolderSetUp) @@ -333,1259 +320,6 @@ describe('AdminSettings.vue', () => { }) }) - describe('form mode and completed status without project folder setup for OIDC authorization config', () => { - it.each([ - [ - 'with empty state', - { - openproject_instance_url: null, - authorization_method: null, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - }, - { - authorizationMethod: F_MODES.EDIT, - authorizationSetting: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - authorizationSetting: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete OpenProject Authorization Method', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: null, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - }, - { - authorizationMethod: F_MODES.EDIT, - authorizationSetting: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - authorizationSetting: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete authorization settings values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - }, - { - authorizationMethod: F_MODES.VIEW, - authorizationSetting: F_MODES.EDIT, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - authorizationSetting: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with complete authorization settings values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - sso_provider_type: 'nextcloud_hub', - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - authorizationSetting: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - authorizationSetting: true, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - [ - 'with everything but empty authorization settings values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - // it means project folder is already set up - fresh_project_folder_setup: false, - }, - { - authorizationMethod: F_MODES.VIEW, - authorizationSetting: F_MODES.EDIT, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - authorizationSetting: false, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - [ - 'with a complete admin settings', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - sso_provider_type: 'nextcloud_hub', - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - authorizationSetting: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - authorizationSetting: true, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - ])('when the form is loaded %s', (name, state, expectedFormMode, expectedFormState) => { - const wrapper = getWrapper({ state }) - expect(wrapper.vm.currentSetting).toBe(ADMIN_SETTINGS_FORM.serverHost.id) - expect(wrapper.vm.formMode.authorizationMethod).toBe(expectedFormMode.authorizationMethod) - expect(wrapper.vm.formMode.authorizationSetting).toBe(expectedFormMode.authorizationSetting) - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(expectedFormMode.projectFolderSetUp) - expect(wrapper.vm.formMode.opUserAppPassword).toBe(expectedFormMode.opUserAppPassword) - - expect(wrapper.vm.isFormCompleted.authorizationMethod).toBe(expectedFormState.authorizationMethod) - expect(wrapper.vm.isFormCompleted.authorizationSetting).toBe(expectedFormState.authorizationSetting) - expect(wrapper.vm.isFormCompleted.projectFolderSetUp).toBe(expectedFormState.projectFolderSetUp) - expect(wrapper.vm.isFormCompleted.opUserAppPassword).toBe(expectedFormState.opUserAppPassword) - }) - }) - - describe('documentation link when OAUTH2 authorization', () => { - it.each([ - [ - 'with all empty state', - { - openproject_instance_url: null, - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - ], - [ - 'with incomplete OpenProject OAuth and NC OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - ], - [ - 'with incomplete NC OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'client-id-here', - openproject_client_secret: 'client-secret-here', - nc_oauth_client: null, - }, - ], - ])('should be visible %s', (name, state) => { - const wrapper = getMountedWrapper({ state }) - const setupIntegrationDocumentationLink = wrapper.find(selectors.setupIntegrationDocumentationLinkSelector) - expect(setupIntegrationDocumentationLink.text()).toBe('Visit our documentation for in-depth information on {htmlLink} integration.') - }) - - it('should be visible when integration is completed', () => { - const wrapper = getMountedWrapper({ state: completeOAUTH2IntegrationState }) - const setupIntegrationDocumentationLink = wrapper.find(selectors.setupIntegrationDocumentationLinkSelector) - expect(setupIntegrationDocumentationLink.text()).toBe('Visit our documentation for in-depth information on {htmlLink} integration.') - }) - }) - - describe('documentation link when OIDC authorization', () => { - it.each([ - [ - 'with all empty state', - { - openproject_instance_url: null, - authorization_method: null, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - }, - ], - [ - 'with incomplete OpenProject authorization settings values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - }, - }, - ], - ])('should be visible %s', (name, state) => { - const wrapper = getMountedWrapper({ state }) - const setupIntegrationDocumentationLink = wrapper.find(selectors.setupIntegrationDocumentationLinkSelector) - expect(setupIntegrationDocumentationLink.text()).toBe('Visit our documentation for in-depth information on {htmlLink} integration.') - }) - - it('should be visible when integration is completed', () => { - const wrapper = getMountedWrapper({ state: completeOIDCIntegrationState }) - const setupIntegrationDocumentationLink = wrapper.find(selectors.setupIntegrationDocumentationLinkSelector) - expect(setupIntegrationDocumentationLink.text()).toBe('Visit our documentation for in-depth information on {htmlLink} integration.') - }) - }) - - describe('OIDC authorization settings', () => { - const formHeaderSelector = `${selectors.authorizationSettings} > formheading-stub` - const errorNoteSelector = `${selectors.authorizationSettings} > errornote-stub` - const errorLabelSelector = `${selectors.authorizationSettings} errorlabel-stub` - const authProviderSelector = `${selectors.authorizationSettings} ncselect-stub` - const authClientSelector = `${selectors.authorizationSettings} textinput-stub` - const NCProviderTypeSelector = `${selectors.authorizationSettings} nccheckboxradioswitch-stub[value="nextcloud_hub"]` - const externalProviderTypeSelector = `${selectors.authorizationSettings} nccheckboxradioswitch-stub[value="external"]` - const tokenExchangeSwitchSelector = `${selectors.authorizationSettings} .sso-token-exchange input.checkbox-radio-switch__input` - const tokenExchangeActive = `${selectors.authorizationSettings} .sso-token-exchange .checkbox-radio-switch--checked` - const state = { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - sso_provider_type: 'nextcloud_hub', - }, - ...appState, - } - - describe('form complete: view mode', () => { - let wrapper - const authorizationSettingsState = { - authorization_settings: { - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'nextcloud_hub', - targeted_audience_client_id: 'some-target-aud-client-id', - }, - } - - describe.each([ - [{ - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'nextcloud_hub', - targeted_audience_client_id: 'some-target-aud-client-id', - }], - [{ - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'external', - token_exchange: false, - }], - [{ - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'external', - token_exchange: true, - targeted_audience_client_id: 'some-target-aud-client-id', - }], - ])('supported user_oidc app enabled', (settings) => { - beforeEach(async () => { - const authSettings = { - authorization_settings: settings, - } - wrapper = getWrapper({ - state: { ...state, ...authSettings }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - it('should show configured OIDC authorization', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.VIEW) - expect(wrapper.find(NCProviderTypeSelector).exists()).toBe(false) - expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true) - expect(formHeader.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - expect(authorizationSettingsForm.element).toMatchSnapshot() - }) - it('should not disable reset button', () => { - const resetButton = wrapper.find(selectors.authorizationSettingsResetButton) - expect(resetButton.attributes().disabled).toBe(undefined) - }) - }) - - describe('unsupported user_oidc app enabled', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - ...state, - ...authorizationSettingsState, - apps: { - ...appState.apps, - user_oidc: { - enabled: true, - supported: false, - minimum_version: appState.apps.user_oidc.minimum_version, - name: 'OpenID Connect user backend', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - it('should show field values and hide authorization settings form', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true) - expect(authorizationSettingsForm.element).toMatchSnapshot() - }) - it('should disable reset button', () => { - const resetButton = wrapper.find(selectors.authorizationSettingsResetButton) - expect(resetButton.attributes().disabled).toBe('true') - }) - it('should show app not supported error messages', () => { - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(formHeader.exists()).toBe(true) - expect(formHeader.attributes().haserror).toBe('true') - expect(wrapper.findAll(errorNoteSelector)).toHaveLength(1) - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) - }) - }) - - describe('supported user_oidc app disabled', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - ...state, - ...authorizationSettingsState, - apps: { - ...appState.apps, - user_oidc: { - enabled: false, - supported: true, - minimum_version: appState.apps.user_oidc.minimum_version, - name: 'OpenID Connect user backend', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - it('should show field values and hide authorization settings form', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true) - expect(authorizationSettingsForm.element).toMatchSnapshot() - }) - it('should disable reset button', () => { - const resetButton = wrapper.find(selectors.authorizationSettingsResetButton) - expect(resetButton.attributes().disabled).toBe('true') - }) - it('should show app disabled error messages', () => { - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(formHeader.exists()).toBe(true) - expect(formHeader.attributes().haserror).toBe('true') - expect(wrapper.findAll(errorNoteSelector)).toHaveLength(1) - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) - }) - }) - - describe('with external SSO provider', () => { - describe('without token exchnage', () => { - beforeEach(async () => { - const authSettings = { - authorization_settings: { - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'external', - }, - } - wrapper = getWrapper({ - state: { - ...state, - ...authSettings, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - it('should show configured OIDC authorization', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(authorizationSettingsForm.element).toMatchSnapshot() - expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true) - expect(formHeader.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - }) - it('should not disable reset button', () => { - const resetButton = wrapper.find(selectors.authorizationSettingsResetButton) - expect(resetButton.attributes().disabled).toBe(undefined) - }) - }) - describe('with token exchnage', () => { - beforeEach(async () => { - const authSettings = { - authorization_settings: { - ...authorizationSettingsState.authorization_settings, - sso_provider_type: 'external', - token_exchange: true, - }, - form: { - serverHost: { complete: true }, - }, - } - wrapper = getWrapper({ - state: { - ...state, - ...authSettings, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - it('should show configured OIDC authorization', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(authorizationSettingsForm.element).toMatchSnapshot() - expect(wrapper.vm.isIntegrationCompleteWithOIDC).toBe(true) - expect(formHeader.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - }) - it('should not disable reset button', () => { - const resetButton = wrapper.find(selectors.authorizationSettingsResetButton) - expect(resetButton.attributes().disabled).toBe(undefined) - }) - }) - }) - - }) - - describe.each([ - [{ - oidc_provider: 'some-oidc-provider', - sso_provider_type: 'nextcloud_hub', - }], - [{ - sso_provider_type: 'external', - token_exchange: true, - }], - ])('form partially complete', (settings) => { - let wrapper - beforeEach(async () => { - const authSettings = { - authorization_settings: settings, - } - wrapper = getWrapper({ - state: { - ...state, - ...authSettings, - }, - form: { - serverHost: { - complete: true, - value: state.openproject_instance_url, - }, - authenticationMethod: { complete: true, value: state.authorization_method }, - }, - }) - }) - - it('should show authorization settings in edit mode', () => { - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.EDIT) - expect(wrapper.find(selectors.authorizationSettingsCancelButton).exists()).toBe(false) - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - }) - - }) - - describe('edit mode form, complete admin configuration with supported user_oidc app', () => { - let wrapper, authorizationSettingsForm, authSettingsResetButton - beforeEach(async () => { - axios.put.mockReset() - jest.clearAllMocks() - wrapper = getMountedWrapper({ - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - sso_provider_type: 'nextcloud_hub', - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - token_exchange: false, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - authSettingsResetButton = authorizationSettingsForm.find(selectors.authorizationSettingsResetButton) - await authSettingsResetButton.trigger('click') - }) - - it('should show authorization settings in edit mode', () => { - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.EDIT) - }) - - it('should show "cancel" button', () => { - const authSettingsCancelButton = wrapper.find(selectors.authorizationSettingsCancelButton) - expect(authSettingsCancelButton.isVisible()).toBe(true) - }) - - it('should show "save" button as disabled', () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('disabled') - }) - - it('should enable "save" button for new auth settings value', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - await wrapper.find(selectors.authSettingTargetAudClient).trigger('click') - await wrapper.find(selectors.authSettingTargetAudClient).setValue('new-openproject-client-id') - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - - describe('external SSO provider', () => { - describe('without token exchange', () => { - beforeEach(async () => { - wrapper = getMountedWrapper({ - registeredOidcProviders: ['keycloak'], - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - sso_provider_type: 'external', - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - authSettingsResetButton = authorizationSettingsForm.find(selectors.authorizationSettingsResetButton) - await authSettingsResetButton.trigger('click') - }) - - it('should show "cancel" button', async () => { - const authSettingsCancelButton = wrapper.find(selectors.authorizationSettingsCancelButton) - expect(authSettingsCancelButton.isVisible()).toBe(true) - }) - it('should show "save" button as disabled', () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('disabled') - }) - it('should not show client id field', async () => { - expect(wrapper.find(selectors.authSettingTargetAudClient).exists()).toBe(false) - }) - it('should show token exchange switch in disabled state', async () => { - expect(wrapper.find(tokenExchangeSwitchSelector).exists()).toBe(true) - expect(wrapper.find(tokenExchangeActive).exists()).toBe(false) - }) - it('should enable "save" button for new provider', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - const providerInputField = wrapper.find(selectors.providerInput) - await providerInputField.setValue('key') - await localVue.nextTick() - const optionList = wrapper.find(selectors.oidcDropDownFirstElement) - await optionList.trigger('click') - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - }) - - describe('with token exchange', () => { - beforeEach(async () => { - wrapper = getMountedWrapper({ - registeredOidcProviders: ['keycloak'], - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: 'some-target-aud-client-id', - sso_provider_type: 'external', - token_exchange: true, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - authSettingsResetButton = authorizationSettingsForm.find(selectors.authorizationSettingsResetButton) - await authSettingsResetButton.trigger('click') - }) - - it('should show "cancel" button', async () => { - const authSettingsCancelButton = wrapper.find(selectors.authorizationSettingsCancelButton) - expect(authSettingsCancelButton.isVisible()).toBe(true) - }) - it('should show "save" button as disabled', () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('disabled') - }) - it('should show client id field', async () => { - expect(wrapper.find(selectors.authSettingTargetAudClient).exists()).toBe(true) - }) - it('should show token exchange switch in enabled state', async () => { - expect(wrapper.find(tokenExchangeSwitchSelector).exists()).toBe(true) - expect(wrapper.find(tokenExchangeActive).exists()).toBe(true) - }) - it('should enable "save" button for new provider', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - const providerInputField = wrapper.find(selectors.providerInput) - await providerInputField.setValue('key') - await localVue.nextTick() - const optionList = wrapper.find(selectors.oidcDropDownFirstElement) - await optionList.trigger('click') - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - it('should enable "save" button if client-id is changed', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - await wrapper.find(selectors.authSettingTargetAudClient).trigger('click') - await wrapper.find(selectors.authSettingTargetAudClient).setValue('new-openproject-client-id') - await localVue.nextTick() - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - }) - }) - - // editing new auth settings values - describe('on trigger save button', () => { - it('should set auth values with new values', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: '' } })) - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - await wrapper.find(selectors.authSettingTargetAudClient).trigger('click') - await wrapper.find(selectors.authSettingTargetAudClient).setValue('new-openproject-client-id') - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - await authSettingsSaveButton.trigger('click') - await wrapper.vm.$nextTick() - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - oidc_provider: 'Nextcloud Hub', - targeted_audience_client_id: 'new-openproject-client-id', - sso_provider_type: 'nextcloud_hub', - token_exchange: false, - }, - }, - ) - }) - }) - - describe('unsupported oidc app', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: 'Nextcloud Hub', - targeted_audience_client_id: 'some-target-aud-client-id', - sso_provider_type: 'nextcloud_hub', - }, - apps: { - ...appState.apps, - oidc: { - enabled: true, - supported: false, - minimum_version: appState.apps.oidc.minimum_version, - name: 'OIDC Identity Provider', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - await wrapper.setData({ - formMode: { - authorizationSetting: F_MODES.EDIT, - SSOSettings: F_MODES.EDIT, - }, - isFormCompleted: { authorizationSetting: false }, - }) - }) - - it('should show app not supported error messages', () => { - const errorLabel = wrapper.find(errorLabelSelector) - const ncProviderRadio = wrapper.find(NCProviderTypeSelector) - const externalProviderRadio = wrapper.find(externalProviderTypeSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(ncProviderRadio.attributes().disabled).toBe('true') - expect(ncProviderRadio.attributes().checked).toBe('nextcloud_hub') - expect(externalProviderRadio.attributes().disabled).toBe(undefined) - expect(errorLabel.attributes().error).toBe(messagesFmt.appNotEnabledOrUnsupported('oidc')) - expect(errorLabel.attributes().disabled).toBe(undefined) - expect(wrapper.findAll(errorNoteSelector)).toHaveLength(1) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.oidc.installLink) - expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) - }) - }) - }) - - describe('edit mode, incomplete admin configuration', () => { - let wrapper - - describe('Supported user_oidc app enabled', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - ...state, - ...appState, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - - it('should show authorization settings in edit mode without errors', () => { - const formHeader = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.EDIT) - expect(formHeader.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - }) - it('should not disable form elements', () => { - const authProviderSelect = wrapper.find(authProviderSelector) - const authClientInput = wrapper.find(authClientSelector) - - expect(authProviderSelect.exists()).toBe(false) - expect(authClientInput.attributes().disabled).toBe(undefined) - }) - it('should show "save" button disabled', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - }) - it('should not show "cancel" button', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsCancelButton) - expect(authSettingsSaveButton.exists()).toBe(false) - }) - it('should disable "save" button for empty "targeted_audience_client_id"', () => { - const wrapper = getWrapper({ - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: 'some-oidc-provider', - targeted_audience_client_id: null, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - }) - - it('should show authorization settings save button after completing authentication method by selecting OIDC', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: null, - user_oidc_enabled: true, - user_oidc_supported: true, - authorization_settings: { - sso_provider_type: null, - oidc_provider: null, - token_exchange: false, - }, - apps: { - oidc: { - enabled: true, - supported: true, - minimum_version: '1.4.0', - name: 'OIDC Identity Provider', - }, - user_oidc: { - enabled: true, - supported: true, - minimum_version: '1.4.0', - name: 'OpenID Connect user backend', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: false }, - }, - formMode: { - authorizationMethod: F_MODES.EDIT, - authorizationSetting: F_MODES.DISABLE, - }, - }) - - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.DISABLE) - - await wrapper.setData({ - form: { - authenticationMethod: { - complete: true, - value: AUTH_METHOD.OIDC, - }, - }, - state: { - authorization_method: AUTH_METHOD.OIDC, - }, - }) - await localVue.nextTick() - - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.EDIT) - expect(wrapper.vm.getCurrentAuthMethod).toBe(AUTH_METHOD.OIDC) - - const authorizationSettingsSection = wrapper.find(selectors.authorizationSettings) - expect(authorizationSettingsSection.exists()).toBe(true) - - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.exists()).toBe(true) - }) - - describe('external SSO provider', () => { - const wrapper = getWrapper({ - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - }, - authorizationSetting: { - SSOProviderType: 'external', - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - - it('should not disable form elements', () => { - const authProviderSelect = wrapper.find(authProviderSelector) - const authClientInput = wrapper.find(authClientSelector) - - expect(authProviderSelect.attributes().disabled).toBe(undefined) - expect(authClientInput.exists()).toBe(false) - }) - it('should show "save" button disabled', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - }) - it('should not show "cancel" button', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsCancelButton) - expect(authSettingsSaveButton.exists()).toBe(false) - }) - it('should show "save" if provider is selected', async () => { - const wrapper = getMountedWrapper({ - registeredOidcProviders: ['keycloak'], - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - }, - authorizationSetting: { - SSOProviderType: 'external', - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - const providerInputField = wrapper.find(selectors.providerInput) - await providerInputField.setValue('key') - await localVue.nextTick() - const optionList = wrapper.find(selectors.oidcDropDownFirstElement) - await optionList.trigger('click') - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - - describe('when token change is enabled', () => { - let wrapper - beforeEach(async () => { - wrapper = getMountedWrapper({ - registeredOidcProviders: ['keycloak'], - state: { - ...appState, - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - }, - authorizationSetting: { - SSOProviderType: 'external', - oidcProviderSet: 'keycloak', - currentOIDCProviderSelected: 'keycloak', - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - const tokenExchange = wrapper.find(tokenExchangeSwitchSelector) - await tokenExchange.trigger('click') - await localVue.nextTick() - }) - it('should show client-id field', async () => { - expect(wrapper.find(selectors.authSettingTargetAudClient).exists()).toBe(true) - }) - it('should disbale "Save" button', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe('disabled') - }) - it('should enable "Save" button if client-id is provided', async () => { - await wrapper.find(selectors.authSettingTargetAudClient).trigger('click') - await wrapper.find(selectors.authSettingTargetAudClient).setValue('openproject-client-id') - await localVue.nextTick() - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - }) - }) - - describe('save button', () => { - describe('Nextcloud Hub', () => { - beforeEach(async () => { - axios.put.mockReset() - jest.clearAllMocks() - wrapper = getMountedWrapper({ - state: { - ...state, - ...appState, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - await localVue.nextTick() - await authorizationSettingsForm.find(selectors.authSettingTargetAudClient).trigger('click') - await authorizationSettingsForm.find(selectors.authSettingTargetAudClient).setValue('openproject') - }) - it('should be enabled for authorization values set', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - it('"on trigger" should set auth settings values', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: '' } })) - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.exists()).toBe(true) - await authSettingsSaveButton.trigger('click') - await wrapper.vm.$nextTick() - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - oidc_provider: 'Nextcloud Hub', - sso_provider_type: 'nextcloud_hub', - targeted_audience_client_id: 'openproject', - token_exchange: undefined, - }, - }, - ) - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.VIEW) - expect(wrapper.vm.state.authorization_settings.sso_provider_type).toBe('nextcloud_hub') - expect(wrapper.vm.state.authorization_settings.oidc_provider).toBe('Nextcloud Hub') - expect(wrapper.vm.state.authorization_settings.token_exchange).toBe(undefined) - expect(wrapper.vm.state.authorization_settings.targeted_audience_client_id).toBe('openproject') - }) - }) - - describe('external SSO Provider', () => { - beforeEach(async () => { - axios.put.mockReset() - jest.clearAllMocks() - wrapper = getMountedWrapper({ - registeredOidcProviders: ['keycloak'], - state: { - ...state, - ...appState, - authorization_settings: {}, - }, - }) - await wrapper.setData({ - authorizationSetting: { - SSOProviderType: 'external', - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - await localVue.nextTick() - const providerInputField = wrapper.find(selectors.providerInput) - await providerInputField.setValue('key') - await localVue.nextTick() - const optionList = wrapper.find(selectors.oidcDropDownFirstElement) - await optionList.trigger('click') - }) - it('should enable "Save" when provider is set', async () => { - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.attributes().disabled).toBe(undefined) - }) - it('"on trigger" should set auth settings values', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: '' } })) - const authSettingsSaveButton = wrapper.find(selectors.authorizationSettingsSaveButton) - expect(authSettingsSaveButton.exists()).toBe(true) - await authSettingsSaveButton.trigger('click') - await wrapper.vm.$nextTick() - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - oidc_provider: 'keycloak', - sso_provider_type: 'external', - targeted_audience_client_id: null, - token_exchange: false, - }, - }, - ) - expect(wrapper.vm.formMode.authorizationSetting).toBe(F_MODES.VIEW) - expect(wrapper.vm.state.authorization_settings.sso_provider_type).toBe('external') - expect(wrapper.vm.state.authorization_settings.oidc_provider).toBe('keycloak') - expect(wrapper.vm.state.authorization_settings.token_exchange).toBe(false) - expect(wrapper.vm.state.authorization_settings.targeted_audience_client_id).toBe(null) - }) - }) - }) - }) - - describe('user_oidc app disabled', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - apps: { - ...appState.apps, - user_oidc: { - enabled: false, - supported: true, - minimum_version: appState.apps.user_oidc.minimum_version, - name: 'OpenID Connect user backend', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - - it('should show app disabled error messages', () => { - const formHeaderError = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(formHeaderError.exists()).toBe(true) - expect(formHeaderError.attributes().haserror).toBe('true') - expect(wrapper.findAll(errorNoteSelector)).toHaveLength(1) - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) - }) - it('should disable form elements', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsSaveButton) - const authProviderSelect = wrapper.find(authProviderSelector) - const authClientInput = wrapper.find(authClientSelector) - - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - expect(authProviderSelect.exists()).toBe(false) - expect(authClientInput.attributes().disabled).toBe('true') - }) - }) - - describe('unsupported user_oidc app is enable', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - apps: { - ...appState.apps, - user_oidc: { - enabled: true, - supported: false, - minimum_version: appState.apps.user_oidc.minimum_version, - name: 'OpenID Connect user backend', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - - it('should show app not supported error messages', () => { - const formHeaderError = wrapper.find(formHeaderSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(formHeaderError.exists()).toBe(true) - expect(formHeaderError.attributes().haserror).toBe('true') - expect(wrapper.findAll(errorNoteSelector)).toHaveLength(1) - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) - }) - it('should disable form elements', () => { - const authorizationSettingsForm = wrapper.find(selectors.authorizationSettings) - const authSettingsSaveButton = authorizationSettingsForm.find(selectors.authorizationSettingsSaveButton) - const authProviderSelect = wrapper.find(authProviderSelector) - const authClientInput = wrapper.find(authClientSelector) - - expect(authSettingsSaveButton.attributes().disabled).toBe('true') - expect(authProviderSelect.exists()).toBe(false) - expect(authClientInput.attributes().disabled).toBe('true') - }) - }) - - describe('unsupported oidc app', () => { - beforeEach(async () => { - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OIDC, - authorization_settings: { - oidc_provider: '', - targeted_audience_client_id: '', - }, - apps: { - ...appState.apps, - oidc: { - enabled: true, - supported: false, - minimum_version: appState.apps.oidc.minimum_version, - name: 'OIDC Identity Provider', - }, - }, - }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OIDC }, - }, - }) - }) - - it('should show app not supported error messages', () => { - const errorLabel = wrapper.find(errorLabelSelector) - const ncProviderRadio = wrapper.find(NCProviderTypeSelector) - const externalProviderRadio = wrapper.find(externalProviderTypeSelector) - const errorNote = wrapper.find(errorNoteSelector) - - expect(ncProviderRadio.attributes().disabled).toBe('true') - expect(ncProviderRadio.attributes().checked).toBe('external') - expect(externalProviderRadio.attributes().disabled).toBe(undefined) - expect(errorLabel.attributes().error).toBe(messagesFmt.appNotEnabledOrUnsupported('oidc')) - expect(errorLabel.attributes().disabled).toBe('true') - expect(errorNote.exists()).toBe(false) - }) - }) - }) - }) - describe('OpenProject OAuth values form', () => { describe('view mode and completed state', () => { let wrapper, opOAuthForm, resetButton @@ -3231,6 +1965,7 @@ describe('AdminSettings.vue', () => { }, form: { serverHost: { complete: true }, + authenticationMethod: { complete: true }, }, }) @@ -3275,6 +2010,7 @@ describe('AdminSettings.vue', () => { }, form: { serverHost: { complete: true }, + authenticationMethod: { complete: true }, }, }) const $defaultEnableNavigation = wrapper.find(selectors.defaultEnableNavigation) diff --git a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap index ae7fd9507..668c0c552 100644 --- a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap +++ b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap @@ -12,460 +12,6 @@ Wrapper { } `; -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode supported user_oidc app disabled should show field values and hide authorization settings form 1`] = ` -
- - - - -
- - - - - - -
- -
-
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode supported user_oidc app enabled should show configured OIDC authorization 1`] = ` -
- - - - -
- - - - - - -
- -
-
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode supported user_oidc app enabled should show configured OIDC authorization 2`] = ` -
- - - - -
- - - - - - - -
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode supported user_oidc app enabled should show configured OIDC authorization 3`] = ` -
- - - - -
- - - - - - -
- -
-
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode unsupported user_oidc app enabled should show field values and hide authorization settings form 1`] = ` -
- - - - -
- - - - - - -
- -
-
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode with external SSO provider with token exchnage should show configured OIDC authorization 1`] = ` -
- - - - -
- - - - - - -
- -
-
- -
- - - Edit authentication settings - - - - - - -
-
-`; - -exports[`AdminSettings.vue OIDC authorization settings form complete: view mode with external SSO provider without token exchnage should show configured OIDC authorization 1`] = ` -
- - - - -
- - - - - - - -
- -
- - - Edit authentication settings - - - - - - -
-
-`; - exports[`AdminSettings.vue OpenProject OAuth values form edit mode should show the form and hide the field values 1`] = ` Wrapper { "selector": ".openproject-oauth-values", diff --git a/tests/jest/components/admin/FormAuthMethod.spec.js b/tests/jest/components/admin/FormAuthMethod.spec.js index 4eb364d42..bdbeaa519 100644 --- a/tests/jest/components/admin/FormAuthMethod.spec.js +++ b/tests/jest/components/admin/FormAuthMethod.spec.js @@ -48,7 +48,7 @@ const selectors = { } const defaultProps = { - currentSetting: ADMIN_SETTINGS_FORM.authenticationMethod.id, + formState: JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)), apps: { user_oidc: { enabled: true, @@ -71,11 +71,8 @@ describe('Component: FormAuthMethod', () => { wrapper = getWrapper() }) - describe('current setting: other form', () => { + describe('server url not set', () => { it('should show form heading with disabled status', async () => { - await wrapper.setProps({ - currentSetting: ADMIN_SETTINGS_FORM.serverHost.id, - }) expect(wrapper.find(selectors.formheading).attributes().isdisabled).toBe('true') expect(wrapper.find(selectors.oauthRadioBox).exists()).toBe(false) expect(wrapper.find(selectors.ssoRadioBox).exists()).toBe(false) @@ -87,6 +84,11 @@ describe('Component: FormAuthMethod', () => { }) describe('current setting: authentication-method', () => { + beforeEach(async () => { + const formState = JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)) + formState.serverHost.complete = true + wrapper = getWrapper({ props: { formState } }) + }) it('should show the required form fields', async () => { expect(wrapper.find(selectors.oauthRadioBox).exists()).toBe(true) expect(wrapper.find(selectors.ssoRadioBox).exists()).toBe(true) @@ -179,6 +181,11 @@ describe('Component: FormAuthMethod', () => { }) describe('disabled user_oidc app', () => { + beforeEach(async () => { + const formState = JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)) + formState.serverHost.complete = true + wrapper = getWrapper({ props: { formState } }) + }) it('should show disabled error message and disabled sso button', async () => { await wrapper.setProps({ apps: { @@ -205,6 +212,11 @@ describe('Component: FormAuthMethod', () => { }) describe('unsupported user_oidc app', () => { + beforeEach(async () => { + const formState = JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)) + formState.serverHost.complete = true + wrapper = getWrapper({ props: { formState } }) + }) it('should show disabled error message and disabled sso button', async () => { await wrapper.setProps({ apps: { @@ -235,9 +247,10 @@ describe('Component: FormAuthMethod', () => { let wrapper beforeEach(() => { - wrapper = getWrapper({ props: { authMethod: AUTH_METHOD.OIDC } }) + const formState = JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)) + formState.serverHost.complete = true + wrapper = getWrapper({ props: { formState, authMethod: AUTH_METHOD.OIDC } }) }) - it('should show set form label in view mode', () => { expect(wrapper.find(selectors.formViewModeLabel).exists()).toBe(true) expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) diff --git a/tests/jest/components/admin/FormOpenProjectHost.spec.js b/tests/jest/components/admin/FormOpenProjectHost.spec.js index 6bbc5ca50..029217701 100644 --- a/tests/jest/components/admin/FormOpenProjectHost.spec.js +++ b/tests/jest/components/admin/FormOpenProjectHost.spec.js @@ -9,7 +9,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils' import { showError, showSuccess } from '@nextcloud/dialogs' import flushPromises from 'flush-promises' // eslint-disable-line n/no-unpublished-import -import { ADMIN_SETTINGS_FORM, F_MODES } from '../../../../src/utils.js' +import { F_MODES } from '../../../../src/utils.js' import { validateOPInstance, saveAdminConfig } from '../../../../src/api/settings.js' import FormOpenProjectHost from '../../../../src/components/admin/FormOpenProjectHost.vue' @@ -414,7 +414,7 @@ describe('Component: FormOpenProjectHost', () => { }) }) -function getWrapper(data = {}, props = { currentSetting: ADMIN_SETTINGS_FORM.serverHost.id }) { +function getWrapper(data = {}, props = { }) { return shallowMount(FormOpenProjectHost, { localVue, mocks: { diff --git a/tests/jest/components/admin/FormSSOSettings.spec.js b/tests/jest/components/admin/FormSSOSettings.spec.js new file mode 100644 index 000000000..fd4c090a4 --- /dev/null +++ b/tests/jest/components/admin/FormSSOSettings.spec.js @@ -0,0 +1,1597 @@ +/* jshint esversion: 8 */ + +/** + * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createLocalVue, shallowMount } from '@vue/test-utils' +import { showError, showSuccess } from '@nextcloud/dialogs' + +import { ADMIN_SETTINGS_FORM, F_MODES, SSO_PROVIDER_TYPE, SSO_PROVIDER_LABEL } from '../../../../src/utils.js' +import { saveAdminConfig } from '../../../../src/api/settings.js' +import FormSSOSettings from '../../../../src/components/admin/FormSSOSettings.vue' +import { messagesFmt, messages } from '../../../../src/constants/messages.js' +import { appLinks } from '../../../../src/constants/links.js' + +// global mocks +global.t = (app, text) => text +global.OC = { + dialogs: { + confirmDestructive: jest.fn(), + YES_NO_BUTTONS: 70, + }, +} +// module mocks +jest.mock('@nextcloud/dialogs', () => ({ + getLanguage: jest.fn(() => ''), + showError: jest.fn(), + showSuccess: jest.fn(), +})) +jest.mock('../../../../src/api/settings.js', () => ({ + saveAdminConfig: jest.fn(() => ''), +})) + +const localVue = createLocalVue() + +const selectors = { + formHeading: 'formheading-stub', + providerSelect: '.sso-provider ncselect-stub', + clientIdInput: '.sso-client-id textinput-stub', + ssoNextcloudRadioBox: `nccheckboxradioswitch-stub[value="${SSO_PROVIDER_TYPE.nextcloudHub}"]`, + ssoExternalRadioBox: `nccheckboxradioswitch-stub[value="${SSO_PROVIDER_TYPE.external}"]`, + tokenExchangeSwitch: '.sso-token-exchange nccheckboxradioswitch-stub', + saveFormButton: '[data-test-id="save-sso-settings"]', + editFormButton: '[data-test-id="edit-sso-settings"]', + cancelFormButton: '[data-test-id="cancel-sso-settings-edit"]', + errorLabel: 'errorlabel-stub', + errorNote: 'errornote-stub', + fieldValue: 'fieldvalue-stub', +} + +const appsState = { + oidc: { + enabled: true, + supported: true, + minimum_version: '1.4.0', + name: 'OIDC Identity Provider', + }, + user_oidc: { + enabled: true, + supported: true, + minimum_version: '2.0.0', + name: 'OpenID Connect user backend', + }, +} + +const formState = JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)) +formState.serverHost.complete = true +formState.authenticationMethod.complete = true +const defaultProps = { + formState, + apps: appsState, + ssoSettings: { + sso_provider_type: '', + oidc_provider: '', + targeted_audience_client_id: '', + token_exchange: '', + }, + ssoProviders: ['keycloak'], +} + +describe('Component: FormSSOSettings', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('new form: edit mode', () => { + let wrapper + + describe('with supported apps enabled', () => { + beforeEach(async () => { + wrapper = getWrapper({ props: defaultProps }) + }) + + it('should hide form fields when preceding form is not complete', () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.formState.authenticationMethod.complete = false + wrapper = getWrapper({ props }) + const formHeading = wrapper.find(selectors.formHeading) + expect(formHeading.attributes().isdisabled).toBe('true') + expect(formHeading.attributes().haserror).toBe(undefined) + expect(formHeading.attributes().iscomplete).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + + it('should show form fields without errors', () => { + expect(wrapper.vm.formMode).toBe(F_MODES.NEW) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe(undefined) + expect(wrapper.find(selectors.formHeading).attributes().disabled).toBe(undefined) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(true) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().value).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(true) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().value).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show "Save" button disabled', () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + }) + it('should not show "Cancel" button', () => { + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + expect(cancelFormButton.exists()).toBe(false) + }) + it('should disable "Save" button if client-id is empty', () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + }) + + it('should enable "Save" button if the form is complete', async () => { + expect(wrapper.vm.formMode).toBe(F_MODES.NEW) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + expect(clientIdInput.attributes().value).toBe('op-client-id') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show form related to selected provider type', async () => { + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + await localVue.nextTick() + + expect(wrapper.find(selectors.providerSelect).exists()).toBe(true) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(true) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe(null) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + + const ssoNCRadioBox = wrapper.find(selectors.ssoNextcloudRadioBox) + ssoNCRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.nextcloudHub) + await localVue.nextTick() + + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(null) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('external SSO provider', () => { + beforeEach(async () => { + wrapper = getWrapper({ props: defaultProps }) + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + await localVue.nextTick() + }) + + it('should not disable form elements', () => { + const providerSelectInput = wrapper.find(selectors.providerSelect) + const clientIdInput = wrapper.find(selectors.clientIdInput) + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + + expect(providerSelectInput.attributes().disabled).toBe(undefined) + expect(tokenExchangeSwitch.exists()).toBe(true) + expect(clientIdInput.exists()).toBe(false) + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + }) + it('should show "Save" button disabled', () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + }) + it('should not show "Cancel" button', () => { + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + expect(cancelFormButton.exists()).toBe(false) + }) + it('should enable "Save" button if the form is complete', async () => { + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + await localVue.nextTick() + + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + expect(providerSelect.attributes().value).toBe('keycloak') + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('when token change is enabled', () => { + beforeEach(async () => { + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + await tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + }) + it('should show client-id field', async () => { + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should disable "Save" button', async () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + }) + it('should enable "Save" button if the form is complete', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe(undefined) + expect(providerSelect.attributes().value).toBe('keycloak') + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(clientIdInput.attributes().value).toBe('op-client-id') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should disable "Save" button if the provider is not selected', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + expect(providerSelect.attributes().value).toBe(undefined) + expect(wrapper.vm.currentForm.oidc_provider).toBe(null) + expect(clientIdInput.attributes().value).toBe('op-client-id') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + }) + it('should disable "Save" button if the client-id is not provided', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const clientIdInput = wrapper.find(selectors.clientIdInput) + await localVue.nextTick() + + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe('true') + expect(providerSelect.attributes().value).toBe('keycloak') + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(clientIdInput.attributes().value).toBe('') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('') + }) + }) + }) + + describe('save button', () => { + describe('Nextcloud Hub', () => { + beforeEach(async () => { + jest.clearAllMocks() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + }) + it('should set sso settings on save', async () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe(undefined) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('') + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + targeted_audience_client_id: 'op-client-id', + token_exchange: null, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe(null) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + + describe('external SSO Provider', () => { + beforeEach(async () => { + jest.clearAllMocks() + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + await ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + await localVue.nextTick() + }) + it('should set sso settings on save: without token exchange', async () => { + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe(undefined) + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe('') + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('') + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + targeted_audience_client_id: null, + token_exchange: '', + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe('') + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe(null) + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(3) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should set sso settings on save: with token exchange', async () => { + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + await tokenExchangeSwitch.vm.$emit('update:checked', true) + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(saveFormButton.attributes().disabled).toBe(undefined) + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('') + expect(wrapper.vm.savedForm.token_exchange).toBe('') + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + targeted_audience_client_id: 'op-client-id', + token_exchange: true, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(true) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(4) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + }) + }) + + describe('apps state', () => { + describe.each([ + ['disabled user_oidc app', { enabled: false, supported: true }], + ['unsupported user_oidc app', { enabled: true, supported: false }], + ])('%s', (_, state) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.apps.user_oidc.enabled = state.enabled + props.apps.user_oidc.supported = state.supported + beforeEach(async () => { + wrapper = getWrapper({ props }) + }) + + it('should not show error card when preceding form is not complete', () => { + const updatedProps = JSON.parse(JSON.stringify(props)) + updatedProps.formState.authenticationMethod.complete = false + wrapper = getWrapper({ props: updatedProps }) + + const formHeading = wrapper.find(selectors.formHeading) + + expect(formHeading.attributes().isdisabled).toBe('true') + expect(formHeading.attributes().haserror).toBe('true') + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show error card with disabled form fields', () => { + const formHeading = wrapper.find(selectors.formHeading) + const errorNote = wrapper.find(selectors.errorNote) + + expect(formHeading.attributes().haserror).toBe('true') + expect(formHeading.attributes().isdisabled).toBe(undefined) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) + expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().disabled).toBe('true') + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().disabled).toBe('true') + expect(wrapper.find(selectors.clientIdInput).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + + describe.each([ + ['disabled oidc app', { enabled: false, supported: true }], + ['unsupported oidc app', { enabled: true, supported: false }], + ])('%s', (_, state) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.apps.oidc.enabled = state.enabled + props.apps.oidc.supported = state.supported + beforeEach(async () => { + wrapper = getWrapper({ props }) + }) + + it('should not show error card when preceding form is not complete', () => { + const updatedProps = JSON.parse(JSON.stringify(props)) + updatedProps.formState.authenticationMethod.complete = false + wrapper = getWrapper({ props: updatedProps }) + + const formHeading = wrapper.find(selectors.formHeading) + + expect(formHeading.attributes().isdisabled).toBe('true') + expect(formHeading.attributes().haserror).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show disabled error label but not error card', () => { + const formHeading = wrapper.find(selectors.formHeading) + expect(formHeading.attributes().isdisabled).toBe(undefined) + expect(formHeading.attributes().haserror).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + const errorLabel = wrapper.find(selectors.errorLabel) + expect(errorLabel.attributes().error).toBe(messagesFmt.appNotEnabledOrUnsupported()) + expect(errorLabel.attributes().disabled).toBe('true') + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().disabled).toBe('true') + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().disabled).toBe(undefined) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(true) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(true) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + }) + }) + + describe('partially complete form', (settings) => { + it.each([ + ['Nextcloud Hub', { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: '', + token_exchange: '', + targeted_audience_client_id: '', + }], + ['external without provider', { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: '', + token_exchange: false, + targeted_audience_client_id: '', + }], + ['external token exchange and without provider', { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: '', + token_exchange: true, + targeted_audience_client_id: 'client-id', + }], + ['external token exchange and without client-id', { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + token_exchange: true, + targeted_audience_client_id: '', + }], + ])('%s - should show form fields', (_, settings) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = settings + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.formMode).toBe(F_MODES.NEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + + if (settings.sso_provider_type === SSO_PROVIDER_TYPE.nextcloudHub) { + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.clientIdInput).attributes().value).toBe(settings.targeted_audience_client_id) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(false) + } else { + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.providerSelect).attributes().value).toBe(settings.oidc_provider) + if (settings.token_exchange) { + expect(wrapper.find(selectors.clientIdInput).attributes().value).toBe(settings.targeted_audience_client_id) + expect(wrapper.find(selectors.tokenExchangeSwitch).attributes().checked).toBe(`${settings.token_exchange}`) + } else { + expect(wrapper.find(selectors.tokenExchangeSwitch).attributes().checked).toBe(undefined) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + } + } + toMatchSerializedSnapshot(wrapper.html()) + }) + + }) + + describe('complete form: view mode', () => { + let wrapper + + describe('with supported apps enabled', () => { + it.each([ + [ + 'complete Nextcloud Hub', + { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + }, + 2, + SSO_PROVIDER_TYPE.nextcloudHub, + ], + [ + 'complete external provider without token exchange', + { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'some-oidc-provider', + token_exchange: false, + targeted_audience_client_id: '', + }, + 3, + SSO_PROVIDER_TYPE.external, + ], + [ + 'complete external provider with token exchange', + { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'some-oidc-provider', + token_exchange: true, + targeted_audience_client_id: 'op-client-id', + }, + 4, + SSO_PROVIDER_TYPE.external, + ], + ])('should show the settings in view mode - %s', (_, settings, fieldsLength, providerType) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = settings + wrapper = getWrapper({ props }) + + const formHeading = wrapper.find(selectors.formHeading) + expect(formHeading.attributes().haserror).toBe(undefined) + expect(formHeading.attributes().disabled).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(settings.sso_provider_type) + expect(wrapper.vm.currentForm.oidc_provider).toBe(settings.oidc_provider) + expect(wrapper.vm.currentForm.token_exchange).toBe(settings.token_exchange) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe(settings.targeted_audience_client_id) + + const formFields = wrapper.findAll(selectors.fieldValue) + + expect(formFields).toHaveLength(fieldsLength) + expect(formFields.at(0).attributes().value).toBe(settings.sso_provider_type) + if (providerType === SSO_PROVIDER_TYPE.nextcloudHub) { + expect(formFields.at(1).attributes().value).toBe(settings.targeted_audience_client_id) + } else { + expect(formFields.at(1).attributes().value).toBe(settings.oidc_provider) + expect(formFields.at(2).attributes().value).toBe(`${settings.token_exchange}`) + fieldsLength === 4 && expect(formFields.at(3).attributes().value).toBe(settings.targeted_audience_client_id) + } + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + + describe('apps state', () => { + describe.each([ + [ + 'disabled user_oidc app', + { enabled: false, supported: true }, + { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + }, + 2, + ], + [ + 'unsupported user_oidc app', + { enabled: true, supported: false }, + { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'some-oidc-provider', + token_exchange: false, + targeted_audience_client_id: '', + }, + 3, + ], + ])('%s', (_, state, settings, fieldsLength) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = settings + props.apps.user_oidc.enabled = state.enabled + props.apps.user_oidc.supported = state.supported + beforeEach(async () => { + wrapper = getWrapper({ props }) + }) + + it('should show error card', () => { + const errorNote = wrapper.find(selectors.errorNote) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(errorNote.exists()).toBe(true) + expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported('user_oidc')) + expect(errorNote.attributes().errorlink).toBe(appLinks.user_oidc.installLink) + expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe('true') + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show saved settings', () => { + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(fieldsLength) + }) + it('should disable "Edit" button', () => { + expect(wrapper.find(selectors.editFormButton).attributes().disabled).toBe('true') + }) + }) + + describe('oidc app', () => { + describe.each([ + [ + 'disabled app', + { enabled: false, supported: true }, + { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + }, + ], + [ + 'unsupported app', + { enabled: false, supported: true }, + { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + }, + ], + ])('%s - Nextcloud Hub settings', (_, state, settings) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = settings + props.apps.oidc.enabled = state.enabled + props.apps.oidc.supported = state.supported + beforeEach(async () => { + wrapper = getWrapper({ props }) + }) + + it('should show error card', () => { + const errorNote = wrapper.find(selectors.errorNote) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(errorNote.exists()).toBe(true) + expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported('oidc')) + expect(errorNote.attributes().errorlink).toBe(appLinks.oidc.installLink) + expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe('true') + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show saved settings', () => { + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + }) + it('should show "Edit" button', () => { + expect(wrapper.find(selectors.editFormButton).attributes().disabled).toBe(undefined) + }) + }) + + describe.each([ + [ + 'disabled app', + { enabled: false, supported: true }, + { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'some-oidc-provider', + token_exchange: false, + targeted_audience_client_id: '', + }, + 3, + ], + [ + 'unsupported app', + { enabled: true, supported: false }, + { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'some-oidc-provider', + token_exchange: true, + targeted_audience_client_id: 'op-client-id', + }, + 4, + ], + ])('%s - external provider settings', (_, state, settings, fieldsLength) => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = settings + props.apps.oidc.enabled = state.enabled + props.apps.oidc.supported = state.supported + beforeEach(async () => { + wrapper = getWrapper({ props }) + }) + + it('should not show error card', () => { + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe(undefined) + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.ssoExternalRadioBox).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show saved settings', () => { + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(fieldsLength) + }) + it('should show "Edit" button', () => { + expect(wrapper.find(selectors.editFormButton).attributes().disabled).toBe(undefined) + }) + }) + }) + }) + }) + + describe('complete form: edit mode', () => { + let wrapper + + describe('Nextcloud Hub', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: '', + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + } + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + }) + + it('should show the form fields', () => { + expect(wrapper.vm.formMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.find(selectors.clientIdInput).attributes().value).toBe('op-client-id') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show the action buttons', () => { + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + }) + it('should enable "save" button if client-id is changed', async () => { + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + // disabled save button on old client id + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should reset the changes on cancel', async () => { + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe('') + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.token_exchange).toBe('') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should set sso settings on save', async () => { + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + const saveFormButton = wrapper.find(selectors.saveFormButton) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + targeted_audience_client_id: 'op-client-id-new', + token_exchange: null, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe(null) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id-new') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.currentForm.token_exchange).toBe(null) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id-new') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('change to external provider', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + } + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + await localVue.nextTick() + }) + + it('should show external provider form fields', async () => { + expect(wrapper.vm.formMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(true) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(true) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + + expect(wrapper.vm.currentForm.oidc_provider).toBe(null) + + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(true) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should reset the changes on cancel', async () => { + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe('') + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.currentForm.token_exchange).toBe('') + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should set settings on save', async () => { + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + targeted_audience_client_id: 'op-client-id-new', + token_exchange: true, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(true) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id-new') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.currentForm.token_exchange).toBe(true) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id-new') + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(4) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + + describe('disabled oidc app', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: '', + token_exchange: '', + targeted_audience_client_id: 'op-client-id', + } + props.apps.oidc.enabled = false + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + }) + + it('should be able to select external provider', async () => { + const ssoNextcloudRadioBox = wrapper.find(selectors.ssoNextcloudRadioBox) + expect(ssoNextcloudRadioBox.attributes().disabled).toBe('true') + expect(ssoNextcloudRadioBox.attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + expect(ssoExternalRadioBox.attributes().disabled).toBe(undefined) + expect(ssoExternalRadioBox.attributes().checked).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + await localVue.nextTick() + + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe('true') + expect(wrapper.find(selectors.errorNote).exists()).toBe(true) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(wrapper.find(selectors.errorLabel).exists()).toBe(true) + expect(wrapper.find(selectors.errorLabel).attributes().disabled).toBe(undefined) + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + targeted_audience_client_id: 'op-client-id-new', + token_exchange: true, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.errorLabel).exists()).toBe(false) + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe(undefined) + + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(true) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id-new') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(4) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should preserve the errors on cancel', async () => { + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + ssoExternalRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.external) + await localVue.nextTick() + + expect(wrapper.find(selectors.errorNote).exists()).toBe(true) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(wrapper.find(selectors.errorLabel).exists()).toBe(true) + expect(wrapper.find(selectors.errorLabel).attributes().disabled).toBe(undefined) + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'keycloak') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.errorNote).exists()).toBe(true) + expect(wrapper.findAll(selectors.errorNote)).toHaveLength(1) + expect(wrapper.find(selectors.errorLabel).exists()).toBe(false) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe('') + expect(wrapper.vm.savedForm.token_exchange).toBe('') + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + }) + + describe('external provider', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + token_exchange: false, + targeted_audience_client_id: '', + } + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + }) + + it('should show the form fields', () => { + expect(wrapper.vm.formMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.providerSelect).attributes().value).toBe('keycloak') + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(true) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show the action buttons', () => { + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + }) + it('should enable "save" button if the settings changed', async () => { + // change provider + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'new-provider') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + // revert + await providerSelect.vm.$emit('option:selected', 'keycloak') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + + // enable token exchange + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id-new') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + // revert + tokenExchangeSwitch.vm.$emit('update:checked', false) + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + }) + it('should reset the changes on cancel', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'new-provider') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(false) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.currentForm.token_exchange).toBe(false) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(3) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should set sso settings on save', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'new-provider') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', true) + await localVue.nextTick() + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'new-provider', + targeted_audience_client_id: 'op-client-id', + token_exchange: true, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('new-provider') + expect(wrapper.vm.savedForm.token_exchange).toBe(true) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('new-provider') + expect(wrapper.vm.currentForm.token_exchange).toBe(true) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(4) + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('change to Nextcloud Hub', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + token_exchange: false, + targeted_audience_client_id: '', + } + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + const ssoNextcloudRadioBox = wrapper.find(selectors.ssoNextcloudRadioBox) + ssoNextcloudRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.nextcloudHub) + await localVue.nextTick() + }) + + it('should show form fields', async () => { + expect(wrapper.vm.formMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.providerSelect).exists()).toBe(false) + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(false) + expect(wrapper.find(selectors.clientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should reset the changes on cancel', async () => { + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(false) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.currentForm.token_exchange).toBe(false) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(3) + }) + it('should set settings on save', async () => { + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + const clientIdInput = wrapper.find(selectors.clientIdInput) + await clientIdInput.vm.$emit('input', 'op-client-id') + await localVue.nextTick() + + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + targeted_audience_client_id: 'op-client-id', + token_exchange: null, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe(null) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.currentForm.token_exchange).toBe(null) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + }) + }) + + describe('disabled oidc app', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + token_exchange: false, + targeted_audience_client_id: '', + } + props.apps.oidc.enabled = false + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + }) + + it('should not show error card', async () => { + const ssoNextcloudRadioBox = wrapper.find(selectors.ssoNextcloudRadioBox) + expect(ssoNextcloudRadioBox.attributes().disabled).toBe('true') + expect(ssoNextcloudRadioBox.attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + const ssoExternalRadioBox = wrapper.find(selectors.ssoExternalRadioBox) + expect(ssoExternalRadioBox.attributes().disabled).toBe(undefined) + expect(ssoExternalRadioBox.attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + + expect(wrapper.find(selectors.formHeading).attributes().haserror).toBe(undefined) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.errorLabel).exists()).toBe(true) + expect(wrapper.find(selectors.errorLabel).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + + describe('token exchange enabled', () => { + beforeEach(async () => { + const props = JSON.parse(JSON.stringify(defaultProps)) + props.ssoSettings = { + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'keycloak', + token_exchange: true, + targeted_audience_client_id: 'op-client-id', + } + wrapper = getWrapper({ props }) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + const editFormButton = wrapper.find(selectors.editFormButton) + editFormButton.vm.$emit('click') + await localVue.nextTick() + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe(undefined) + }) + + it('should show the form fields', () => { + expect(wrapper.vm.formMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.ssoNextcloudRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.ssoExternalRadioBox).attributes().checked).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.find(selectors.providerSelect).attributes().value).toBe('keycloak') + expect(wrapper.find(selectors.tokenExchangeSwitch).exists()).toBe(true) + expect(wrapper.find(selectors.clientIdInput).attributes().value).toBe('op-client-id') + + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe('true') + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should reset the changes on cancel', async () => { + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', false) + await localVue.nextTick() + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + + const cancelFormButton = wrapper.find(selectors.cancelFormButton) + await cancelFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(0) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.savedForm.token_exchange).toBe(true) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('keycloak') + expect(wrapper.vm.currentForm.token_exchange).toBe(true) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(4) + }) + it('should set sso settings on save', async () => { + const providerSelect = wrapper.find(selectors.providerSelect) + await providerSelect.vm.$emit('option:selected', 'new-provider') + const tokenExchangeSwitch = wrapper.find(selectors.tokenExchangeSwitch) + tokenExchangeSwitch.vm.$emit('update:checked', false) + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.external, + oidc_provider: 'new-provider', + targeted_audience_client_id: null, + token_exchange: false, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.savedForm.oidc_provider).toBe('new-provider') + expect(wrapper.vm.savedForm.token_exchange).toBe(false) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe(null) + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.external) + expect(wrapper.vm.currentForm.oidc_provider).toBe('new-provider') + expect(wrapper.vm.currentForm.token_exchange).toBe(false) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe(null) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(3) + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('change to Nextcloud Hub', () => { + it('should set sso settings on save', async () => { + const ssoNextcloudRadioBox = wrapper.find(selectors.ssoNextcloudRadioBox) + ssoNextcloudRadioBox.vm.$emit('update:checked', SSO_PROVIDER_TYPE.nextcloudHub) + await localVue.nextTick() + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + + const saveFormButton = wrapper.find(selectors.saveFormButton) + await saveFormButton.vm.$emit('click') + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(saveAdminConfig).toBeCalledWith({ + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + targeted_audience_client_id: 'op-client-id', + token_exchange: null, + }) + expect(wrapper.vm.formMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.formHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.editFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(false) + + expect(wrapper.vm.savedForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.savedForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.savedForm.token_exchange).toBe(null) + expect(wrapper.vm.savedForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.currentForm.sso_provider_type).toBe(SSO_PROVIDER_TYPE.nextcloudHub) + expect(wrapper.vm.currentForm.oidc_provider).toBe(SSO_PROVIDER_LABEL.nextcloudHub) + expect(wrapper.vm.currentForm.token_exchange).toBe(null) + expect(wrapper.vm.currentForm.targeted_audience_client_id).toBe('op-client-id') + expect(wrapper.vm.loading).toBe(false) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(wrapper.findAll(selectors.fieldValue)).toHaveLength(2) + }) + }) + }) + }) + }) + + describe('save failure', () => { + beforeEach(() => { + saveAdminConfig.mockImplementation(() => { + throw new Error('Save failed') + }) + }) + + it('should show error message on save failure', async () => { + const wrapper = getWrapper() + await wrapper.setData({ + currentForm: { + sso_provider_type: SSO_PROVIDER_TYPE.nextcloudHub, + oidc_provider: SSO_PROVIDER_LABEL.nextcloudHub, + token_exchange: null, + targeted_audience_client_id: 'op-client-id', + }, + }) + await wrapper.vm.saveSettings() + await localVue.nextTick() + + expect(saveAdminConfig).toBeCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledTimes(0) + + expect(wrapper.vm.formMode).toBe(F_MODES.NEW) + expect(wrapper.find(selectors.editFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.cancelFormButton).exists()).toBe(false) + expect(wrapper.find(selectors.saveFormButton).exists()).toBe(true) + expect(wrapper.find(selectors.saveFormButton).attributes().disabled).toBe(undefined) + expect(wrapper.vm.loading).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) +}) + +function getWrapper({ data = {}, props = {} } = {}) { + return shallowMount(FormSSOSettings, { + localVue, + mocks: { + t: (app, msg) => msg, + }, + propsData: { ...defaultProps, ...props }, + data() { + return data + }, + }) +} + +function toMatchSerializedSnapshot(element) { + element = element.replace(/ id="[^"]+"/g, ' id="__ID__"').replace(/ uid="[^"]+"/g, ' uid="__UID__"') + expect(element).toMatchSnapshot() +} diff --git a/tests/jest/components/admin/__snapshots__/FormAuthMethod.spec.js.snap b/tests/jest/components/admin/__snapshots__/FormAuthMethod.spec.js.snap index f3dea7bab..9177040d1 100644 --- a/tests/jest/components/admin/__snapshots__/FormAuthMethod.spec.js.snap +++ b/tests/jest/components/admin/__snapshots__/FormAuthMethod.spec.js.snap @@ -352,14 +352,6 @@ exports[`Component: FormAuthMethod initial incomplete form current setting: auth
" `; -exports[`Component: FormAuthMethod initial incomplete form current setting: other form should show form heading with disabled status 1`] = ` -"
- - - -
" -`; - exports[`Component: FormAuthMethod initial incomplete form disabled user_oidc app should show disabled error message and disabled sso button 1`] = ` "
@@ -398,6 +390,14 @@ exports[`Component: FormAuthMethod initial incomplete form disabled user_oidc ap
" `; +exports[`Component: FormAuthMethod initial incomplete form server url not set should show form heading with disabled status 1`] = ` +"
+ + + +
" +`; + exports[`Component: FormAuthMethod initial incomplete form unsupported user_oidc app should show disabled error message and disabled sso button 1`] = ` "
diff --git a/tests/jest/components/admin/__snapshots__/FormSSOSettings.spec.js.snap b/tests/jest/components/admin/__snapshots__/FormSSOSettings.spec.js.snap new file mode 100644 index 000000000..47a91b89b --- /dev/null +++ b/tests/jest/components/admin/__snapshots__/FormSSOSettings.spec.js.snap @@ -0,0 +1,1593 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub change to external provider should reset the changes on cancel 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub change to external provider should set settings on save 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub change to external provider should show external provider form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub disabled oidc app should be able to select external provider 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub disabled oidc app should preserve the errors on cancel 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub should enable "save" button if client-id is changed 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub should reset the changes on cancel 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub should set sso settings on save 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode Nextcloud Hub should show the form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider change to Nextcloud Hub should show form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider disabled oidc app should not show error card 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider should reset the changes on cancel 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider should set sso settings on save 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider should show the form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider token exchange enabled should set sso settings on save 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: edit mode external provider token exchange enabled should show the form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + Cancel + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state disabled user_oidc app should show error card 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state oidc app disabled app - Nextcloud Hub settings should show error card 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state oidc app disabled app - external provider settings should not show error card 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state oidc app unsupported app - Nextcloud Hub settings should show error card 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state oidc app unsupported app - external provider settings should not show error card 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode apps state unsupported user_oidc app should show error card 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode with supported apps enabled should show the settings in view mode - complete Nextcloud Hub 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode with supported apps enabled should show the settings in view mode - complete external provider with token exchange 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings complete form: view mode with supported apps enabled should show the settings in view mode - complete external provider without token exchange 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state disabled oidc app should not show error card when preceding form is not complete 1`] = ` +"
+ + +
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state disabled oidc app should show disabled error label but not error card 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state disabled user_oidc app should not show error card when preceding form is not complete 1`] = ` +"
+ + +
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state disabled user_oidc app should show error card with disabled form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state unsupported oidc app should not show error card when preceding form is not complete 1`] = ` +"
+ + +
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state unsupported oidc app should show disabled error label but not error card 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state unsupported user_oidc app should not show error card when preceding form is not complete 1`] = ` +"
+ + +
" +`; + +exports[`Component: FormSSOSettings new form: edit mode apps state unsupported user_oidc app should show error card with disabled form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled external SSO provider should enable "Save" button if the form is complete 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled external SSO provider when token change is enabled should enable "Save" button if the form is complete 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled external SSO provider when token change is enabled should show client-id field 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled save button Nextcloud Hub should set sso settings on save 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled save button external SSO Provider should set sso settings on save: with token exchange 1`] = ` +"
+ +
+ +
+ + + +
+ +
+
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled save button external SSO Provider should set sso settings on save: without token exchange 1`] = ` +"
+ +
+ +
+ + + + +
+
+ + Edit authentication settings + + + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled should enable "Save" button if the form is complete 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled should hide form fields when preceding form is not complete 1`] = ` +"
+ + +
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled should show form fields without errors 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled should show form related to selected provider type 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings new form: edit mode with supported apps enabled should show form related to selected provider type 2`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings partially complete form Nextcloud Hub - should show form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings partially complete form external token exchange and without client-id - should show form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings partially complete form external token exchange and without provider - should show form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings partially complete form external without provider - should show form fields 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+
+ + +

You can configure OIDC providers in the {settingsLink}

+
+
+ +

+ When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process. +

+ Enable token exchange +
+ +
+
+ + + + Save + +
+
+
" +`; + +exports[`Component: FormSSOSettings save failure should show error message on save failure 1`] = ` +"
+ +
+ +
+
+ + + Nextcloud Hub + +
+ +
+ + External Provider + +
+ + +
+
+ +
+
+
+
+ + + + Save + +
+
+
" +`;