diff --git a/src/api/endpoints.js b/src/api/endpoints.js index 75b47379b..295020e81 100644 --- a/src/api/endpoints.js +++ b/src/api/endpoints.js @@ -8,4 +8,5 @@ import { generateUrl } from '@nextcloud/router' export default { validateOPInstance: generateUrl('/apps/integration_openproject/is-valid-op-instance'), adminConfig: generateUrl('/apps/integration_openproject/admin-config'), + nextcloudOAuth: generateUrl('/apps/integration_openproject/nc-oauth'), } diff --git a/src/api/settings.js b/src/api/settings.js index 2efd87766..d8e12372c 100644 --- a/src/api/settings.js +++ b/src/api/settings.js @@ -13,3 +13,7 @@ export function validateOPInstance(url) { export function saveAdminConfig(configs) { return axios.put(endpoints.adminConfig, { values: configs }) } + +export function createNextcloudOAuthClient() { + return axios.post(endpoints.nextcloudOAuth) +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 515515879..481a4829d 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -13,270 +13,31 @@ + + -
- - - -
- -
- - - {{ messages.nextcloudHubProvider }} - -
- -
- - {{ messages.externalOIDCProvider }} - -
- -
- - -

-

- -
- -

- {{ messages.tokenExchangeHintText }} -

- - {{ messages.enableTokenExchange }} - -
-
- -
- -
-
-
-
- - - {{ t('integration_openproject', 'Edit authentication settings') }} - - - {{ t('integration_openproject', 'Cancel') }} - - - - {{ t('integration_openproject', 'Save') }} - -
-
-
- -
- - - - -
- - - {{ t('integration_openproject', 'Replace OpenProject OAuth values') }} - - - - {{ t('integration_openproject', 'Save') }} - -
-
-
-
- -
- - - - -
- - - {{ t('integration_openproject', 'Yes, I have copied these values') }} - - - - {{ t('integration_openproject', 'Replace Nextcloud OAuth values') }} - -
-
-
- - - {{ t('integration_openproject', 'Create Nextcloud OAuth values') }} - -
-
{{ t('integration_openproject', 'Reset') }} -
+

{{ t('integration_openproject', 'Default user settings') }}

{{ t('integration_openproject', 'A new user will receive these defaults and they will be applied to the integration app till the user changes them.') }} @@ -470,7 +231,6 @@ import { NcCheckboxRadioSwitch, NcButton, NcNoteCard, - NcSelect, } from '@nextcloud/vue' import RestoreIcon from 'vue-material-design-icons/Restore.vue' import AutoRenewIcon from 'vue-material-design-icons/Autorenew.vue' @@ -480,20 +240,19 @@ import FormHeading from './admin/FormHeading.vue' import CheckBox from '../components/settings/CheckBox.vue' import SettingsTitle from '../components/settings/SettingsTitle.vue' import ErrorNote from './settings/ErrorNote.vue' -import { F_MODES, FORM, USER_SETTINGS, AUTH_METHOD, SSO_PROVIDER_TYPE, SSO_PROVIDER_LABEL, ADMIN_SETTINGS_FORM, settingsFlowGenerator } from '../utils.js' +import { F_MODES, FORM, USER_SETTINGS, AUTH_METHOD, SSO_PROVIDER_TYPE, SSO_PROVIDER_LABEL, ADMIN_SETTINGS_FORM } from '../utils.js' import TermsOfServiceUnsigned from './admin/TermsOfServiceUnsigned.vue' import dompurify from 'dompurify' import { messages, messagesFmt } from '../constants/messages.js' import { appLinks } from '../constants/links.js' -import ErrorLabel from './ErrorLabel.vue' import FormOpenProjectHost from './admin/FormOpenProjectHost.vue' import FormAuthMethod from './admin/FormAuthMethod.vue' +import FormSSOSettings from './admin/FormSSOSettings.vue' +import FormOAuthSettings from './admin/FormOAuthSettings.vue' export default { name: 'AdminSettings', components: { - ErrorLabel, - NcSelect, NcButton, FieldValue, FormHeading, @@ -511,25 +270,20 @@ export default { ErrorNote, FormOpenProjectHost, FormAuthMethod, + FormSSOSettings, + FormOAuthSettings, }, data() { return { form: JSON.parse(JSON.stringify(ADMIN_SETTINGS_FORM)), - currentSetting: null, - settingsStepper: settingsFlowGenerator(), formMode: { // server host form is never disabled. // it's either editable or view only - authorizationMethod: F_MODES.DISABLE, - authorizationSetting: F_MODES.DISABLE, - SSOSettings: F_MODES.DISABLE, - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, opUserAppPassword: F_MODES.DISABLE, projectFolderSetUp: F_MODES.DISABLE, }, isFormCompleted: { - server: false, authorizationMethod: false, authorizationSetting: false, opOauth: false, ncOauth: false, opUserAppPassword: false, projectFolderSetUp: false, + opUserAppPassword: false, projectFolderSetUp: false, }, buttonTextLabel: { keepCurrentChange: t('integration_openproject', 'Keep current setup'), @@ -538,12 +292,8 @@ export default { retrySetupWithProjectFolder: t('integration_openproject', 'Retry setup OpenProject user, group and folder'), }, loadingProjectFolderSetup: false, - loadingOPOauthForm: false, - loadingAuthorizationMethodForm: false, - loadingAuthorizationSettingForm: false, state: loadState('integration_openproject', 'admin-settings-config'), isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'), - oPOAuthTokenRevokeStatus: null, oPUserAppPassword: null, isProjectFolderSwitchEnabled: null, projectFolderSetupError: null, @@ -561,14 +311,6 @@ export default { userSettingDescription: USER_SETTINGS, SSO_PROVIDER_TYPE, SSO_PROVIDER_LABEL, - authorizationSetting: { - oidcProviderSet: null, - currentOIDCProviderSelected: null, - currentTargetedAudienceClientIdSelected: null, - SSOProviderType: SSO_PROVIDER_TYPE.nextcloudHub, - enableTokenExchange: false, - }, - registeredOidcProviders: [], messages, messagesFmt, appLinks, @@ -584,13 +326,6 @@ export default { || this.state.openproject_client_secret return formAdded || hasPreSEtup }, - ncClientId() { - return this.state.nc_oauth_client?.nextcloud_client_id - }, - ncClientSecret() { - return '*******' - - }, opUserAppPassword() { return this.state.app_password_set }, @@ -601,13 +336,10 @@ export default { return this.form.serverHost.complete }, isAuthorizationMethodFormComplete() { - return this.isFormCompleted.authorizationMethod + return this.form.authenticationMethod.complete }, isAuthorizationSettingFormComplete() { - return this.isFormCompleted.authorizationSetting - }, - isOPOAuthFormComplete() { - return this.isFormCompleted.opOauth + return (this.form.openprojectOauth.complete && this.form.nextcloudOauth.complete) || this.form.ssoSettings.complete }, isManagedGroupFolderSetUpComplete() { return this.isFormCompleted.projectFolderSetUp @@ -615,43 +347,13 @@ export default { isOPUserAppPasswordFormComplete() { return this.isFormCompleted.opUserAppPassword }, - isNcOAuthFormComplete() { - return this.isFormCompleted.ncOauth - }, - isAuthorizationSettingsInViewMode() { - return this.formMode.authorizationSetting === F_MODES.VIEW - }, - isOPOAuthFormInView() { - return this.formMode.opOauth === F_MODES.VIEW - }, - isNcOAuthFormInEdit() { - return this.formMode.ncOauth === F_MODES.EDIT - }, - isOPOAuthFormInDisableMode() { - return this.formMode.opOauth === F_MODES.DISABLE - }, - isAuthorizationSettingFormInDisabledMode() { - return this.formMode.authorizationSetting === F_MODES.DISABLE - }, isOPUserAppPasswordFormInEdit() { return this.formMode.opUserAppPassword === F_MODES.EDIT }, isProjectFolderSetupFormInEdit() { return this.formMode.projectFolderSetUp === F_MODES.EDIT }, - isProjectFolderSetupFormInDisableMode() { - return this.formMode.projectFolderSetUp === F_MODES.DISABLE - }, - isAuthorizationSettingInEditMode() { - return this.formMode.authorizationSetting === F_MODES.EDIT - }, - isSSOSettingsInEditMode() { - return this.formMode.SSOSettings === F_MODES.EDIT - }, - isNcOAuthFormInDisableMode() { - return this.formMode.ncOauth === F_MODES.DISABLE - }, - isProjectFolderSetUpInDisableMode() { + isProjectFolderFormInDisableMode() { return this.formMode.projectFolderSetUp === F_MODES.DISABLE }, isOPUserAppPasswordInDisableMode() { @@ -672,24 +374,11 @@ export default { isOidcMethod() { return this.getCurrentAuthMethod === AUTH_METHOD.OIDC }, - showOAuthSettings() { - return this.isOAuthMethod || !this.form.authenticationMethod.complete - }, adminFileStorageHref() { const path = '%s/admin/settings/storages' const host = this.form.serverHost.value return util.format(path, host) }, - openProjectClientHint() { - const linkText = t('integration_openproject', 'Administration > File storages') - const htmlLink = `${linkText}` - return t('integration_openproject', 'Go to your OpenProject {htmlLink} as an Administrator and start the setup and copy the values here.', { htmlLink }, null, { escape: false, sanitize: false }) - }, - nextcloudClientHint() { - const linkText = t('integration_openproject', 'Administration > File storages') - const htmlLink = `${linkText}` - return t('integration_openproject', 'Copy the following values back into the OpenProject {htmlLink} as an Administrator.', { htmlLink }, null, { escape: false, sanitize: false }) - }, userAppPasswordHint() { const linkText = t('integration_openproject', 'Administration > File storages') const htmlLink = `${linkText}` @@ -714,32 +403,14 @@ export default { const htmlLink = `${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() { + isSetupComplete() { return (this.isServerHostFormComplete && this.isAuthorizationMethodFormComplete - && this.isOPOAuthFormComplete - && this.isNcOAuthFormComplete + && this.isAuthorizationSettingFormComplete && this.isManagedGroupFolderSetUpComplete && !this.isOPUserAppPasswordFormInEdit ) }, - isIntegrationCompleteWithOIDC() { - return (this.isServerHostFormComplete - && this.isAuthorizationMethodFormComplete - && this.isAuthorizationSettingFormComplete - && this.isManagedGroupFolderSetUpComplete - && !this.isOPUserAppPasswordFormInEdit - ) - }, isSetupCompleteWithoutProjectFolders() { if (this.isProjectFolderSetupFormInEdit) { return false @@ -756,114 +427,42 @@ 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.ssoSettings.complete'() { + if (this.form.ssoSettings.complete && this.formMode.projectFolderSetUp === F_MODES.DISABLE) { + this.formMode.projectFolderSetUp = F_MODES.EDIT + this.showDefaultManagedProjectFolders = true + this.isProjectFolderSwitchEnabled = true + this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup } }, - 'form.authenticationMethod.complete'() { - if (this.form.authenticationMethod.complete && this.formMode.authorizationSetting === F_MODES.DISABLE) { - this.formMode.authorizationSetting = F_MODES.EDIT + 'form.nextcloudOauth.complete'() { + if (this.form.nextcloudOauth.complete && 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,106 +479,25 @@ 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 { this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange } - // for oauth2 authorization - if (this.state.openproject_instance_url - && this.state.openproject_client_id - && this.state.openproject_client_secret - && this.state.nc_oauth_client - ) { - 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 - ) { + if (this.state.openproject_instance_url && this.isAuthorizationSettingFormComplete) { this.showDefaultManagedProjectFolders = true + this.formMode.projectFolderSetUp = F_MODES.EDIT } 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 - } - } - if (this.state.nc_oauth_client) { - this.formMode.ncOauth = F_MODES.VIEW - this.isFormCompleted.ncOauth = true - } - if (!this.state.nc_oauth_client - && this.state.openproject_instance_url - && this.state.openproject_client_id - && this.state.openproject_client_secret - && this.textLabelProjectFolderSetupButton === 'Keep current setup') { + if (this.textLabelProjectFolderSetupButton === 'Keep current setup') { this.showDefaultManagedProjectFolders = true this.formMode.projectFolderSetUp = F_MODES.VIEW this.isFormCompleted.projectFolderSetUp = true } - if (this.formMode.ncOauth === F_MODES.VIEW || this.formMode.authorizationSetting === F_MODES.VIEW) { - this.showDefaultManagedProjectFolders = true - } - if (this.showDefaultManagedProjectFolders) { - this.formMode.projectFolderSetUp = F_MODES.VIEW - this.isFormCompleted.projectFolderSetUp = true - } if (this.state.app_password_set) { this.formMode.opUserAppPassword = F_MODES.VIEW this.isFormCompleted.opUserAppPassword = true @@ -988,35 +506,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 @@ -1030,16 +523,6 @@ export default { this.formMode.projectFolderSetUp = F_MODES.VIEW this.isProjectFolderSetupCorrect = true }, - async setNCOAuthFormToViewMode() { - this.formMode.ncOauth = F_MODES.VIEW - this.isFormCompleted.ncOauth = true - if (!this.isIntegrationCompleteWithOauth2 && 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 - } - }, setOPUserAppPasswordToViewMode() { this.formMode.opUserAppPassword = F_MODES.VIEW this.isFormCompleted.opUserAppPassword = true @@ -1091,77 +574,6 @@ export default { this.projectFolderSetupError = null } }, - async saveOPOAuthClientValues() { - this.isFormStep = FORM.OP_OAUTH - if (await this.saveOPOptions()) { - this.formMode.opOauth = F_MODES.VIEW - this.isFormCompleted.opOauth = true - - // if we do not have Nextcloud OAuth client yet, a new client is created - if (!this.state.nc_oauth_client) { - this.createNCOAuthClient() - } - } - }, - 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.'), - t('integration_openproject', 'Replace OpenProject OAuth values'), - { - type: OC.dialogs.YES_NO_BUTTONS, - confirm: t('integration_openproject', 'Yes, replace'), - confirmClasses: 'error', - cancel: t('integration_openproject', 'Cancel'), - }, - async (result) => { - if (result) { - await this.clearOPOAuthClientValues() - } - }, - true, - ) - }, - async clearOPOAuthClientValues() { - this.isFormStep = FORM.OP_OAUTH - this.formMode.opOauth = F_MODES.EDIT - this.isFormCompleted.opOauth = false - this.state.openproject_client_id = null - this.state.openproject_client_secret = null - const saved = await this.saveOPOptions() - if (!saved) { - this.formMode.opOauth = F_MODES.VIEW - this.isFormCompleted.opOauth = true - } - }, resetAllAppValuesConfirmation() { OC.dialogs.confirmDestructive( t('integration_openproject', 'Are you sure that you want to reset this app and delete all settings and all connections of all Nextcloud users to OpenProject?'), @@ -1186,11 +598,6 @@ export default { // also, form completeness should be set to false // 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 this.oPUserAppPassword = null @@ -1201,7 +608,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 +635,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 = { @@ -1278,13 +668,11 @@ export default { this.state.app_password_set = true this.oPUserAppPassword = response?.data?.oPUserAppPassword } - this.oPOAuthTokenRevokeStatus = response?.data?.oPOAuthTokenRevokeStatus showSuccess(t('integration_openproject', 'OpenProject admin options saved')) success = true } catch (error) { console.error() this.isAdminConfigOk = null - this.oPOAuthTokenRevokeStatus = null if (error.response.data.error) { this.projectFolderSetupError = error.response.data.error } @@ -1292,7 +680,6 @@ export default { t('integration_openproject', 'Failed to save OpenProject admin options'), ) } - this.notifyAboutOPOAuthTokenRevoke() return success }, async checkIfProjectFolderIsAlreadyReadyForSetup() { @@ -1306,46 +693,6 @@ export default { } return success }, - notifyAboutOPOAuthTokenRevoke() { - switch (this.oPOAuthTokenRevokeStatus) { - case 'connection_error': - showError( - t('integration_openproject', 'Failed to perform revoke request due to connection error with the OpenProject server'), - ) - break - case 'other_error': - showError( - t('integration_openproject', 'Failed to revoke some users\' OpenProject OAuth access tokens'), - ) - break - case 'success': - showSuccess( - t('integration_openproject', 'Successfully revoked users\' OpenProject OAuth access tokens'), - ) - break - default: - break - } - }, - resetNcOauthValues() { - OC.dialogs.confirmDestructive( - t('integration_openproject', 'If you proceed you will need to update the settings in your OpenProject with the new Nextcloud OAuth credentials. Also, all users in OpenProject will need to reauthorize access to their Nextcloud account.'), - t('integration_openproject', 'Replace Nextcloud OAuth values'), - { - type: OC.dialogs.YES_NO_BUTTONS, - confirm: t('integration_openproject', 'Yes, replace'), - confirmClasses: 'error', - cancel: t('integration_openproject', 'Cancel'), - }, - async (result) => { - if (result) { - this.state.nc_oauth_client = null - this.createNCOAuthClient() - } - }, - true, - ) - }, async completeIntegrationWithoutProjectFolderSetUp() { this.isFormStep = FORM.GROUP_FOLDER this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange @@ -1390,21 +737,6 @@ export default { this.isFormCompleted.opUserAppPassword = false await this.saveOPOptions() }, - createNCOAuthClient() { - const url = generateUrl('/apps/integration_openproject/nc-oauth') - axios.post(url).then((response) => { - this.state.nc_oauth_client = response.data - // generate part is complete but still the NC OAuth form is set to - // edit mode and not completed state so that copy buttons will be available for the user - this.formMode.ncOauth = F_MODES.EDIT - this.isFormCompleted.ncOauth = false - }).catch((error) => { - showError( - t('integration_openproject', 'Failed to create Nextcloud OAuth client') - + ': ' + error.response.request.responseText, - ) - }) - }, setDefaultConfig() { const url = generateUrl('/apps/integration_openproject/admin-config') const req = { @@ -1422,9 +754,6 @@ export default { ) }) }, - onSelectOIDCProvider(selectedOption) { - this.authorizationSetting.currentOIDCProviderSelected = selectedOption - }, }, } @@ -1506,54 +835,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/FormOAuthSettings.vue b/src/components/admin/FormOAuthSettings.vue new file mode 100644 index 000000000..25df849a8 --- /dev/null +++ b/src/components/admin/FormOAuthSettings.vue @@ -0,0 +1,425 @@ + + + + + + + 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..cf9c2cbaa 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,1950 +125,45 @@ 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: { - 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', - }, - groupfolders: { - enabled: true, - supported: true, - minimum_version: '1.0.0', - name: 'Team folders', - }, - }, -} - -describe('AdminSettings.vue', () => { - afterEach(() => { - jest.restoreAllMocks() - }) - const confirmSpy = jest.spyOn(global.OC.dialogs, 'confirmDestructive') - - describe('form mode and completed status without project folder setup for OAUTH2 authorization config', () => { - it.each([ - [ - 'with empty state', - { - openproject_instance_url: null, - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - { - authorizationMethod: F_MODES.EDIT, - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete OpenProject Authorization Method', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - { - authorizationMethod: F_MODES.EDIT, - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete OpenProject 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, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.EDIT, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with complete OpenProject OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'abcd', - openproject_client_secret: 'abcdefgh', - nc_oauth_client: null, - fresh_project_folder_setup: true, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.VIEW, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: true, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with everything but empty OpenProject 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: { - nextcloud_client_id: 'some-client-id-here', - nextcloud_client_secret: 'some-client-secret-here', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.EDIT, - ncOauth: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: false, - ncOauth: true, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - [ - 'with a complete admin settings', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'client-id-here', - openproject_client_secret: 'client-id-here', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id-here', - nextcloud_client_secret: 'nc-client-secret-here', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.VIEW, - ncOauth: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: true, - ncOauth: 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.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) - expect(wrapper.vm.isFormCompleted.opUserAppPassword).toBe(expectedFormState.opUserAppPassword) - }) - }) - - 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 - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: '' } })) - beforeEach(() => { - wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'openproject-client-id', - openproject_client_secret: 'openproject-client-secret', - nc_oauth_client: null, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - opOAuthForm = wrapper.find(selectors.opOauthForm) - resetButton = opOAuthForm.find(selectors.resetOPOAuthFormButton) - }) - it('should show field values and hide the form if server host form is complete', () => { - expect(opOAuthForm).toMatchSnapshot() - }) - describe('reset button', () => { - it('should trigger confirm dialog on click', async () => { - await resetButton.trigger('click') - expect(confirmSpy).toBeCalledTimes(1) - - const expectedDialogMessage = '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.' - const expectedDialogTitle = 'Replace OpenProject OAuth values' - const expectedDialogOpts = { - cancel: 'Cancel', - confirm: 'Yes, replace', - confirmClasses: 'error', - type: 70, - } - expect(confirmSpy).toHaveBeenCalledWith( - expectedDialogMessage, - expectedDialogTitle, - expectedDialogOpts, - expect.any(Function), - true, - ) - jest.clearAllMocks() - wrapper.destroy() - }) - it('should clear values on confirm', async () => { - jest.clearAllMocks() - await wrapper.vm.clearOPOAuthClientValues() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(wrapper.vm.state.openproject_client_id).toBe(null) - }) - }) - }) - describe('edit mode', () => { - let wrapper - beforeEach(() => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ - data: { - clientId: 'nc-client-id101', - clientSecret: 'nc-client-secret101', - }, - })) - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: null, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - }) - afterEach(() => { - axios.post.mockReset() - axios.put.mockReset() - jest.clearAllMocks() - wrapper.destroy() - }) - it('should show the form and hide the field values', () => { - expect(wrapper.find(selectors.opOauthForm)).toMatchSnapshot() - }) - describe('submit button', () => { - it('should be enabled with complete client values', async () => { - let submitButton - submitButton = wrapper.find(selectors.submitOPOAuthFormButton) - expect(submitButton.attributes().disabled).toBe('true') - await wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - - submitButton = wrapper.find(selectors.submitOPOAuthFormButton) - expect(submitButton.attributes().disabled).toBe(undefined) - }) - describe('when clicked', () => { - describe('when the save is successful', () => { - beforeEach(async () => { - jest.spyOn(wrapper.vm, 'saveOPOptions').mockReturnValue(true) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - }) - it('should set the form to view mode', async () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.VIEW) - }) - it('should set the isFormCompleted to true', async () => { - expect(wrapper.vm.isFormCompleted.opOauth).toBe(true) - }) - - it('should not create Nextcloud OAuth client if already present', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) - - it('should create Nextcloud OAuth client if not already present', async () => { - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ data: { status: false } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: '', - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - - expect(createNCOAuthClientSpy).toBeCalledTimes(1) - }) - }) - - describe('when the save fails', () => { - beforeEach(async () => { - jest.spyOn(wrapper.vm, 'saveOPOptions').mockReturnValue(false) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - }) - it('should set the form to view mode', async () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.EDIT) - }) - it('should set the isFormCompleted to true', async () => { - expect(wrapper.vm.isFormCompleted.opOauth).toBe(false) - }) - - it('should not create Nextcloud OAuth client', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) - }) - - describe('when the admin config is ok on save options', () => { - beforeEach(async () => { - await wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - await flushPromises() - }) - it('should set the form to view mode', () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.VIEW) - }) - it('should set the adminConfigStatus as "true"', () => { - expect(wrapper.vm.isAdminConfigOk).toBe(true) - }) - it('should create Nextcloud OAuth client if not already present', () => { - expect(wrapper.vm.state.nc_oauth_client).toMatchObject({ - clientId: 'nc-client-id101', - clientSecret: 'nc-client-secret101', - }) - }) - it('should not create Nextcloud OAuth client if already present', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - await flushPromises() - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) +const appState = { + apps: { + 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', + }, + groupfolders: { + enabled: true, + supported: true, + minimum_version: '1.0.0', + name: 'Team folders', + }, + }, +} - it('should not create new user app password if already present', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { oPUserAppPassword: null } })) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: false, - }, - oPUserAppPassword: 'opUserPassword', - }) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - openproject_client_id: 'qwerty', - openproject_client_secret: 'qwerty', - }, - }, - ) - expect(wrapper.vm.oPUserAppPassword).toBe('opUserPassword') - }) - }) - }) - }) - }) +describe('AdminSettings.vue', () => { + afterEach(() => { + jest.restoreAllMocks() }) - describe('Nextcloud OAuth values form', () => { - describe('view mode with complete values', () => { - it('should show the field values and hide the form', () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'some-client-id-here', - openproject_client_secret: 'some-client-secret-here', - nc_oauth_client: { - nextcloud_client_id: 'some-nc-client-id-here', - nextcloud_client_secret: 'some-nc-client-secret-here', - }, - }, - }) - expect(wrapper.find(selectors.ncOauthForm)).toMatchSnapshot() - }) - describe('reset button', () => { - afterEach(() => { - jest.clearAllMocks() - }) - it('should trigger the confirm dialog', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-clientid', - nextcloud_client_secret: 'nc-clientsecret', - }, - }, - }) - - const expectedConfirmText = 'If you proceed you will need to update the settings in your OpenProject ' - + 'with the new Nextcloud OAuth credentials. Also, all users in OpenProject ' - + 'will need to reauthorize access to their Nextcloud account.' - const expectedConfirmOpts = { - cancel: 'Cancel', - confirm: 'Yes, replace', - confirmClasses: 'error', - type: 70, - } - const expectedConfirmTitle = 'Replace Nextcloud OAuth values' - - const resetButton = wrapper.find(selectors.resetNcOAuthFormButton) - resetButton.vm.$emit('click') - await flushPromises() - - expect(confirmSpy).toBeCalledTimes(1) - expect(confirmSpy).toBeCalledWith( - expectedConfirmText, - expectedConfirmTitle, - expectedConfirmOpts, - expect.any(Function), - true, - ) - wrapper.destroy() - }) - it('should create new client on confirm', async () => { - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ - data: { - clientId: 'new-client-id77', - clientSecret: 'new-client-secret77', - }, - })) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id', - nextcloud_client_secret: 'nc-client-secret', - }, - }, - }) - await wrapper.vm.createNCOAuthClient() - expect(wrapper.vm.state.nc_oauth_client).toMatchObject({ - clientId: 'new-client-id77', - clientSecret: 'new-client-secret77', - }) - expect(wrapper.vm.formMode.ncOauth).toBe(F_MODES.EDIT) - expect(wrapper.vm.isFormCompleted.ncOauth).toBe(false) - wrapper.destroy() - }) - }) - }) - describe('recreate button', () => { - it('should be displayed if nextcloud oauth credentials is empty and everything is set', async () => { - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: null, - }, - formMode: { - projectFolderSetUp: F_MODES.VIEW, - }, - showDefaultManagedProjectFolders: true, - isFormCompleted: { - projectFolderSetUp: true, - }, - - }) - const resetButton = wrapper.find(selectors.resetNcOAuthFormButton) - expect(resetButton.isVisible()).toBe(true) - expect(resetButton.text()).toBe('Create Nextcloud OAuth values') - wrapper.destroy() - }) - }) - describe('edit mode', () => { - it('should show the form and hide the field values', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id', - nextcloud_client_secret: 'nc-client-secret', - }, - }, - }) - await wrapper.setData({ - formMode: { - ncOauth: F_MODES.EDIT, - }, - }) - expect(wrapper.find(selectors.ncOauthForm)).toMatchSnapshot() - }) - describe('done button', () => { - it('should set the form to view mode if the oauth values are complete', async () => { - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'some-client-id-for-op', - openproject_client_secret: 'some-client-secret-for-op', - nc_oauth_client: { - nextcloud_client_id: 'something', - nextcloud_client_secret: 'something-else', - }, - }, - }) - await wrapper.setData({ - formMode: { - ncOauth: F_MODES.EDIT, - }, - }) - await wrapper.find(selectors.ncOauthForm) - .find(selectors.submitNcOAuthFormButton) - .trigger('click') - expect(wrapper.vm.formMode.ncOauth).toBe(F_MODES.VIEW) - expect(wrapper.vm.isFormCompleted.ncOauth).toBe(true) - }) - }) - }) - }) + const commonState = { + form: { + serverHost: { complete: true }, + authenticationMethod: { + value: AUTH_METHOD.OAUTH2, + complete: true, + }, + openprojectOauth: { complete: true }, + nextcloudOauth: { complete: true }, + }, + } describe('Project folders form (Project Folder Setup)', () => { describe('Disable mode', () => { @@ -2128,14 +223,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_id: 'some-nc-client-id-here', nextcloud_client_secret: 'some-nc-client-secret-here', }, - fresh_project_folder_setup: true, - // project folder is already not set up + fresh_project_folder_setup: false, + // project folder is already set up project_folder_info: { - status: false, + status: true, }, app_password_set: false, ...appState, }, + ...commonState, }) const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) const actualProjectFolderStatusValue = projectFolderStatus.text() @@ -2170,6 +266,7 @@ describe('AdminSettings.vue', () => { formMode: { projectFolderSetUp: F_MODES.VIEW, }, + ...commonState, }) const formHeading = wrapper.find(selectors.projectFolderFormHeading) const errorNote = wrapper.find(selectors.projectFolderErrorNote) @@ -2245,15 +342,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'some-nc-client-secret-here', }, fresh_project_folder_setup: true, - // project folder is already not set up + // project folder is not set up project_folder_info: { status: false, }, app_password_set: false, ...appState, }, + ...commonState, }) - await wrapper.vm.setNCOAuthFormToViewMode() expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Setup OpenProject user, group and folder') @@ -2278,8 +375,8 @@ describe('AdminSettings.vue', () => { app_password_set: false, ...appState, }, + ...commonState, }) - await wrapper.vm.setNCOAuthFormToViewMode() expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupButtonStub) projectFolderSetupSwitchButton.vm.$emit('update:checked', false) @@ -2320,6 +417,7 @@ describe('AdminSettings.vue', () => { }, app_password_set: false, }, + ...commonState, }) await wrapper.setData({ formMode: { @@ -2413,6 +511,7 @@ describe('AdminSettings.vue', () => { projectFolderSetupError: null, ...appState, }, + ...commonState, }) await wrapper.setData({ @@ -2479,6 +578,7 @@ describe('AdminSettings.vue', () => { }, }, isGroupFolderAlreadySetup: null, + ...commonState, }) await wrapper.setData({ formMode: { @@ -3229,9 +1329,7 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'something-else', }, }, - form: { - serverHost: { complete: true }, - }, + ...commonState, }) const $defaultEnableNavigation = wrapper.find(selectors.defaultEnableNavigation) @@ -3273,9 +1371,7 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'something-else', }, }, - form: { - serverHost: { complete: true }, - }, + ...commonState, }) const $defaultEnableNavigation = wrapper.find(selectors.defaultEnableNavigation) await $defaultEnableNavigation.trigger('click') @@ -3287,60 +1383,6 @@ describe('AdminSettings.vue', () => { }) }) - describe('revoke OpenProject OAuth token', () => { - beforeEach(() => { - axios.put.mockReset() - dialogs.showSuccess.mockReset() - dialogs.showError.mockReset() - }) - it('should show success when revoke status is success', async () => { - dialogs.showSuccess - .mockImplementationOnce() - .mockImplementationOnce() - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce( - () => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: 'success' } }), - ) - const wrapper = getMountedWrapper({ - state: completeOAUTH2IntegrationState, - }) - await wrapper.vm.saveOPOptions() - - await localVue.nextTick() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledTimes(2) - expect(dialogs.showSuccess).toBeCalledWith('OpenProject admin options saved') - expect(dialogs.showSuccess).toBeCalledWith('Successfully revoked users\' OpenProject OAuth access tokens') - - }) - it.each([ - ['connection_error', 'Failed to perform revoke request due to connection error with the OpenProject server'], - ['other_error', 'Failed to revoke some users\' OpenProject OAuth access tokens'], - ])('should show error message on various failure', async (errorCode, errorMessage) => { - dialogs.showSuccess - .mockImplementationOnce() - .mockImplementationOnce() - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce( - () => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: errorCode } }), - ) - const wrapper = getMountedWrapper({ - state: completeOAUTH2IntegrationState, - }) - await wrapper.vm.saveOPOptions() - - await localVue.nextTick() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledTimes(1) - expect(dialogs.showError).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledWith('OpenProject admin options saved') - expect(dialogs.showError).toBeCalledWith(errorMessage) - - }) - }) - describe('terms of service', () => { const termsOfServiceComponentStub = 'termsofserviceunsigned-stub' const termsOfServiceComponentStubAttribute = 'isalltermsofservicesignedforuseropenproject' diff --git a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap index ae7fd9507..a74441053 100644 --- a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap +++ b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap @@ -1,483 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AdminSettings.vue Nextcloud OAuth values form edit mode should show the form and hide the field values 1`] = ` -Wrapper { - "selector": ".nextcloud-oauth-values", -} -`; - -exports[`AdminSettings.vue Nextcloud OAuth values form view mode with complete values should show the field values and hide the form 1`] = ` -Wrapper { - "selector": ".nextcloud-oauth-values", -} -`; - -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", -} -`; - -exports[`AdminSettings.vue OpenProject OAuth values form view mode and completed state should show field values and hide the form if server host form is complete 1`] = ` -Wrapper { - "selector": ".openproject-oauth-values", -} -`; - exports[`AdminSettings.vue Project folders form (Project Folder Setup) view mode without project folder setup should show status as "Inactive" 1`] = ` Wrapper { "selector": ".project-folder-setup", 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/FormOAuthSettings.spec.js b/tests/jest/components/admin/FormOAuthSettings.spec.js new file mode 100644 index 000000000..638e29594 --- /dev/null +++ b/tests/jest/components/admin/FormOAuthSettings.spec.js @@ -0,0 +1,706 @@ +/* jshint esversion: 8 */ + +/** + * SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createLocalVue, shallowMount } from '@vue/test-utils' +import flushPromises from 'flush-promises' // eslint-disable-line n/no-unpublished-import +import { showError, showSuccess } from '@nextcloud/dialogs' + +import { ADMIN_SETTINGS_FORM, F_MODES, AUTH_METHOD } from '../../../../src/utils.js' +import { saveAdminConfig, createNextcloudOAuthClient } from '../../../../src/api/settings.js' +import FormOAuthSettings from '../../../../src/components/admin/FormOAuthSettings.vue' + +// global mocks +global.t = (app, text) => text +global.OC = { + dialogs: { + confirmDestructive: jest.fn(), + YES_NO_BUTTONS: 70, + }, +} +// module mocks +jest.mock('@nextcloud/dialogs', () => ({ + showError: jest.fn(), + showSuccess: jest.fn(), +})) +jest.mock('../../../../src/api/settings.js', () => ({ + saveAdminConfig: jest.fn(() => ''), + createNextcloudOAuthClient: jest.fn(() => ''), +})) + +const localVue = createLocalVue() + +const selectors = { + opFormHeading: 'formheading-stub[title="OpenProject OAuth settings"]', + opFormContainer: '.oauth-settings--openproject', + opClientIdLabel: 'fieldvalue-stub[title="OpenProject OAuth client ID"]', + opClientIdInput: 'textinput-stub[label="OpenProject OAuth client ID"]', + opClientSecretLabel: 'fieldvalue-stub[title="OpenProject OAuth client secret"]', + opClientSecretInput: 'textinput-stub[label="OpenProject OAuth client secret"]', + opSaveButton: 'ncbutton-stub[data-test-id="submit-op-oauth-btn"]', + opResetButton: 'ncbutton-stub[data-test-id="reset-op-oauth-btn"]', + ncFormHeading: 'formheading-stub[title="Nextcloud OAuth client"]', + ncFormContainer: '.oauth-settings--nextcloud', + ncClientIdLabel: 'fieldvalue-stub[title="Nextcloud OAuth client ID"]', + ncClientIdInput: 'textinput-stub[label="Nextcloud OAuth client ID"]', + ncClientSecretLabel: 'fieldvalue-stub[title="Nextcloud OAuth client secret"]', + ncClientSecretInput: 'textinput-stub[label="Nextcloud OAuth client secret"]', + ncCreateButton: 'ncbutton-stub[data-test-id="create-nc-oauth-btn"]', + ncSaveButton: 'ncbutton-stub[data-test-id="submit-nc-oauth-btn"]', + ncResetButton: 'ncbutton-stub[data-test-id="reset-nc-oauth-btn"]', +} + +const formState = structuredClone(ADMIN_SETTINGS_FORM) +formState.serverHost.complete = true +formState.authenticationMethod.value = AUTH_METHOD.OAUTH2 +formState.authenticationMethod.complete = true +const defaultProps = { + formState, + oauthSettings: { + openproject_client_id: '', + openproject_client_secret: '', + nc_oauth_client: { + nextcloud_client_id: '', + }, + }, +} + +describe('Component: FormOAuthSettings', () => { + afterEach(() => { + jest.clearAllMocks() + saveAdminConfig.mockRestore() + createNextcloudOAuthClient.mockRestore() + }) + + describe('form states', () => { + it('form state: incomplete authorization method', () => { + const props = { + formState: { + ...defaultProps.formState, + authenticationMethod: { + complete: false, + }, + }, + } + const wrapper = getWrapper({ props }) + + const opFormHeading = wrapper.find(selectors.opFormHeading) + const ncFormHeading = wrapper.find(selectors.ncFormHeading) + const opFormContainer = wrapper.find(selectors.opFormContainer) + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(opFormHeading.exists()).toBe(true) + expect(opFormHeading.attributes().isdisabled).toBe('true') + expect(ncFormHeading.exists()).toBe(true) + expect(ncFormHeading.attributes().isdisabled).toBe('true') + expect(opFormContainer.exists()).toBe(false) + expect(ncFormContainer.exists()).toBe(false) + expect(wrapper.element).toMatchSnapshot() + }) + it('form state: complete authorization method but incomplete oauth settings', () => { + const wrapper = getWrapper() + + const opFormHeading = wrapper.find(selectors.opFormHeading) + const ncFormHeading = wrapper.find(selectors.ncFormHeading) + const opFormContainer = wrapper.find(selectors.opFormContainer) + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(opFormHeading.exists()).toBe(true) + expect(opFormHeading.attributes().isdisabled).toBe(undefined) + expect(ncFormHeading.exists()).toBe(true) + expect(ncFormHeading.attributes().isdisabled).toBe('true') + expect(opFormContainer.exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientIdInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientSecretLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientSecretInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).attributes().disabled).toBe('true') + expect(opFormContainer.find(selectors.opResetButton).exists()).toBe(false) + expect(ncFormContainer.exists()).toBe(false) + expect(wrapper.element).toMatchSnapshot() + }) + it('form state: complete authorization method and complete OpenProject form but not Nextcloud', () => { + const wrapper = getWrapper({ + props: { + formState: { + ...defaultProps.formState, + openprojectOauth: { + complete: true, + }, + }, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: '', + }, + }, + }, + }) + + const opFormHeading = wrapper.find(selectors.opFormHeading) + const ncFormHeading = wrapper.find(selectors.ncFormHeading) + const opFormContainer = wrapper.find(selectors.opFormContainer) + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(opFormHeading.exists()).toBe(true) + expect(opFormHeading.attributes().isdisabled).toBe(undefined) + expect(ncFormHeading.exists()).toBe(true) + expect(ncFormHeading.attributes().isdisabled).toBe(undefined) + expect(opFormContainer.exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).attributes().value).toBe('op-client') + expect(opFormContainer.find(selectors.opClientIdInput).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientSecretLabel).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientSecretLabel).attributes().value).toBe('op-client-secret') + expect(opFormContainer.find(selectors.opClientSecretInput).exists()).toBe(false) + expect(opFormContainer.find(selectors.opSaveButton).exists()).toBe(false) + expect(opFormContainer.find(selectors.opResetButton).exists()).toBe(true) + expect(ncFormContainer.exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientIdInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncCreateButton).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncSaveButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncResetButton).exists()).toBe(false) + expect(wrapper.element).toMatchSnapshot() + }) + it('form state: complete authorization method and complete Nextcloud form but not OpenProject', () => { + const wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: '', + openproject_client_secret: '', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + }, + }) + + const opFormHeading = wrapper.find(selectors.opFormHeading) + const ncFormHeading = wrapper.find(selectors.ncFormHeading) + const opFormContainer = wrapper.find(selectors.opFormContainer) + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(opFormHeading.exists()).toBe(true) + expect(opFormHeading.attributes().isdisabled).toBe(undefined) + expect(ncFormHeading.exists()).toBe(true) + expect(ncFormHeading.attributes().isdisabled).toBe(undefined) + expect(opFormContainer.exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientIdInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientSecretLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientSecretInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).attributes().disabled).toBe('true') + expect(opFormContainer.find(selectors.opResetButton).exists()).toBe(false) + expect(ncFormContainer.exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).attributes().value).toBe('nc-client-id') + expect(ncFormContainer.find(selectors.ncClientIdInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretLabel).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientSecretLabel).attributes().value).toBe('***') + expect(ncFormContainer.find(selectors.ncClientSecretInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncCreateButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncSaveButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncResetButton).exists()).toBe(true) + expect(wrapper.element).toMatchSnapshot() + }) + it('form state: complete OpenProject and Nextcloud oauth values', async () => { + const wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + }, + }) + + const opFormHeading = wrapper.find(selectors.opFormHeading) + const ncFormHeading = wrapper.find(selectors.ncFormHeading) + const opFormContainer = wrapper.find(selectors.opFormContainer) + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(opFormHeading.exists()).toBe(true) + expect(opFormHeading.attributes().isdisabled).toBe(undefined) + expect(ncFormHeading.exists()).toBe(true) + expect(ncFormHeading.attributes().isdisabled).toBe(undefined) + expect(opFormContainer.exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).attributes().value).toBe('op-client') + expect(opFormContainer.find(selectors.opClientIdInput).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientSecretLabel).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientSecretLabel).attributes().value).toBe('op-client-secret') + expect(opFormContainer.find(selectors.opClientSecretInput).exists()).toBe(false) + expect(opFormContainer.find(selectors.opSaveButton).exists()).toBe(false) + expect(opFormContainer.find(selectors.opResetButton).exists()).toBe(true) + expect(ncFormContainer.exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).attributes().value).toBe('nc-client-id') + expect(ncFormContainer.find(selectors.ncClientIdInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretLabel).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientSecretLabel).attributes().value).toBe('***') + expect(ncFormContainer.find(selectors.ncClientSecretInput).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncSaveButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncCreateButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncResetButton).exists()).toBe(true) + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('OpenProject OAuth form', () => { + describe('view mode: reset values', () => { + let wrapper + beforeEach(() => { + wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + }, + }) + }) + + it('should trigger confirm dialog on click', async () => { + const spyConfirmDialog = jest.spyOn(global.OC.dialogs, 'confirmDestructive') + const resetButton = wrapper.find(selectors.opResetButton) + await resetButton.vm.$emit('click') + await flushPromises() + + expect(spyConfirmDialog).toHaveBeenCalledTimes(1) + }) + it('should clear OpenProject OAuth values on confirm', async () => { + await wrapper.vm.confirmResetOpenProjectClient() + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + openproject_client_id: '', + openproject_client_secret: '', + }) + expect(wrapper.vm.currentOpenProjectForm.clientId).toBe('') + expect(wrapper.vm.currentOpenProjectForm.clientSecret).toBe('') + expect(wrapper.vm.openprojectFormMode).toBe(F_MODES.EDIT) + const opFormContainer = wrapper.find(selectors.opFormContainer) + expect(opFormContainer.exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientIdLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientIdInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opClientSecretLabel).exists()).toBe(false) + expect(opFormContainer.find(selectors.opClientSecretInput).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).exists()).toBe(true) + expect(opFormContainer.find(selectors.opSaveButton).attributes().disabled).toBe('true') + expect(opFormContainer.find(selectors.opResetButton).exists()).toBe(false) + }) + }) + describe('edit mode', () => { + let wrapper + beforeEach(() => { + wrapper = getWrapper() + }) + + it('should not enable save button if the form is incomplete', async () => { + const saveButton = wrapper.find(selectors.opSaveButton) + expect(saveButton.attributes().disabled).toBe('true') + + await wrapper.find(selectors.opClientIdInput).vm.$emit('input', 'op-client-id') + + expect(saveButton.attributes().disabled).toBe('true') + }) + it('should enable save button if the form is complete', async () => { + const saveButton = wrapper.find(selectors.opSaveButton) + expect(saveButton.attributes().disabled).toBe('true') + + await wrapper.find(selectors.opClientIdInput).vm.$emit('input', 'op-client-id') + await wrapper.find(selectors.opClientSecretInput).vm.$emit('input', 'op-client-secret') + + expect(saveButton.attributes().disabled).toBe(undefined) + }) + + describe('save action', () => { + const opClientId = 'op-client-id' + const opClientSecret = 'op-client-secret' + + describe('when the save is successful', () => { + it('should show success and set form to view mode', async () => { + createNextcloudOAuthClient.mockImplementation(() => Promise.resolve({ + data: { + nextcloud_client_id: 'nc-client-id', + nextcloud_client_secret: 'nc-client-secret', + }, + })) + const wrapper = getWrapper() + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient') + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + wrapper.find(selectors.opClientIdInput).vm.$emit('input', opClientId) + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', opClientSecret) + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + openproject_client_id: opClientId, + openproject_client_secret: opClientSecret, + }) + + expect(wrapper.vm.savedOpenProjectForm.clientId).toBe(opClientId) + expect(wrapper.vm.savedOpenProjectForm.clientSecret).toBe(opClientSecret) + expect(wrapper.vm.openprojectFormMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.opClientIdLabel).exists()).toBe(true) + expect(wrapper.find(selectors.opClientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.opClientSecretLabel).exists()).toBe(true) + expect(wrapper.find(selectors.opClientSecretInput).exists()).toBe(false) + expect(wrapper.find(selectors.opResetButton).exists()).toBe(true) + + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + expect(spyCreateNextcloudClient).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + + expect(wrapper.vm.nextcloudFormMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.ncClientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.ncClientIdInput).attributes().value).toBe('nc-client-id') + expect(wrapper.find(selectors.ncClientIdLabel).exists()).toBe(false) + expect(wrapper.find(selectors.ncClientSecretInput).exists()).toBe(true) + expect(wrapper.find(selectors.ncClientSecretInput).attributes().value).toBe('nc-client-secret') + expect(wrapper.find(selectors.ncClientSecretLabel).exists()).toBe(false) + expect(wrapper.find(selectors.ncSaveButton).exists()).toBe(true) + expect(wrapper.find(selectors.ncCreateButton).exists()).toBe(false) + expect(wrapper.find(selectors.ncResetButton).exists()).toBe(false) + }) + + it('should not create Nextcloud OAuth client if already present', async () => { + const wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: '', + openproject_client_secret: '', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + }, + }) + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient') + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + wrapper.find(selectors.opClientIdInput).vm.$emit('input', opClientId) + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', opClientSecret) + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(spyCreateNextcloudClient).not.toHaveBeenCalled() + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(0) + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + }) + }) + + describe('when the save fails', () => { + it('should show error and keep the form in edit mode', async () => { + saveAdminConfig.mockImplementation(() => Promise.reject(new Error('Failure'))) + const wrapper = getWrapper() + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient').mockImplementation(() => jest.fn()) + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + + wrapper.find(selectors.opClientIdInput).vm.$emit('input', opClientId) + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', opClientSecret) + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + + expect(wrapper.vm.savedOpenProjectForm.clientId).toBe('') + expect(wrapper.vm.savedOpenProjectForm.clientSecret).toBe('') + expect(wrapper.vm.openprojectFormMode).toBe(F_MODES.EDIT) + expect(spyCreateNextcloudClient).toHaveBeenCalledTimes(0) + + expect(wrapper.vm.openprojectTokenRevokeStatus).toBe(null) + expect(showSuccess).toHaveBeenCalledTimes(0) + expect(showError).toHaveBeenCalledTimes(1) + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + }) + it('should show error if Nextcloud OAuth client creation fails', async () => { + createNextcloudOAuthClient.mockImplementation(() => Promise.reject(new Error('Failure'))) + const wrapper = getWrapper() + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient') + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + + wrapper.find(selectors.opClientIdInput).vm.$emit('input', opClientId) + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', opClientSecret) + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(wrapper.vm.savedOpenProjectForm.clientId).toBe(opClientId) + expect(wrapper.vm.savedOpenProjectForm.clientSecret).toBe(opClientSecret) + expect(wrapper.vm.openprojectFormMode).toBe(F_MODES.VIEW) + + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + expect(spyCreateNextcloudClient).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(1) + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + }) + }) + }) + }) + }) + + describe('Nextcloud OAuth form', () => { + describe('view mode: reset button', () => { + let wrapper + beforeEach(() => { + wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + }, + }) + }) + + it('should trigger confirm dialog on click', async () => { + const spyConfirmDialog = jest.spyOn(global.OC.dialogs, 'confirmDestructive') + const resetButton = wrapper.find(selectors.ncResetButton) + resetButton.vm.$emit('click') + await flushPromises() + + expect(spyConfirmDialog).toHaveBeenCalledTimes(1) + }) + it('should create new client on confirm', async () => { + createNextcloudOAuthClient.mockImplementationOnce(() => Promise.resolve({ + data: { + nextcloud_client_id: 'new-client-id', + nextcloud_client_secret: 'new-client-secret', + }, + })) + await wrapper.vm.createNextcloudClient() + await flushPromises() + + expect(createNextcloudOAuthClient).toHaveBeenCalledTimes(1) + expect(wrapper.vm.savedNextcloudForm.clientId).toBe('new-client-id') + expect(wrapper.vm.savedNextcloudForm.clientSecret).toBe('new-client-secret') + expect(wrapper.vm.nextcloudFormMode).toBe(F_MODES.EDIT) + + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(ncFormContainer.exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientIdInput).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdInput).attributes().value).toBe('new-client-id') + expect(ncFormContainer.find(selectors.ncClientSecretLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretInput).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientSecretInput).attributes().value).toBe('new-client-secret') + + expect(ncFormContainer.find(selectors.ncSaveButton).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncSaveButton).attributes().disabled).toBe(undefined) + expect(ncFormContainer.find(selectors.ncSaveButton).text()).toBe('Yes, I have copied these values') + expect(ncFormContainer.find(selectors.ncResetButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncCreateButton).exists()).toBe(false) + }) + }) + + describe('create button', () => { + let wrapper + beforeEach(() => { + createNextcloudOAuthClient.mockImplementationOnce(() => Promise.resolve({ + data: { + nextcloud_client_id: 'nc-client-id', + nextcloud_client_secret: 'nc-client-secret', + }, + })) + wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: '', + }, + }, + formState: { + ...defaultProps.formState, + openprojectOauth: { + complete: true, + }, + }, + }, + }) + }) + it('should show create button if other settings are set but not Nextcloud OAuth client', async () => { + const createButton = wrapper.find(selectors.ncCreateButton) + expect(createButton.exists()).toBe(true) + expect(createButton.text()).toBe('Create Nextcloud OAuth values') + expect(wrapper.find(selectors.ncResetButton).exists()).toBe(false) + expect(wrapper.find(selectors.ncSaveButton).exists()).toBe(false) + }) + + it('should create Nextcloud OAuth client and set the form mode to edit', async () => { + const createButton = wrapper.find(selectors.ncCreateButton) + createButton.vm.$emit('click') + await flushPromises() + + expect(createNextcloudOAuthClient).toHaveBeenCalledTimes(1) + expect(wrapper.vm.savedNextcloudForm.clientId).toBe('nc-client-id') + expect(wrapper.vm.savedNextcloudForm.clientSecret).toBe('nc-client-secret') + expect(wrapper.vm.nextcloudFormMode).toBe(F_MODES.EDIT) + + const ncFormContainer = wrapper.find(selectors.ncFormContainer) + expect(ncFormContainer.exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientIdInput).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientIdInput).attributes().value).toBe('nc-client-id') + expect(ncFormContainer.find(selectors.ncClientSecretLabel).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncClientSecretInput).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncClientSecretInput).attributes().value).toBe('nc-client-secret') + + expect(ncFormContainer.find(selectors.ncSaveButton).exists()).toBe(true) + expect(ncFormContainer.find(selectors.ncSaveButton).attributes().disabled).toBe(undefined) + expect(ncFormContainer.find(selectors.ncSaveButton).text()).toBe('Yes, I have copied these values') + expect(ncFormContainer.find(selectors.ncResetButton).exists()).toBe(false) + expect(ncFormContainer.find(selectors.ncCreateButton).exists()).toBe(false) + }) + }) + + describe('edit mode', () => { + it('should set the form to view mode on save', async () => { + createNextcloudOAuthClient.mockImplementationOnce(() => Promise.resolve({ + data: { + nextcloud_client_id: 'new-client-id', + nextcloud_client_secret: 'new-client-secret', + }, + })) + const wrapper = getWrapper({ + props: { + ...defaultProps, + oauthSettings: { + openproject_client_id: 'op-client', + openproject_client_secret: 'op-client-secret', + nc_oauth_client: { + nextcloud_client_id: 'nc-client-id', + }, + }, + formState: { + ...defaultProps.formState, + openprojectOauth: { + complete: true, + }, + }, + }, + }) + const resetButton = wrapper.find(selectors.ncResetButton) + resetButton.vm.$emit('click') + await wrapper.vm.createNextcloudClient() + await flushPromises() + + expect(wrapper.vm.nextcloudFormMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.ncClientIdInput).exists()).toBe(true) + expect(wrapper.find(selectors.ncClientSecretInput).exists()).toBe(true) + expect(wrapper.find(selectors.ncClientSecretInput).exists()).toBe(true) + expect(wrapper.find(selectors.ncResetButton).exists()).toBe(false) + expect(wrapper.find(selectors.ncCreateButton).exists()).toBe(false) + + const saveButton = wrapper.find(selectors.ncSaveButton) + expect(saveButton.exists()).toBe(true) + saveButton.vm.$emit('click') + await flushPromises() + + expect(wrapper.vm.nextcloudFormMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.ncClientIdInput).exists()).toBe(false) + expect(wrapper.find(selectors.ncClientIdLabel).exists()).toBe(true) + expect(wrapper.find(selectors.ncClientSecretInput).exists()).toBe(false) + expect(wrapper.find(selectors.ncClientSecretLabel).exists()).toBe(true) + expect(wrapper.find(selectors.ncResetButton).exists()).toBe(true) + expect(wrapper.find(selectors.ncCreateButton).exists()).toBe(false) + expect(wrapper.find(selectors.ncSaveButton).exists()).toBe(false) + }) + }) + }) + + describe('revoke OpenProject OAuth token', () => { + it('should show success when revoke status is success', async () => { + saveAdminConfig.mockImplementationOnce(() => Promise.resolve({ + data: { + oPOAuthTokenRevokeStatus: 'success', + }, + })) + const wrapper = getWrapper() + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient').mockImplementation(() => jest.fn()) + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + wrapper.find(selectors.opClientIdInput).vm.$emit('input', 'id') + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', 'secret') + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(spyCreateNextcloudClient).toHaveBeenCalledTimes(1) + expect(wrapper.vm.openprojectTokenRevokeStatus).toBe('success') + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledTimes(2) + expect(showError).toHaveBeenCalledTimes(0) + expect(showSuccess).toHaveBeenCalledWith('OpenProject admin options saved') + expect(showSuccess).toHaveBeenCalledWith('Successfully revoked users\' OpenProject OAuth access tokens') + + }) + it.each([ + ['connection_error', 'Failed to perform revoke request due to connection error with the OpenProject server'], + ['other_error', 'Failed to revoke some users\' OpenProject OAuth access tokens'], + ])('should show error message on various failure', async (errorCode, errorMessage) => { + saveAdminConfig.mockImplementationOnce(() => Promise.resolve({ + data: { + oPOAuthTokenRevokeStatus: errorCode, + }, + })) + const wrapper = getWrapper() + const spyCreateNextcloudClient = jest.spyOn(wrapper.vm, 'createNextcloudClient').mockImplementation(() => jest.fn()) + const spyNotifyOpenProjectTokenRevoke = jest.spyOn(wrapper.vm, 'notifyOpenProjectTokenRevoke') + wrapper.find(selectors.opClientIdInput).vm.$emit('input', 'id') + wrapper.find(selectors.opClientSecretInput).vm.$emit('input', 'secret') + wrapper.find(selectors.opSaveButton).vm.$emit('click') + await flushPromises() + + expect(spyCreateNextcloudClient).toHaveBeenCalledTimes(1) + expect(wrapper.vm.openprojectTokenRevokeStatus).toBe(errorCode) + expect(spyNotifyOpenProjectTokenRevoke).toHaveBeenCalledTimes(1) + + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledWith('OpenProject admin options saved') + expect(showError).toHaveBeenCalledWith(errorMessage) + }) + }) +}) + +function getWrapper({ data = {}, props = {} } = {}) { + return shallowMount(FormOAuthSettings, { + localVue, + mocks: { + t: (app, msg) => msg, + }, + propsData: { ...defaultProps, ...props }, + data() { + return data + }, + }) +} 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__/FormOAuthSettings.spec.js.snap b/tests/jest/components/admin/__snapshots__/FormOAuthSettings.spec.js.snap new file mode 100644 index 000000000..d641cb0a8 --- /dev/null +++ b/tests/jest/components/admin/__snapshots__/FormOAuthSettings.spec.js.snap @@ -0,0 +1,382 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Component: FormOAuthSettings form states form state: complete OpenProject and Nextcloud oauth values 1`] = ` +
+
+ + +
+ + + + +
+ + + Replace OpenProject OAuth values + + +
+
+
+ +
+ + +
+ + + + +
+ + + Replace Nextcloud OAuth values + + +
+
+
+
+`; + +exports[`Component: FormOAuthSettings form states form state: complete authorization method and complete Nextcloud form but not OpenProject 1`] = ` +
+
+ + +
+ + + + +
+ + + Save + + +
+
+
+ +
+ + +
+ + + + +
+ + + Replace Nextcloud OAuth values + + +
+
+
+
+`; + +exports[`Component: FormOAuthSettings form states form state: complete authorization method and complete OpenProject form but not Nextcloud 1`] = ` +
+
+ + +
+ + + + +
+ + + Replace OpenProject OAuth values + + +
+
+
+ +
+ + +
+ + + + +
+ + + Create Nextcloud OAuth values + + +
+
+
+
+`; + +exports[`Component: FormOAuthSettings form states form state: complete authorization method but incomplete oauth settings 1`] = ` +
+
+ + +
+ + + + +
+ + + Save + + +
+
+
+ +
+ + + +
+
+`; + +exports[`Component: FormOAuthSettings form states form state: incomplete authorization method 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 + +
+
+
" +`;