diff --git a/src/api/endpoints.js b/src/api/endpoints.js index 295020e81..ad57fc459 100644 --- a/src/api/endpoints.js +++ b/src/api/endpoints.js @@ -9,4 +9,5 @@ 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'), + projectFolderStatus: generateUrl('/apps/integration_openproject/project-folder-status'), } diff --git a/src/api/settings.js b/src/api/settings.js index d8e12372c..9681867a0 100644 --- a/src/api/settings.js +++ b/src/api/settings.js @@ -17,3 +17,7 @@ export function saveAdminConfig(configs) { export function createNextcloudOAuthClient() { return axios.post(endpoints.nextcloudOAuth) } + +export function getProjectFolderStatus() { + return axios.get(endpoints.projectFolderStatus) +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index e85438bf1..db0769ecb 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -35,158 +35,25 @@ :oauth-settings="{ openproject_client_id: state.openproject_client_id, openproject_client_secret: state.openproject_client_secret, - nc_oauth_client: state.nc_oauth_client + nc_oauth_client: state.nc_oauth_client, + }" + @formcomplete="markFormComplete" /> + -
- - - -

- {{ state.project_folder_info.errorMessage }} - {{ projectFolderSetupError }} -

-

- - -

- {{ t('integration_openproject', 'Encryption for the Team Folders App is not enabled.') }} -

-

- -

-
- - {{ t('integration_openproject', 'Automatically managed folders') }} - -
-

- {{ - t('integration_openproject', 'We recommend using this functionality but it is not mandatory. Please activate it in case you want to use the automatic creation and management of project folders.') - }} -

-
- - - {{ - textLabelProjectFolderSetupButton - }} - -
-
-
-

- {{ - t('integration_openproject', 'Let OpenProject create folders per project automatically. It will ensure that every team member has always the correct access permissions.') - }} -

-
- {{ t('integration_openproject', 'OpenProject user, group and folder') }} -

- {{ - t('integration_openproject', 'For automatically managing project folders, this app needs to setup a special team folder, assigned to a group and managed by a user, each called "OpenProject".') - }}
- {{ - t('integration_openproject', 'The app will never delete files or folders, even if you deactivate this later.') - }} -

-
- - - {{ textLabelProjectFolderSetupButton }} - -
-
-
-
-
- {{ t('integration_openproject','Automatically managed folders:') }} {{ opUserAppPassword ? t('integration_openproject', 'Active') : t('integration_openproject', 'Inactive') }} -
-
- - - {{ t('integration_openproject', 'Edit project folders') }} - -
-
-
-
-
- -
- - -
- - - {{ t('integration_openproject', 'Done, complete setup') }} - - - - {{ t('integration_openproject', 'Replace application password') }} - -
-
-
+ @click="resetIntegrationSetup"> @@ -219,93 +86,45 @@ + + diff --git a/src/constants/messages.js b/src/constants/messages.js index c59c8f43a..2c5871b94 100644 --- a/src/constants/messages.js +++ b/src/constants/messages.js @@ -24,6 +24,12 @@ export const messages = { noMachingStausFound: t(APP_ID, 'No matching status found'), noMachingTypeFound: t(APP_ID, 'No matching type found'), noMachingAssigneeFound: t(APP_ID, 'No matching assignee found'), + projectFolderSetup: { + keepCurrentChange: t(APP_ID, 'Keep current setup'), + completeWithoutProjectFolderSetup: t(APP_ID, 'Complete without project folders'), + completeWithProjectFolderSetup: t(APP_ID, 'Setup OpenProject user, group and folder'), + retrySetupWithProjectFolder: t(APP_ID, 'Retry setup OpenProject user, group and folder'), + }, } export const messagesFmt = { diff --git a/src/utils.js b/src/utils.js index f712d91b1..f72390aaa 100644 --- a/src/utils.js +++ b/src/utils.js @@ -132,6 +132,14 @@ export const ADMIN_SETTINGS_FORM = { // order: 4 or 5 depending on auth method projectFolder: { id: 'project-folder', + // default order is 5 as OAuth is the default auth method. + order: 5, + [AUTH_METHOD.OAUTH2]: { + order: 5, + }, + [AUTH_METHOD.OIDC]: { + order: 4, + }, complete: false, }, } diff --git a/tests/jest/components/AdminSettings.spec.js b/tests/jest/components/AdminSettings.spec.js index cf9c2cbaa..90990c212 100644 --- a/tests/jest/components/AdminSettings.spec.js +++ b/tests/jest/components/AdminSettings.spec.js @@ -9,11 +9,8 @@ import axios from '@nextcloud/axios' 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 } from '../../../src/utils.js' -import { appLinks } from '../../../src/constants/links.js' -import { messagesFmt, messages } from '../../../src/constants/messages.js' +import { AUTH_METHOD } from '../../../src/utils.js' jest.mock('@nextcloud/axios', () => { const originalModule = jest.requireActual('@nextcloud/axios') @@ -70,48 +67,10 @@ Object.assign(global.navigator, { }) const selectors = { - opOauthForm: '.openproject-oauth-values', - authorizationMethod: '.authorization-method', - authorizationSettings: '.authorization-settings', - authorizationMethodSaveButton: '[data-test-id="submit-auth-method-values-btn"]', - authorizationSettingsSaveButton: '[data-test-id="submit-oidc-auth-settings-values-btn"]', - providerInput: '#provider-search-input', - oidcDropDownFirstElement: 'ul [title="keycloak"]', - authorizationMethodResetButton: '[data-test-id="reset-authorization-method-btn"]', - authorizationCancelResetButton: '[data-test-id="cancel-edit-auth-method-btn"]', - authorizationSettingsResetButton: '[data-test-id="reset-auth-settings-btn"]', - authorizationSettingsCancelButton: '[data-test-id="cancel-edit-auth-setting-btn"]', - openIdIdentityRadio: '[value="oidc"]', - oauth2Radio: '[value="oauth2"]', - openIdIdentityDisabled: 'input[type="radio"][value="oidc"][disabled="disabled"]', - authSettingTargetAudClient: '#authorization-method-target-client-id input', - ncOauthForm: '.nextcloud-oauth-values', - projectFolderSetupForm: '.project-folder-setup', - resetServerHostButton: '[data-test-id="reset-server-host-btn"]', - textInputWrapper: '.text-input', - resetOPOAuthFormButton: '[data-test-id="reset-op-oauth-btn"]', - resetNcOAuthFormButton: '[data-test-id="reset-nc-oauth-btn"]', - submitOPOAuthFormButton: '[data-test-id="submit-op-oauth-btn"]', - opOauthClientIdInput: '#openproject-oauth-client-id', - opOauthClientSecretInput: '#openproject-oauth-client-secret', - submitNcOAuthFormButton: '[data-test-id="submit-nc-oauth-values-form-btn"]', resetAllAppSettingsButton: '#reset-all-app-settings-btn', defaultUserConfigurationsForm: '.default-prefs', defaultEnableNavigation: '#default-prefs--link', - projectFolderFormHeading: '.project-folder-setup formheading-stub', - projectFolderSetupSwitch: '[type="checkbox"]', - projectFolderSetupButtonStub: 'nccheckboxradioswitch-stub[type="switch"]', - completeProjectFolderSetupWithGroupFolderButton: '[data-test-id="complete-with-project-folders-form-btn"]', - completeWithoutProjectFolderSetupButton: '[data-test-id="complete-without-project-folder-form-btn"]', - editProjectFolderSetup: '[data-test-id="edit-project-folder-setup"]', - projectFolderStatus: '.project-folder-status-value', - projectFolderErrorMessage: '.note-card--title', - projectFolderErrorMessageDetails: '.note-card--error-description', - projectFolderErrorNote: '.project-folder-setup errornote-stub', - userAppPasswordButton: '[data-test-id="reset-user-app-password"]', - setupIntegrationDocumentationLinkSelector: '.settings--documentation-info', adminAuditNoteCardInfoSelector: '[type="info"]', - encryptionNoteCardWarningSelector: '.project-folder-setup ncnotecard-stub', } const completeOAUTH2IntegrationState = { @@ -123,6 +82,15 @@ const completeOAUTH2IntegrationState = { nextcloud_client_id: 'something', nextcloud_client_secret: 'something-else', }, + fresh_project_folder_setup: false, + app_password_set: true, + project_folder_info: { + status: true, + }, + encryption_info: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, } const appState = { @@ -162,940 +130,10 @@ describe('AdminSettings.vue', () => { }, openprojectOauth: { complete: true }, nextcloudOauth: { complete: true }, + projectFolder: { complete: true }, }, } - describe('Project folders form (Project Folder Setup)', () => { - describe('Disable mode', () => { - it('should not show groupfolders error note during initial setup if app is disabled', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: null, - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - authorization_settings: { - oidc_provider: null, - targeted_audience_client_id: null, - sso_provider_type: null, - }, - fresh_project_folder_setup: true, - project_folder_info: { - status: false, - }, - app_password_set: false, - apps: { - ...appState.apps, - groupfolders: { - enabled: false, - supported: false, - minimum_version: '1.0.0', - }, - }, - form: { - serverHost: { complete: false }, - authenticationMethod: { complete: false }, - }, - formMode: { - // Initial setup - project folder form is in DISABLE mode - projectFolderSetUp: F_MODES.DISABLE, - }, - }, - }, - ) - - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(F_MODES.DISABLE) - expect(wrapper.vm.showGroupfoldersAppError).toBe(false) - expect(wrapper.find(selectors.projectFolderErrorNote).exists()).toBe(false) - }) - }) - describe('view mode', () => { - describe('without project folder setup', () => { - it('should show status as "Inactive"', () => { - 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', - }, - fresh_project_folder_setup: false, - // project folder is already set up - project_folder_info: { - status: true, - }, - app_password_set: false, - ...appState, - }, - ...commonState, - }) - const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) - const actualProjectFolderStatusValue = projectFolderStatus.text() - expect(actualProjectFolderStatusValue).toContain('Inactive') - expect(wrapper.find(selectors.projectFolderErrorNote).exists()).toBe(false) - expect(wrapper.find(selectors.projectFolderSetupForm)).toMatchSnapshot() - }) - }) - - describe('disabled groupfolders app', function() { - it('should not show error message if project folder is not setup', async () => { - 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', - }, - fresh_project_folder_setup: true, - app_password_set: false, - apps: { - ...appState.apps, - groupfolders: { - enabled: false, - supported: false, - }, - }, - }, - formMode: { - projectFolderSetUp: F_MODES.VIEW, - }, - ...commonState, - }) - const formHeading = wrapper.find(selectors.projectFolderFormHeading) - const errorNote = wrapper.find(selectors.projectFolderErrorNote) - const editButton = wrapper.find(selectors.editProjectFolderSetup) - expect(formHeading.exists()).toBe(true) - expect(formHeading.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - expect(editButton.attributes().disabled).toBe(undefined) - }) - - it('should show error message if project folder is setup', async () => { - 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', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: true, - encryption_info: { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: false, - }, - apps: { - ...appState.apps, - groupfolders: { - enabled: false, - supported: false, - }, - }, - }, - formMode: { - projectFolderSetUp: F_MODES.VIEW, - }, - }) - const formHeading = wrapper.find(selectors.projectFolderFormHeading) - const errorNote = wrapper.find(selectors.projectFolderErrorNote) - const editButton = wrapper.find(selectors.editProjectFolderSetup) - expect(formHeading.exists()).toBe(true) - expect(formHeading.attributes().haserror).toBe('true') - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.groupfolders.installLink) - expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) - expect(editButton.attributes().disabled).toBe(undefined) - }) - }) - }) - - describe('edit mode ', () => { - describe('fresh setup/after reset', () => { - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - }) - - it('should show the switch as "On" and button text label as "Setup OpenProject user, group and folder"', async () => { - 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', - }, - fresh_project_folder_setup: true, - // project folder is not set up - project_folder_info: { - status: false, - }, - app_password_set: false, - ...appState, - }, - ...commonState, - }) - expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Setup OpenProject user, group and folder') - }) - - it('should show button text label "Complete without team folder setup" when the switch is "off"', async () => { - 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', - }, - fresh_project_folder_setup: true, - // team folder is already not set up - project_folder_info: { - status: false, - }, - app_password_set: false, - ...appState, - }, - ...commonState, - }) - expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) - const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupButtonStub) - projectFolderSetupSwitchButton.vm.$emit('update:checked', false) - await flushPromises() - expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(false) - const completeWithoutProjectFolderSetupButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - expect(completeWithoutProjectFolderSetupButton.text()).toBe('Complete without project folders') - }) - - describe('on trigger "Complete without team folder setup"', () => { - let wrapper = {} - let saveOPOptionsSpy - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ - data: { - oPUserAppPassword: null, - }, - })) - wrapper = getMountedWrapper({ - 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', - default_enable_unified_search: false, - default_enable_navigation: false, - nc_oauth_client: { - 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 - project_folder_info: { - status: false, - }, - app_password_set: false, - }, - ...commonState, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - }) - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(F_MODES.EDIT) - const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupSwitch) - await projectFolderSetupSwitchButton.trigger('click') - await wrapper.vm.$nextTick() - const completeWithoutProjectFolderSetupButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - expect(completeWithoutProjectFolderSetupButton.text()).toBe('Complete without project folders') - await completeWithoutProjectFolderSetupButton.trigger('click') - await wrapper.vm.$nextTick() - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: false, - setup_project_folder: false, - }, - }, - ) - }) - - it('should set status "Inactive"', async () => { - const projectFolderStatusWrapper = wrapper.find(selectors.projectFolderStatus) - const actualProjectFolderStatusValue = projectFolderStatusWrapper.text() - expect(actualProjectFolderStatusValue).toContain('Inactive') - }) - - it('should set form mode to view', async () => { - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(F_MODES.VIEW) - - }) - - it('should not create user app password', async () => { - expect(wrapper.vm.$data.oPUserAppPassword).toBe(null) - }) - }) - - // test for error while setting up the team folder - describe('trigger on "Setup OpenProject user, group and folder" button', () => { - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - }) - - describe('upon failure', () => { - it.each([ - [ - 'should set the user already exists error message and error details when user already exists', - { - error: 'The user "OpenProject" already exists', - expectedErrorDetailsMessage: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', - }, - ], - [ - 'should set the team folder name already exists error message and error details when team folder already exists', - { - error: 'The team folder name "OpenProject" already exists', - expectedErrorDetailsMessage: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', - }, - ], - [ - 'should set the group already exists error message and error details when group already exists', - { - error: 'The group "OpenProject" already exists', - expectedErrorDetailsMessage: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', - }, - ], - - ])('%s', async (name, expectedErrorDetails) => { - 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', - default_enable_unified_search: false, - default_enable_navigation: false, - nc_oauth_client: { - nextcloud_client_id: 'some-nc-client-id-here', - nextcloud_client_secret: 'some-nc-client-secret-here', - }, - fresh_project_folder_setup: true, - project_folder_info: { - status: false, - }, - app_password_set: false, - projectFolderSetupError: null, - ...appState, - }, - ...commonState, - }) - - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - }) - const getgroupfolderStatus = jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.resolve({ - data: { - result: false, - }, - })) - - // creating an error since the put request is not resolved - const err = new Error() - err.response = {} - err.response.data = {} - err.response.data.error = expectedErrorDetails.error - - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.reject(err)) - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - completeProjectFolderSetupWithGroupFolderButton.vm.$emit('click') - await flushPromises() - expect(getgroupfolderStatus).toBeCalledTimes(1) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: true, - setup_project_folder: true, - }, - }, - ) - const projectFolderErrorMessage = wrapper.find(selectors.projectFolderErrorMessage) - const projectFolderErrorMessageDetails = wrapper.find(selectors.projectFolderErrorMessageDetails) - expect(projectFolderErrorMessage.text()).toBe(expectedErrorDetails.error) - expect(projectFolderErrorMessageDetails.text()).toBe(expectedErrorDetails.expectedErrorDetailsMessage) - }) - }) - - describe('upon success', () => { - let wrapper = {} - let getgroupfolderStatusSpy - let saveOPOptionsSpy - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - wrapper = getMountedWrapper({ - 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', - }, - fresh_project_folder_setup: true, - app_password_set: false, - encryption_info: { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: false, - }, - }, - isGroupFolderAlreadySetup: null, - ...commonState, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - }) - - getgroupfolderStatusSpy = jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.resolve({ - data: { - result: false, - }, - })) - saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ - data: { - oPUserAppPassword: 'opUserAppPassword', - }, - })) - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - await completeProjectFolderSetupWithGroupFolderButton.trigger('click') - await wrapper.vm.$nextTick() - expect(getgroupfolderStatusSpy).toBeCalledTimes(1) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: true, - setup_project_folder: true, - }, - }, - ) - await wrapper.vm.$nextTick() - }) - - it('should set status as "Active"', async () => { - expect(wrapper.vm.$data.oPUserAppPassword).toBe('opUserAppPassword') - expect(wrapper.vm.formMode.opUserAppPassword).toBe(F_MODES.EDIT) - await wrapper.vm.$nextTick() - const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) - const actualProjectFolderStatusValue = projectFolderStatus.text() - expect(actualProjectFolderStatusValue).toContain('Active') - }) - - it('should set user app password form to edit mode', async () => { - expect(wrapper.vm.formMode.opUserAppPassword).toBe(F_MODES.EDIT) - }) - - it('should set project folder setup form to edit mode', async () => { - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(F_MODES.VIEW) - }) - it('should create a new app password', async () => { - expect(wrapper.vm.$data.oPUserAppPassword).toBe('opUserAppPassword') - }) - }) - }) - }) - - describe('deactivate', function() { - describe('after complete setup', () => { - let wrapper = {} - let saveOPOptionsSpy - beforeEach(async () => { - wrapper = getMountedWrapper({ - 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', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: true, - encryption_info: { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: false, - }, - }, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - oPUserAppPassword: 'userAppPassword', - }) - saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ - data: { - oPUserAppPassword: null, - }, - })) - const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupSwitch) - await projectFolderSetupSwitchButton.trigger('click') - await wrapper.vm.$nextTick() - const completeWithoutProjectFolderSetupButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - await completeWithoutProjectFolderSetupButton.trigger('click') - await wrapper.vm.$nextTick() - }) - - it('should delete user app password', async () => { - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: false, - setup_project_folder: false, - }, - }, - ) - expect(wrapper.vm.state.app_password_set).toBe(false) - expect(wrapper.vm.state.oPUserAppPassword).not.toBe('userAppPassword') - expect(wrapper.vm.state.oPUserAppPassword).not.toBe(null) - }) - - it('should set project folder status to "Inactive"', async () => { - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: false, - setup_project_folder: false, - }, - }, - ) - const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) - const actualProjectFolderStatusValue = projectFolderStatus.text() - expect(actualProjectFolderStatusValue).toContain('Inactive') - }) - }) - }) - - describe('default deactivate state', function() { - let wrapper = {} - beforeEach(async () => { - wrapper = getMountedWrapper({ - 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', - }, - fresh_project_folder_setup: false, - // team folder is already not set up - project_folder_info: { - status: false, - }, - app_password_set: false, - }, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - }) - }) - - it('should show project folder status as "Inactive"', async () => { - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.VIEW, - }, - }) - const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) - const actualProjectFolderStatusValue = projectFolderStatus.text() - expect(actualProjectFolderStatusValue).toContain('Inactive') - }) - - it('should set the button label to "keep current change"', async () => { - const completeWithoutProjectFolderSetupButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - expect(completeWithoutProjectFolderSetupButton.text()).toBe('Keep current setup') - }) - - it('should show button label to "Setup OpenProject user, group and folder" when switch is "On"', async () => { - const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupSwitch) - await projectFolderSetupSwitchButton.trigger('click') - await wrapper.vm.$nextTick() - expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Setup OpenProject user, group and folder') - }) - }) - - describe('complete setup (project folder and app password)', function() { - describe('edit mode', function() { - let wrapper = {} - let getgroupfolderStatusSpy - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - wrapper = getMountedWrapper({ - 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', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: true, - encryption_info: { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: false, - }, - }, - isGroupFolderAlreadySetup: null, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - oPUserAppPassword: 'opUserPassword', - }) - getgroupfolderStatusSpy = jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.resolve({ - data: { - result: true, - }, - })) - }) - - it('should show button label to "keep current"', async () => { - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Keep current setup') - }) - - it('should not create new user app password on trigger "Keep on current change"', async () => { - const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Keep current setup') - completeProjectFolderSetupWithGroupFolderButton.trigger('click') - expect(getgroupfolderStatusSpy).toBeCalledTimes(1) - expect(wrapper.vm.oPUserAppPassword).toBe('opUserPassword') - }) - - it('should show button label as "Complete without team folder setup" when switch is "off" ', async () => { - const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupSwitch) - await projectFolderSetupSwitchButton.trigger('click') - const completeWithoutProjectFolderSetupButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - expect(completeWithoutProjectFolderSetupButton.text()).toBe('Complete without project folders') - }) - - it('should set switch as "on" again (same as fresh set up) when completely reset', async () => { - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: null, - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - fresh_project_folder_setup: true, - project_folder_info: { - status: true, - }, - app_password_set: false, - }, - }) - expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) - }) - - describe('disabled groupfolders app', function() { - beforeEach(async () => { - 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', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: true, - encryption_info: { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: false, - }, - apps: { - ...appState.apps, - groupfolders: { - enabled: false, - supported: false, - }, - }, - }, - isGroupFolderAlreadySetup: null, - }) - await wrapper.find(selectors.editProjectFolderSetup).vm.$emit('click') - }) - it('should show error message', async () => { - const formHeading = wrapper.find(selectors.projectFolderFormHeading) - const errorNote = wrapper.find(selectors.projectFolderErrorNote) - const saveButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) - expect(formHeading.exists()).toBe(true) - expect(formHeading.attributes().haserror).toBe('true') - expect(errorNote.exists()).toBe(true) - expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) - expect(errorNote.attributes().errorlink).toBe(appLinks.groupfolders.installLink) - expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) - expect(saveButton.attributes().disabled).toBe('true') - expect(saveButton.text()).toBe('Keep current setup') - }) - it('should be able to disable the project folder setup and no error card', async () => { - const toggleButton = wrapper.find(selectors.projectFolderSetupButtonStub) - expect(toggleButton.attributes().checked).toBe('true') - - await toggleButton.vm.$emit('update:checked', false) - - const saveButton = wrapper.find(selectors.completeWithoutProjectFolderSetupButton) - expect(toggleButton.attributes().checked).toBe(undefined) - expect(saveButton.text()).toBe('Complete without project folders') - - const formHeading = wrapper.find(selectors.projectFolderFormHeading) - const errorNote = wrapper.find(selectors.projectFolderErrorNote) - expect(formHeading.attributes().haserror).toBe(undefined) - expect(errorNote.exists()).toBe(false) - }) - }) - }) - }) - }) - }) - - describe('user app password reset', () => { - let confirmSpy - let wrapper - beforeEach(async () => { - axios.put.mockReset() - confirmSpy = jest.spyOn(global.OC.dialogs, 'confirmDestructive') - wrapper = getMountedWrapper({ - 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', - }, - app_password_set: true, - }, - }) - await wrapper.setData({ - oPUserAppPassword: 'oldUserAppPassword', - }) - }) - afterEach(() => { - jest.clearAllMocks() - }) - it('should trigger a confirm dialog', async () => { - const expectedConfirmText = 'If you proceed, your old application password for the OpenProject user will be deleted and you will receive a new OpenProject user password.' - const expectedConfirmOpts = { - cancel: 'Cancel', - confirm: 'Yes, replace', - confirmClasses: 'error', - type: 70, - } - const expectedConfirmTitle = 'Replace user app password' - const resetUserAppPassword = wrapper.find(selectors.userAppPasswordButton) - await resetUserAppPassword.trigger('click') - await wrapper.vm.$nextTick() - expect(confirmSpy).toBeCalledTimes(1) - expect(confirmSpy).toBeCalledWith( - expectedConfirmText, - expectedConfirmTitle, - expectedConfirmOpts, - expect.any(Function), - true, - ) - wrapper.destroy() - }) - - it('should replace old password with new password on confirm', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ - data: { - oPUserAppPassword: 'newUserAppPassword', - }, - })) - await wrapper.vm.createNewAppPassword() - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - setup_app_password: true, - }, - }, - ) - expect(wrapper.vm.oPUserAppPassword).toBe('newUserAppPassword') - expect(wrapper.vm.oPUserAppPassword).not.toBe('oldUserAppPassword') - expect(wrapper.vm.formMode.opUserAppPassword).toBe(F_MODES.EDIT) - expect(wrapper.vm.isFormCompleted.opUserAppPassword).toBe(false) - }) - }) - - describe('error after project folder is already setup', () => { - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - }) - it.each([ - [ - 'should set the user already exists error message and error details when user already exists', - { - error: 'The user "OpenProject" already exists', - expectedErrorDetailsMessage: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', - }, - ], - ])('%s', async (name, expectedErrorDetails) => { - 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', - default_enable_unified_search: false, - default_enable_navigation: false, - nc_oauth_client: { - nextcloud_client_id: 'some-nc-client-id-here', - nextcloud_client_secret: 'some-nc-client-secret-here', - }, - // status is false - // with error message when something went wrong after project folder is already setup - project_folder_info: { - errorMessage: expectedErrorDetails.error, - status: false, - }, - app_password_set: true, - }, - }) - await wrapper.setData({ - formMode: { - projectFolderSetUp: F_MODES.EDIT, - }, - projectFolderSetupError: expectedErrorDetails.error, - }) - expect(wrapper.vm.isFormCompleted.opUserAppPassword).toBe(true) - const projectFolderErrorMessage = wrapper.find(selectors.projectFolderErrorMessage) - const projectFolderErrorMessageDetails = wrapper.find(selectors.projectFolderErrorMessageDetails) - expect(projectFolderErrorMessage.text()).toBe(expectedErrorDetails.error) - expect(projectFolderErrorMessageDetails.text()).toBe(expectedErrorDetails.expectedErrorDetailsMessage) - }) - }) - - describe('Encryption warning after project folder setup', () => { - beforeEach(async () => { - axios.put.mockReset() - axios.get.mockReset() - }) - - it.each([ - [ - 'should show warning when server side encryption is enabled but encryption for groupfolders is not enabled', - { - server_side_encryption_enabled: true, - encryption_enabled_for_groupfolders: false, - }, - true, - ], - [ - 'should not show warning when server side encryption and groupfolders encryption is enabled', - { - server_side_encryption_enabled: true, - encryption_enabled_for_groupfolders: true, - }, - false, - ], - [ - 'should not show warning when server side encryption not enabled but groupfolders encryption is enabled', - { - server_side_encryption_enabled: false, - encryption_enabled_for_groupfolders: true, - }, - false, - ], - ])('%s', (name, encryptionInfoState, expectedResult) => { - 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', - default_enable_unified_search: false, - default_enable_navigation: false, - nc_oauth_client: { - nextcloud_client_id: 'some-nc-client-id-here', - nextcloud_client_secret: 'some-nc-client-secret-here', - }, - project_folder_info: { - status: true, - }, - app_password_set: true, - encryption_info: encryptionInfoState, - }, - }) - const encryptionWarningNoteCard = wrapper.find(selectors.encryptionNoteCardWarningSelector) - expect(encryptionWarningNoteCard.exists()).toBe(expectedResult) - if (expectedResult) { - expect(encryptionWarningNoteCard.attributes().type).toBe('warning') - expect(encryptionWarningNoteCard.find('p.note-card--title').text()).toBe('Encryption for the Team Folders App is not enabled.') - } - }) - }) - describe('reset button', () => { it.each([ { @@ -1186,13 +224,24 @@ describe('AdminSettings.vue', () => { nextcloud_client_id: 'something', nextcloud_client_secret: 'something-else', }, + fresh_project_folder_setup: false, + app_password_set: true, + project_folder_info: { + status: true, + }, + encryption_info: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, }, }) confirmSpy = jest.spyOn(global.OC.dialogs, 'confirmDestructive') }) + afterEach(() => { jest.clearAllMocks() }) + it('should trigger confirm dialog on click', async () => { const resetButton = wrapper.find(selectors.resetAllAppSettingsButton) await resetButton.trigger('click') @@ -1218,48 +267,7 @@ describe('AdminSettings.vue', () => { it('should reset all settings on confirm when project folder is not setup', async () => { const saveOPOptionsSpy = jest.spyOn(axios, 'put') .mockImplementationOnce(() => Promise.resolve({ data: true })) - await wrapper.vm.resetAllAppValues() - - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - openproject_client_id: null, - openproject_client_secret: null, - openproject_instance_url: null, - authorization_method: null, - default_enable_navigation: false, - default_enable_unified_search: false, - setup_project_folder: false, - setup_app_password: false, - oidc_provider: null, - sso_provider_type: null, - targeted_audience_client_id: null, - token_exchange: null, - }, - }, - ) - axios.put.mockReset() - }) - it('should reset all settings on confirm along with app password when app password is set', async () => { - 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', - }, - app_password_set: true, - }, - oPUserAppPassword: 'oPUserAppPassword', - }) - - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: true })) - await wrapper.vm.resetAllAppValues() + await wrapper.vm.confirmResetIntegrationSetup() expect(saveOPOptionsSpy).toBeCalledWith( 'http://localhost/apps/integration_openproject/admin-config', @@ -1271,7 +279,7 @@ describe('AdminSettings.vue', () => { authorization_method: null, default_enable_navigation: false, default_enable_unified_search: false, - setup_project_folder: false, + setup_project_folder: null, setup_app_password: false, oidc_provider: null, sso_provider_type: null, @@ -1280,13 +288,10 @@ describe('AdminSettings.vue', () => { }, }, ) - // no new app password is received on response - expect(wrapper.vm.oPUserAppPassword).toBe(null) - expect(wrapper.vm.oPUserAppPassword).not.toBe('oPUserAppPassword') axios.put.mockReset() }) it('should reload the window at the end', async () => { - await wrapper.vm.resetAllAppValues() + await wrapper.vm.confirmResetIntegrationSetup() await wrapper.vm.$nextTick() expect(window.location.reload).toBeCalledTimes(1) window.location = location @@ -1298,8 +303,9 @@ describe('AdminSettings.vue', () => { it('should be visible when the integration is complete', () => { const wrapper = getMountedWrapper({ state: completeOAUTH2IntegrationState, + ...commonState, }) - expect(wrapper.find(selectors.defaultUserConfigurationsForm)).toMatchSnapshot() + expect(wrapper.find(selectors.defaultUserConfigurationsForm).element).toMatchSnapshot() }) it('should not be visible if the integration is not complete', () => { const wrapper = getMountedWrapper({ @@ -1309,6 +315,15 @@ describe('AdminSettings.vue', () => { openproject_client_id: 'some-client-id-for-op', openproject_client_secret: 'some-client-secret-for-op', nc_oauth_client: null, + fresh_project_folder_setup: false, + app_password_set: true, + project_folder_info: { + status: true, + }, + encryption_info: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, }, }) expect(wrapper.find(selectors.defaultUserConfigurationsForm).exists()).toBeFalsy() @@ -1328,6 +343,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_id: 'something', nextcloud_client_secret: 'something-else', }, + fresh_project_folder_setup: false, + app_password_set: true, + project_folder_info: { + status: true, + }, + encryption_info: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, }, ...commonState, }) @@ -1370,6 +394,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_id: 'something', nextcloud_client_secret: 'something-else', }, + fresh_project_folder_setup: false, + app_password_set: true, + project_folder_info: { + status: true, + }, + encryption_info: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, }, ...commonState, }) diff --git a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap index a74441053..470a20015 100644 --- a/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap +++ b/tests/jest/components/__snapshots__/AdminSettings.spec.js.snap @@ -1,13 +1,69 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -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", -} -`; - exports[`AdminSettings.vue default user configurations form should be visible when the integration is complete 1`] = ` -ErrorWrapper { - "selector": ".default-prefs", -} +
+

+ Default user settings +

+ +

+ + A new user will receive these defaults and they will be applied to the integration app till the user changes them. + +

+ +
+ +
+ + + + +

+ Displays a link to your OpenProject instance in the Nextcloud header. +

+
+ +
+ + + + +

+ Allows you to search OpenProject work packages via the universal search bar in Nextcloud. +

+
+
`; diff --git a/tests/jest/components/admin/FormProjectFolder.spec.js b/tests/jest/components/admin/FormProjectFolder.spec.js new file mode 100644 index 000000000..090d655dc --- /dev/null +++ b/tests/jest/components/admin/FormProjectFolder.spec.js @@ -0,0 +1,1183 @@ +/* jshint esversion: 8 */ + +/** + * SPDX-FileCopyrightText: 2022-2025 Jankari Tech Pvt. Ltd. + * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { showSuccess, showError } from '@nextcloud/dialogs' +import { createLocalVue, shallowMount } from '@vue/test-utils' +import flushPromises from 'flush-promises' // eslint-disable-line n/no-unpublished-import +import { toMatchSerializedSnapshot } from '../../utils.js' +import FormProjectFolder from '../../../../src/components/admin/FormProjectFolder.vue' +import { F_MODES, AUTH_METHOD, ADMIN_SETTINGS_FORM } from '../../../../src/utils.js' +import { appLinks } from '../../../../src/constants/links.js' +import { messagesFmt, messages } from '../../../../src/constants/messages.js' +import { saveAdminConfig, getProjectFolderStatus } from '../../../../src/api/settings.js' + +jest.mock('@nextcloud/dialogs', () => ({ + showError: jest.fn(), + showSuccess: jest.fn(), +})) +jest.mock('../../../../src/api/settings.js', () => ({ + saveAdminConfig: jest.fn(() => ''), + getProjectFolderStatus: jest.fn(), +})) + +global.t = (app, text) => text +global.OC = { + dialogs: { + confirmDestructive: jest.fn(), + YES_NO_BUTTONS: 70, + }, +} +const writeText = jest.fn() +Object.assign(global.navigator, { + clipboard: { + writeText, + }, +}) + +const localVue = createLocalVue() + +const selectors = { + errorNote: 'errornote-stub', + noteCard: 'ncnotecard-stub', + noteCardWarning: 'ncnotecard-stub .note-card--warning-description', + noteCardTitle: 'ncnotecard-stub .note-card--title', + noteCardDescription: 'ncnotecard-stub .note-card--error-description', + projectFolderFormHeading: '.project-folder-setup formheading-stub', + projectFolderFormContainer: '.project-folder-form-container', + projectFolderFormStatus: '.project-folder-status', + projectFolderFormStatusLabel: '.project-folder-status-value', + projectFolderForm: '.project-folder-form', + projectFolderSetupSwitch: '.project-folder-form nccheckboxradioswitch-stub', + projectFolderDescription: '.project-folder-description', + projectFolderDisabledDescription: '.complete-without-groupfolders', + projectFolderActionButton: '.project-folder-form-container ncbutton-stub', + appPasswordFormContainer: '.app-password-form-container', + appPasswordFormHeading: '.app-password-form-container formheading-stub', + appPasswordFormLabel: '.app-password-form-container fieldvalue-stub', + appPasswordInput: '.app-password-form-container textinput-stub', + appPasswordSubmitButton: 'ncbutton-stub[data-test-id="submit-op-system-password-form-btn"]', + appPasswordResetButton: 'ncbutton-stub[data-test-id="reset-user-app-password"]', +} + +const formState = structuredClone(ADMIN_SETTINGS_FORM) +formState.serverHost.complete = true +formState.authenticationMethod.value = AUTH_METHOD.OAUTH2 +formState.authenticationMethod.complete = true +formState.openprojectOauth.complete = true +formState.nextcloudOauth.complete = true + +const defaultProps = { + formState, + formOrder: 5, + projectFolderInfo: { + freshSetup: true, + hasAppPassword: false, + app: { + enabled: true, + supported: true, + minimum_version: '1.0.0', + name: 'Team folders', + }, + folderStatus: { + status: false, + error: null, + }, + encryption: { + server_side_encryption_enabled: false, + encryption_enabled_for_groupfolders: false, + }, + }, +} +describe('Component: FormProjectFolder', () => { + beforeEach(() => { + getProjectFolderStatus.mockImplementation(() => Promise.resolve({ data: { result: false } })) + }) + + afterEach(() => { + jest.clearAllMocks() + saveAdminConfig.mockReset() + getProjectFolderStatus.mockReset() + }) + + describe('Project folder setup', () => { + describe('disable mode', () => { + it('should not show form fields', async () => { + const props = structuredClone(defaultProps) + props.formState.nextcloudOauth.complete = false + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.DISABLE) + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().isdisabled).toBe('true') + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(projectFolderFormHeading.attributes().index).toBe(defaultProps.formOrder.toString()) + expect(wrapper.find(selectors.projectFolderFormContainer).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + describe('view mode', () => { + describe('project folder setup status', () => { + it('should show active if the project folder is enabled', () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: true, + folderStatus: { + status: true, + }, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.VIEW) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().iscomplete).toBe('true') + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(projectFolderFormHeading.attributes().index).toBe(defaultProps.formOrder.toString()) + + expect(wrapper.find(selectors.projectFolderFormContainer).exists()).toBe(true) + const projectFolderFormStatus = wrapper.find(selectors.projectFolderFormStatus) + expect(projectFolderFormStatus.exists()).toBe(true) + expect(projectFolderFormStatus.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Active') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.projectFolderForm).exists()).toBe(false) + + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(true) + const appPasswordFormHeading = wrapper.find(selectors.appPasswordFormHeading) + expect(appPasswordFormHeading.attributes().isdisabled).toBe(undefined) + expect(appPasswordFormHeading.attributes().iscomplete).toBe('true') + expect(appPasswordFormHeading.attributes().index).toBe((defaultProps.formOrder + 1).toString()) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordSubmitButton).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordResetButton).text()).toBe('Replace application password') + + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show inactive if the project folder is disabled', async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: false, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe('true') + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + + expect(wrapper.find(selectors.projectFolderFormContainer).exists()).toBe(true) + const projectFolderFormStatus = wrapper.find(selectors.projectFolderFormStatus) + expect(projectFolderFormStatus.exists()).toBe(true) + expect(projectFolderFormStatus.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Inactive') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.projectFolderForm).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show completed form even if the auth settings are incomplete', () => { + const props = structuredClone(defaultProps) + props.formState.nextcloudOauth.complete = false + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: false, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + + expect(wrapper.find(selectors.projectFolderFormContainer).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderActionButton).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderForm).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + }) + + describe('teamfolders app error', function() { + it('should not show error message if project folder is disabled', async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: false, + app: { + enabled: false, + }, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe('true') + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderActionButton).attributes().disabled).toBe(undefined) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should show error message if project folder is enabled', async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: true, + app: { + enabled: false, + }, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.VIEW) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(projectFolderFormHeading.attributes().iscomplete).toBe('true') + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe('true') + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + const errorNote = wrapper.find(selectors.errorNote) + expect(errorNote.exists()).toBe(true) + expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) + expect(errorNote.attributes().errorlink).toBe(appLinks.groupfolders.installLink) + expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) + expect(wrapper.find(selectors.projectFolderActionButton).attributes().disabled).toBe(undefined) + + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + }) + + describe('edit mode ', () => { + describe('fresh setup', () => { + let wrapper + beforeEach(async () => { + wrapper = getWrapper({ props: defaultProps }) + }) + + it('should show enabled form fields', async () => { + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(wrapper.emitted().formcomplete).toBe(undefined) + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe(undefined) + + const projectFolderForm = wrapper.find(selectors.projectFolderForm) + expect(projectFolderForm.exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe('true') + expect(wrapper.findAll(selectors.projectFolderDescription).length).toBe(2) + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + const projectFolderActionButton = projectFolderForm.find(selectors.projectFolderActionButton) + expect(projectFolderActionButton.text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + expect(wrapper.find(selectors.projectFolderDisabledDescription).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + it('should show disabled form fields if project folder is disabled', async () => { + const projectFolderSetupSwitch = wrapper.find(selectors.projectFolderSetupSwitch) + projectFolderSetupSwitch.vm.$emit('update:checked', false) + await flushPromises() + + expect(projectFolderSetupSwitch.attributes().checked).toBe(undefined) + const projectFolderActionButton = wrapper.find(selectors.projectFolderActionButton) + expect(wrapper.find(selectors.projectFolderDisabledDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(projectFolderActionButton.text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + + describe('on save: disabled project folder', () => { + it('should set status "Inactive"', async () => { + const spySetAppPasswordFormToEditMode = jest.spyOn(wrapper.vm, 'setAppPasswordFormToEditMode') + const projectFolderSetupSwitch = wrapper.find(selectors.projectFolderSetupSwitch) + projectFolderSetupSwitch.vm.$emit('update:checked', false) + await wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: false, + setup_project_folder: false, + }) + expect(spySetAppPasswordFormToEditMode).not.toHaveBeenCalled() + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.loading).toBe(false) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledWith('OpenProject admin options saved') + + expect(wrapper.vm.appPassword).toBe(null) + expect(wrapper.vm.appPasswordCreated).toBe(false) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe('true') + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Inactive') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('enable project folder immediately after complete setup', () => { + it('should show the correct label and form', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + const wrapper = getWrapper({ props }) + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + // edit mode + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderDisabledDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + // enable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + // disable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDisabledDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // enable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: true, + setup_app_password: true, + }) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderForm).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Active') + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + // save app password + const appPasswordSubmitButton = wrapper.find(selectors.appPasswordSubmitButton) + expect(appPasswordSubmitButton.text()).toBe('Done, complete setup') + appPasswordSubmitButton.vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + }) + }) + }) + + describe('on save: enabled project folder', () => { + describe('upon success', () => { + beforeEach(() => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + wrapper = getWrapper({ props }) + }) + + it('should set status as "Active"', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + const spySetAppPasswordFormToEditMode = jest.spyOn(wrapper.vm, 'setAppPasswordFormToEditMode') + expect(wrapper.vm.appPassword).toBe(null) + expect(wrapper.vm.appPasswordCreated).toBe(false) + await wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: true, + setup_project_folder: true, + }) + expect(spySetAppPasswordFormToEditMode).toHaveBeenCalledTimes(1) + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.loading).toBe(false) + expect(wrapper.vm.appPassword).toBe(appPassword) + expect(wrapper.vm.appPasswordCreated).toBe(true) + expect(wrapper.emitted().formcomplete.length).toBe(1) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + expect(showSuccess).toHaveBeenCalledTimes(1) + expect(showSuccess).toHaveBeenCalledWith('OpenProject admin options saved') + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Active') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(false) + const appPasswordFormHeading = wrapper.find(selectors.appPasswordFormHeading) + expect(appPasswordFormHeading.attributes().isdisabled).toBe(undefined) + expect(appPasswordFormHeading.attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.appPasswordInput).attributes().value).toBe(appPassword) + expect(wrapper.find(selectors.appPasswordResetButton).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordSubmitButton).text()).toBe('Done, complete setup') + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should set app password form to view mode on "Done" action', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + expect(wrapper.vm.appPassword).toBe(null) + await wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.EDIT) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordInput).attributes().value).toBe(appPassword) + expect(wrapper.find(selectors.appPasswordResetButton).exists()).toBe(false) + const appPasswordSubmitButton = wrapper.find(selectors.appPasswordSubmitButton) + expect(appPasswordSubmitButton.text()).toBe('Done, complete setup') + await appPasswordSubmitButton.vm.$emit('click') + + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + expect(appPasswordSubmitButton.text()).toBe('Replace application password') + toMatchSerializedSnapshot(wrapper.html()) + }) + + describe('disable project folder immediately after complete setup', () => { + it('should show the correct label and form', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + const wrapper = getWrapper({ props }) + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + wrapper.find(selectors.appPasswordSubmitButton).vm.$emit('click') + await flushPromises() + // edit mode + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // disable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDisabledDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + // enable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + + // disable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + expect(wrapper.find(selectors.projectFolderDisabledDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: false, + setup_app_password: false, + }) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderForm).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Inactive') + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().issetupcompletewithoutprojectfolders).toBe('true') + }) + it('enable-disable-enable project folder flow should work', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + const wrapper = getWrapper({ props }) + + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + wrapper.find(selectors.appPasswordSubmitButton).vm.$emit('click') + await flushPromises() + + // edit + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // disable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: false, + setup_app_password: false, + }) + saveAdminConfig.mockReset() + // mock again after reset + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + getProjectFolderStatus.mockImplementation(() => Promise.resolve({ + data: { + result: true, + }, + })) + + // edit + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // re-enable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: false, + setup_app_password: true, + }) + + // save app password + const appPasswordSubmitButton = wrapper.find(selectors.appPasswordSubmitButton) + expect(appPasswordSubmitButton.text()).toBe('Done, complete setup') + appPasswordSubmitButton.vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + }) + it('complete-disable-enable project folder flow should work', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + getProjectFolderStatus.mockImplementation(() => Promise.resolve({ + data: { + result: true, + }, + })) + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo.freshSetup = false + props.projectFolderInfo.hasAppPassword = true + props.projectFolderInfo.folderStatus.status = true + const wrapper = getWrapper({ props }) + + // edit + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // disable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: false, + setup_app_password: false, + }) + saveAdminConfig.mockReset() + // mock again after reset + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + + // edit + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + // re-enable project folder + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + // save + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_project_folder: false, + setup_app_password: true, + }) + + // save app password + const appPasswordSubmitButton = wrapper.find(selectors.appPasswordSubmitButton) + expect(appPasswordSubmitButton.text()).toBe('Done, complete setup') + appPasswordSubmitButton.vm.$emit('click') + await flushPromises() + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + }) + }) + }) + + describe('upon failure', () => { + it.each([ + [ + 'should set the user already exists error message and error details when user already exists', + { + error: 'The user "OpenProject" already exists', + errorDescription: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', + }, + ], + [ + 'should set the team folder name already exists error message and error details when team folder already exists', + { + error: 'The team folder name "OpenProject" already exists', + errorDescription: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', + }, + ], + [ + 'should set the group already exists error message and error details when group already exists', + { + error: 'The group "OpenProject" already exists', + errorDescription: 'Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.', + }, + ], + ])('%s', async (name, expected) => { + const props = structuredClone(defaultProps) + const wrapper = getWrapper({ props }) + + const errResponse = new Error('Request failed') + errResponse.response = {} + errResponse.response.data = {} + errResponse.response.data.error = expected.error + saveAdminConfig.mockImplementation(() => Promise.reject(errResponse)) + + const spySetAppPasswordFormToEditMode = jest.spyOn(wrapper.vm, 'setAppPasswordFormToEditMode') + const spySetProjectFolderFormToViewMode = jest.spyOn(wrapper.vm, 'setProjectFolderFormToViewMode') + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: true, + setup_project_folder: true, + }) + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + expect(spySetAppPasswordFormToEditMode).toHaveBeenCalledTimes(0) + expect(spySetProjectFolderFormToViewMode).toHaveBeenCalledTimes(0) + expect(wrapper.emitted().formcomplete).toBe(undefined) + expect(showSuccess).toHaveBeenCalledTimes(0) + + expect(wrapper.vm.projectFolderSetupError).toBe(expected.error) + expect(showError).toHaveBeenCalledTimes(1) + expect(showError).toHaveBeenCalledWith(`Failed to save OpenProject admin options: ${expected.error}`) + + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().haserror).toBe('true') + expect(wrapper.find(selectors.noteCardTitle).text()).toBe(expected.error) + expect(wrapper.find(selectors.noteCardDescription).text()).toBe(expected.errorDescription) + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe(undefined) + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe('true') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.retrySetupWithProjectFolder) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + toMatchSerializedSnapshot(wrapper.html()) + }) + }) + }) + + describe('teamfolders app error', function() { + let wrapper + beforeEach(async () => { + const props = structuredClone(defaultProps) + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + app: { + enabled: false, + }, + } + wrapper = getWrapper({ props }) + }) + + it('should show error message if project folder is enabled', async () => { + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.DISABLE) + + const projectFolderFormHeading = wrapper.find(selectors.projectFolderFormHeading) + expect(projectFolderFormHeading.attributes().issetupcompletewithoutprojectfolders).toBe(undefined) + expect(projectFolderFormHeading.attributes().iscomplete).toBe(undefined) + expect(projectFolderFormHeading.attributes().isdisabled).toBe(undefined) + expect(projectFolderFormHeading.attributes().haserror).toBe('true') + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + const errorNote = wrapper.find(selectors.errorNote) + expect(errorNote.exists()).toBe(true) + expect(errorNote.attributes().errortitle).toBe(messagesFmt.appNotEnabledOrUnsupported()) + expect(errorNote.attributes().errorlink).toBe(appLinks.groupfolders.installLink) + expect(errorNote.attributes().errorlinklabel).toBe(messages.installLatestVersionNow) + expect(wrapper.find(selectors.projectFolderActionButton).attributes().disabled).toBe('true') + + toMatchSerializedSnapshot(wrapper.html()) + }) + it('should not show error message if project folder is disabled', async () => { + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderDisabledDescription).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderActionButton).attributes().disabled).toBe(undefined) + + expect(wrapper.find(selectors.noteCard).exists()).toBe(false) + expect(wrapper.find(selectors.errorNote).exists()).toBe(false) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + toMatchSerializedSnapshot(wrapper.html()) + }) + + }) + }) + + describe('disable project folder after complete setup', function() { + let wrapper + beforeEach(async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo.freshSetup = false + props.projectFolderInfo.hasAppPassword = true + props.projectFolderInfo.folderStatus.status = true + wrapper = getWrapper({ props }) + }) + + it('should not call server on keep current', async () => { + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormHeading).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.VIEW) + + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe('true') + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + expect(wrapper.find(selectors.appPasswordFormHeading).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe(undefined) + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe('true') + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + expect(wrapper.find(selectors.appPasswordFormHeading).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.VIEW) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormLabel).exists()).toBe(true) + expect(saveAdminConfig).toHaveBeenCalledTimes(0) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Active') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + }) + it('should hide app password form', async () => { + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderFormHeading).attributes().iscomplete).toBe('true') + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe(undefined) + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithoutProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + it('should set project folder status to "Inactive" on save', async () => { + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: false, + setup_project_folder: false, + }) + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.emitted().formcomplete.length).toBe(2) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + expect(showSuccess).toHaveBeenCalledTimes(1) + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Inactive') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + }) + + describe('enable project folder after complete setup', function() { + let wrapper + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + + beforeEach(async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo.freshSetup = false + props.projectFolderInfo.hasAppPassword = false + props.projectFolderInfo.folderStatus.status = false + wrapper = getWrapper({ props }) + }) + + it('should not call server on keep current', async () => { + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + expect(wrapper.vm.folderFormMode).toBe(F_MODES.EDIT) + + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe(undefined) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('Let OpenProject create folders per project automatically') + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe('true') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.completeWithProjectFolderSetup) + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', false) + await flushPromises() + + expect(wrapper.find(selectors.projectFolderSetupSwitch).attributes().checked).toBe(undefined) + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(false) + expect(wrapper.find(selectors.projectFolderDescription).text()).toContain('We recommend using this functionality but it is not mandatory') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe(messages.projectFolderSetup.keepCurrentChange) + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(0) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Inactive') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.appPasswordFormContainer).exists()).toBe(false) + }) + it('should show project folder status as "Inactive"', async () => { + const appPassword = '12345678' + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo.freshSetup = false + props.projectFolderInfo.hasAppPassword = false + props.projectFolderInfo.folderStatus.status = false + const wrapper = getWrapper({ props }) + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + wrapper.find(selectors.projectFolderSetupSwitch).vm.$emit('update:checked', true) + await flushPromises() + + wrapper.find(selectors.projectFolderActionButton).vm.$emit('click') + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: true, + setup_project_folder: true, + }) + expect(wrapper.vm.folderFormMode).toBe(F_MODES.VIEW) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.EDIT) + expect(wrapper.emitted().formcomplete.length).toBe(2) + expect(wrapper.emitted().formcomplete[0][0]).toBeInstanceOf(Function) + expect(wrapper.vm.appPassword).toBe(appPassword) + expect(showSuccess).toHaveBeenCalledTimes(1) + + expect(wrapper.find(selectors.projectFolderFormStatus).exists()).toBe(true) + expect(wrapper.find(selectors.projectFolderFormStatusLabel).text()).toContain(': Active') + expect(wrapper.find(selectors.projectFolderActionButton).text()).toBe('Edit project folders') + expect(wrapper.find(selectors.appPasswordInput).exists()).toBe(true) + expect(wrapper.find(selectors.appPasswordSubmitButton).exists()).toBe(true) + }) + }) + + describe('reset app password', () => { + let wrapper + beforeEach(async () => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: true, + folderStatus: { + status: true, + }, + } + wrapper = getWrapper({ props }) + }) + + it('should trigger a confirm dialog', async () => { + const spyConfirmDialog = jest.spyOn(global.OC.dialogs, 'confirmDestructive') + + const expectedConfirmText = 'If you proceed, your old application password for the OpenProject user will be deleted and you will receive a new OpenProject user password.' + const expectedConfirmOpts = { + cancel: 'Cancel', + confirm: 'Yes, replace', + confirmClasses: 'error', + type: 70, + } + const expectedConfirmTitle = 'Replace user app password' + + wrapper.find(selectors.appPasswordResetButton).vm.$emit('click') + await flushPromises() + + expect(spyConfirmDialog).toBeCalledTimes(1) + expect(spyConfirmDialog).toBeCalledWith( + expectedConfirmText, + expectedConfirmTitle, + expectedConfirmOpts, + expect.any(Function), + true, + ) + }) + it('should replace old password with new one on confirm', async () => { + const appPassword = '12345678' + const spySetAppPasswordFormToEditMode = jest.spyOn(wrapper.vm, 'setAppPasswordFormToEditMode') + saveAdminConfig.mockImplementation(() => Promise.resolve({ + data: { + oPUserAppPassword: appPassword, + }, + })) + getProjectFolderStatus.mockImplementation(() => Promise.resolve({ + data: { + result: true, + }, + })) + await wrapper.vm.recreateAppPassword() + await flushPromises() + + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: true, + }) + expect(wrapper.vm.passwordFormMode).toBe(F_MODES.EDIT) + expect(saveAdminConfig).toHaveBeenCalledTimes(1) + expect(saveAdminConfig).toHaveBeenCalledWith({ + setup_app_password: true, + }) + expect(spySetAppPasswordFormToEditMode).toHaveBeenCalledTimes(1) + expect(wrapper.vm.appPassword).toBe('12345678') + }) + }) + }) + }) + + describe('Encryption warning after project folder setup', () => { + it.each([ + [ + 'should show warning when server side encryption is enabled but encryption for groupfolders is not enabled', + { + serverSideEnabled: true, + teamFoldersEnabled: false, + }, + true, + ], + [ + 'should not show warning when server side encryption and groupfolders encryption is enabled', + { + serverSideEnabled: true, + teamFoldersEnabled: true, + }, + false, + ], + [ + 'should not show warning when server side encryption not enabled but groupfolders encryption is enabled', + { + serverSideEnabled: false, + teamFoldersEnabled: true, + }, + false, + ], + ])('%s', (name, encryption, showWarning) => { + const props = structuredClone(defaultProps) + props.formState.projectFolder.complete = true + props.projectFolderInfo = { + ...defaultProps.projectFolderInfo, + freshSetup: false, + hasAppPassword: true, + folderStatus: { + status: true, + }, + encryption: { + server_side_encryption_enabled: encryption.serverSideEnabled, + encryption_enabled_for_groupfolders: encryption.teamFoldersEnabled, + }, + } + const wrapper = getWrapper({ props }) + + expect(wrapper.find(selectors.noteCardWarning).exists()).toBe(showWarning) + if (showWarning) { + expect(wrapper.find(selectors.noteCard).attributes().type).toBe('warning') + expect(wrapper.find(selectors.noteCardTitle).text()).toBe('Encryption for the Team Folders App is not enabled.') + } + }) + }) +}) + +function getWrapper({ data = {}, props = {} } = {}) { + return shallowMount(FormProjectFolder, { + localVue, + mocks: { + t: (app, msg) => msg, + }, + propsData: { ...defaultProps, ...props }, + data() { + return data + }, + }) +} diff --git a/tests/jest/components/admin/FormSSOSettings.spec.js b/tests/jest/components/admin/FormSSOSettings.spec.js index fd4c090a4..e04047085 100644 --- a/tests/jest/components/admin/FormSSOSettings.spec.js +++ b/tests/jest/components/admin/FormSSOSettings.spec.js @@ -8,6 +8,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils' import { showError, showSuccess } from '@nextcloud/dialogs' +import { toMatchSerializedSnapshot } from '../../utils.js' 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' @@ -1590,8 +1591,3 @@ function getWrapper({ data = {}, props = {} } = {}) { }, }) } - -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__/FormProjectFolder.spec.js.snap b/tests/jest/components/admin/__snapshots__/FormProjectFolder.spec.js.snap new file mode 100644 index 000000000..1accdf95c --- /dev/null +++ b/tests/jest/components/admin/__snapshots__/FormProjectFolder.spec.js.snap @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Component: FormProjectFolder Project folder setup disable mode should not show form fields 1`] = ` +"
+
+ + + +
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: disabled project folder should set status "Inactive" 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Inactive +
+
+ + Edit project folders + +
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: enabled project folder upon failure should set the group already exists error message and error details when group already exists 1`] = ` +"
+
+ + +

The group "OpenProject" already exists

+

Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.

+
+
+
+ Automatically managed folders +
+

+ Let OpenProject create folders per project automatically. It will ensure that every team member has always the correct access permissions. +


OpenProject user, group and folder +

+ For automatically managing project folders, this app needs to setup a special team folder, assigned to a group and managed by a user, each called "OpenProject".
+ The app will never delete files or folders, even if you deactivate this later. +

+
+ + Retry setup OpenProject user, group and folder + +
+
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: enabled project folder upon failure should set the team folder name already exists error message and error details when team folder already exists 1`] = ` +"
+
+ + +

The team folder name "OpenProject" already exists

+

Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.

+
+
+
+ Automatically managed folders +
+

+ Let OpenProject create folders per project automatically. It will ensure that every team member has always the correct access permissions. +


OpenProject user, group and folder +

+ For automatically managing project folders, this app needs to setup a special team folder, assigned to a group and managed by a user, each called "OpenProject".
+ The app will never delete files or folders, even if you deactivate this later. +

+
+ + Retry setup OpenProject user, group and folder + +
+
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: enabled project folder upon failure should set the user already exists error message and error details when user already exists 1`] = ` +"
+
+ + +

The user "OpenProject" already exists

+

Setting up the OpenProject user, group and team folder was not possible. Please check this {htmlLink} on how to resolve this situation.

+
+
+
+ Automatically managed folders +
+

+ Let OpenProject create folders per project automatically. It will ensure that every team member has always the correct access permissions. +


OpenProject user, group and folder +

+ For automatically managing project folders, this app needs to setup a special team folder, assigned to a group and managed by a user, each called "OpenProject".
+ The app will never delete files or folders, even if you deactivate this later. +

+
+ + Retry setup OpenProject user, group and folder + +
+
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: enabled project folder upon success should set app password form to view mode on "Done" action 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Active +
+
+ + Edit project folders + +
+
+
+
+
+ + +
+ + Replace application password + +
+
+
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup on save: enabled project folder upon success should set status as "Active" 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Active +
+
+ + Edit project folders + +
+
+
+
+
+ + +
+ + Done, complete setup + +
+
+
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup teamfolders app error should not show error message if project folder is disabled 1`] = ` +"
+
+ + +
+
+ Automatically managed folders +
+

+ We recommend using this functionality but it is not mandatory. Please activate it in case you want to use the automatic creation and management of project folders. +

+
+ + Complete without project folders + +
+
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup edit mode fresh setup teamfolders app error should show error message if project folder is enabled 1`] = ` +"
+
+ + +
+
+ Automatically managed folders +
+

+ Let OpenProject create folders per project automatically. It will ensure that every team member has always the correct access permissions. +


OpenProject user, group and folder +

+ For automatically managing project folders, this app needs to setup a special team folder, assigned to a group and managed by a user, each called "OpenProject".
+ The app will never delete files or folders, even if you deactivate this later. +

+
+ + Setup OpenProject user, group and folder + +
+
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup view mode project folder setup status should show active if the project folder is enabled 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Active +
+
+ + Edit project folders + +
+
+
+
+
+ + +
+ + Replace application password + +
+
+
" +`; + +exports[`Component: FormProjectFolder Project folder setup view mode project folder setup status should show inactive if the project folder is disabled 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Inactive +
+
+ + Edit project folders + +
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup view mode teamfolders app error should not show error message if project folder is disabled 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Inactive +
+
+ + Edit project folders + +
+
+
+
+ +
" +`; + +exports[`Component: FormProjectFolder Project folder setup view mode teamfolders app error should show error message if project folder is enabled 1`] = ` +"
+
+ + +
+
+
Automatically managed folders: Active +
+
+ + Edit project folders + +
+
+
+
+
+ + +
+ + Replace application password + +
+
+
" +`; diff --git a/tests/jest/utils.js b/tests/jest/utils.js new file mode 100644 index 000000000..03e936580 --- /dev/null +++ b/tests/jest/utils.js @@ -0,0 +1,9 @@ +/** + * SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export function toMatchSerializedSnapshot(element) { + element = element.replace(/ id="[^"]+"/g, ' id="__ID__"').replace(/ uid="[^"]+"/g, ' uid="__UID__"') + expect(element).toMatchSnapshot() +}