diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2021-06-24 14:15:06 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-06-28 20:03:21 +0000 |
commit | f3180f874f095ffa1d26f876a6fd4acfb3cdf6c4 (patch) | |
tree | 4ed2db36d87cb988067f19a2af84d1fbf9db2648 /server/sonar-web/src/main/js/apps | |
parent | afac7cec97a340daf10f8b61e132bc29ae4999f2 (diff) | |
download | sonarqube-f3180f874f095ffa1d26f876a6fd4acfb3cdf6c4.tar.gz sonarqube-f3180f874f095ffa1d26f876a6fd4acfb3cdf6c4.zip |
SONAR-14932 Trigger alm settings validation before closing the modal
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
8 files changed, 329 insertions, 51 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index b4c8c35affb..43a9a8fb69c 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx @@ -257,6 +257,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { alreadyHaveInstanceConfigured={false} onCancel={this.handleOnCancelCreation} afterSubmit={this.handleAfterSubmit} + enforceValidation={true} /> )} </div> diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx index da7ede592d4..2845ff4d050 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx @@ -19,9 +19,12 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { getAlmSettings } from '../../../../api/alm-settings'; import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks'; import { AlmKeys } from '../../../../types/alm-settings'; +import AlmBindingDefinitionForm from '../../../settings/components/almIntegration/AlmBindingDefinitionForm'; +import CreateProjectModeSelection from '../CreateProjectModeSelection'; import { CreateProjectPage } from '../CreateProjectPage'; import { CreateProjectModes } from '../types'; @@ -36,42 +39,65 @@ it('should render correctly', () => { expect(getAlmSettings).toBeCalled(); }); -it('should render correctly if the manual method is selected', () => { +it.each([ + [CreateProjectModes.Manual], + [CreateProjectModes.AzureDevOps], + [CreateProjectModes.BitbucketServer], + [CreateProjectModes.BitbucketCloud], + [CreateProjectModes.GitHub], + [CreateProjectModes.GitLab] +])('should render correctly for %s mode', (mode: CreateProjectModes) => { expect( shallowRender({ - location: mockLocation({ query: { mode: CreateProjectModes.Manual } }) + location: mockLocation({ query: { mode } }) }) ).toMatchSnapshot(); }); -it('should render correctly if the Azure method is selected', () => { - expect( - shallowRender({ - location: mockLocation({ query: { mode: CreateProjectModes.AzureDevOps } }) - }) - ).toMatchSnapshot(); -}); +it('should render alm configuration creation correctly', () => { + const wrapper = shallowRender(); -it('should render correctly if the BBS method is selected', () => { - expect( - shallowRender({ - location: mockLocation({ query: { mode: CreateProjectModes.BitbucketServer } }) - }) - ).toMatchSnapshot(); + wrapper + .find(CreateProjectModeSelection) + .props() + .onConfigMode(AlmKeys.Azure); + expect(wrapper).toMatchSnapshot(); }); -it('should render correctly if the GitHub method is selected', () => { - const wrapper = shallowRender({ - location: mockLocation({ query: { mode: CreateProjectModes.GitHub } }) - }); - expect(wrapper).toMatchSnapshot(); +it('should cancel alm configuration creation properly', () => { + const wrapper = shallowRender(); + + wrapper + .find(CreateProjectModeSelection) + .props() + .onConfigMode(AlmKeys.Azure); + expect(wrapper.state().creatingAlmDefinition).toBe(AlmKeys.Azure); + + wrapper + .find(AlmBindingDefinitionForm) + .props() + .onCancel(); + expect(wrapper.state().creatingAlmDefinition).toBeUndefined(); }); -it('should render correctly if the GitLab method is selected', () => { - const wrapper = shallowRender({ - location: mockLocation({ query: { mode: CreateProjectModes.GitLab } }) - }); - expect(wrapper).toMatchSnapshot(); +it('should submit alm configuration creation properly', async () => { + const push = jest.fn(); + const wrapper = shallowRender({ router: mockRouter({ push }) }); + + wrapper + .find(CreateProjectModeSelection) + .props() + .onConfigMode(AlmKeys.Azure); + expect(wrapper.state().creatingAlmDefinition).toBe(AlmKeys.Azure); + + wrapper + .find(AlmBindingDefinitionForm) + .props() + .afterSubmit({ key: 'test-key' }); + await waitAndUpdate(wrapper); + expect(wrapper.state().creatingAlmDefinition).toBeUndefined(); + expect(getAlmSettings).toHaveBeenCalled(); + expect(push).toHaveBeenCalledWith({ pathname: '/path', query: { mode: AlmKeys.Azure } }); }); it('should submit alm configuration creation properly for BBC', async () => { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap index 58fa5abb10e..e23064c7a37 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should render alm configuration creation correctly 1`] = ` +<Fragment> + <Helmet + defer={true} + encodeSpecialCharacters={true} + title="onboarding.create_project.select_method" + titleTemplate="%s" + /> + <A11ySkipTarget + anchor="create_project_main" + /> + <div + className="page page-limited huge-spacer-bottom position-relative" + id="create-project" + > + <Connect(withAppState(CreateProjectModeSelection)) + almCounts={ + Object { + "azure": 0, + "bitbucket": 0, + "bitbucketcloud": 0, + "github": 0, + "gitlab": 0, + } + } + loadingBindings={true} + onConfigMode={[Function]} + onSelectMode={[Function]} + /> + <AlmBindingDefinitionForm + afterSubmit={[Function]} + alm="azure" + alreadyHaveInstanceConfigured={false} + enforceValidation={true} + onCancel={[Function]} + /> + </div> +</Fragment> +`; + exports[`should render correctly 1`] = ` <Fragment> <Helmet @@ -33,7 +73,7 @@ exports[`should render correctly 1`] = ` </Fragment> `; -exports[`should render correctly if the Azure method is selected 1`] = ` +exports[`should render correctly for azure mode 1`] = ` <Fragment> <Helmet defer={true} @@ -84,7 +124,7 @@ exports[`should render correctly if the Azure method is selected 1`] = ` </Fragment> `; -exports[`should render correctly if the BBS method is selected 1`] = ` +exports[`should render correctly for bitbucket mode 1`] = ` <Fragment> <Helmet defer={true} @@ -135,7 +175,58 @@ exports[`should render correctly if the BBS method is selected 1`] = ` </Fragment> `; -exports[`should render correctly if the GitHub method is selected 1`] = ` +exports[`should render correctly for bitbucketcloud mode 1`] = ` +<Fragment> + <Helmet + defer={true} + encodeSpecialCharacters={true} + title="onboarding.create_project.select_method" + titleTemplate="%s" + /> + <A11ySkipTarget + anchor="create_project_main" + /> + <div + className="page page-limited huge-spacer-bottom position-relative" + id="create-project" + > + <BitbucketCloudProjectCreate + canAdmin={false} + loadingBindings={true} + location={ + Object { + "action": "PUSH", + "hash": "", + "key": "key", + "pathname": "/path", + "query": Object { + "mode": "bitbucketcloud", + }, + "search": "", + "state": Object {}, + } + } + onProjectCreate={[Function]} + router={ + Object { + "createHref": [MockFunction], + "createPath": [MockFunction], + "go": [MockFunction], + "goBack": [MockFunction], + "goForward": [MockFunction], + "isActive": [MockFunction], + "push": [MockFunction], + "replace": [MockFunction], + "setRouteLeaveHook": [MockFunction], + } + } + settings={Array []} + /> + </div> +</Fragment> +`; + +exports[`should render correctly for github mode 1`] = ` <Fragment> <Helmet defer={true} @@ -186,7 +277,7 @@ exports[`should render correctly if the GitHub method is selected 1`] = ` </Fragment> `; -exports[`should render correctly if the GitLab method is selected 1`] = ` +exports[`should render correctly for gitlab mode 1`] = ` <Fragment> <Helmet defer={true} @@ -237,7 +328,7 @@ exports[`should render correctly if the GitLab method is selected 1`] = ` </Fragment> `; -exports[`should render correctly if the manual method is selected 1`] = ` +exports[`should render correctly for manual mode 1`] = ` <Fragment> <Helmet defer={true} diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx index 739089ab2fd..090b22c04dd 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx @@ -24,11 +24,13 @@ import { createBitbucketServerConfiguration, createGithubConfiguration, createGitlabConfiguration, + deleteConfiguration, updateAzureConfiguration, updateBitbucketCloudConfiguration, updateBitbucketServerConfiguration, updateGithubConfiguration, - updateGitlabConfiguration + updateGitlabConfiguration, + validateAlmSettings } from '../../../../api/alm-settings'; import { AlmBindingDefinition, @@ -47,8 +49,9 @@ interface Props { alm: AlmKeys; bindingDefinition?: AlmBindingDefinition; alreadyHaveInstanceConfigured: boolean; - onCancel?: () => void; - afterSubmit?: (data: AlmBindingDefinitionBase) => void; + onCancel: () => void; + afterSubmit: (data: AlmBindingDefinitionBase) => void; + enforceValidation?: boolean; } interface State { @@ -56,6 +59,8 @@ interface State { touched: boolean; submitting: boolean; bitbucketVariant?: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud; + alreadySavedFormData?: AlmBindingDefinition; + validationError?: string; } const BINDING_PER_ALM = { @@ -144,33 +149,71 @@ export default class AlmBindingDefinitionForm extends React.PureComponent<Props, }; handleFormSubmit = async () => { - const { alm } = this.props; - const { formData, bitbucketVariant } = this.state; + const { alm, enforceValidation } = this.props; + const { formData, bitbucketVariant, alreadySavedFormData, validationError } = this.state; const apiAlm = bitbucketVariant ?? alm; - const apiMethod = this.props.bindingDefinition?.key - ? BINDING_PER_ALM[apiAlm].updateApi({ - newKey: formData.key, - ...formData, - key: this.props.bindingDefinition.key - } as any) - : BINDING_PER_ALM[apiAlm].createApi({ ...formData } as any); + let apiMethod; + + if (alreadySavedFormData && validationError) { + apiMethod = BINDING_PER_ALM[apiAlm].updateApi({ + newKey: formData.key, + ...formData, + key: alreadySavedFormData.key + } as any); + } else if (this.props.bindingDefinition?.key) { + apiMethod = BINDING_PER_ALM[apiAlm].updateApi({ + newKey: formData.key, + ...formData, + key: this.props.bindingDefinition.key + } as any); + } else { + apiMethod = BINDING_PER_ALM[apiAlm].createApi({ ...formData } as any); + } this.setState({ submitting: true }); try { await apiMethod; - if (this.props.afterSubmit) { + if (!this.mounted) { + return; + } + + this.setState({ alreadySavedFormData: formData }); + + let error: string | undefined; + + if (enforceValidation) { + error = await validateAlmSettings(formData.key); + } + + if (!this.mounted) { + return; + } + + if (error) { + this.setState({ validationError: error }); + } else { this.props.afterSubmit(formData); } } finally { if (this.mounted) { - this.setState({ submitting: false }); + this.setState({ submitting: false, touched: false }); } } }; + handleOnCancel = async () => { + const { alreadySavedFormData } = this.state; + + if (alreadySavedFormData) { + await deleteConfiguration(alreadySavedFormData.key); + } + + this.props.onCancel(); + }; + handleBitbucketVariantChange = ( bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud ) => { @@ -188,7 +231,7 @@ export default class AlmBindingDefinitionForm extends React.PureComponent<Props, render() { const { alm, bindingDefinition, alreadyHaveInstanceConfigured } = this.props; - const { formData, submitting, bitbucketVariant } = this.state; + const { formData, submitting, bitbucketVariant, validationError } = this.state; const isUpdate = !!bindingDefinition; @@ -198,13 +241,14 @@ export default class AlmBindingDefinitionForm extends React.PureComponent<Props, isUpdate={isUpdate} canSubmit={this.canSubmit()} alreadyHaveInstanceConfigured={alreadyHaveInstanceConfigured} - onCancel={() => this.props.onCancel && this.props.onCancel()} + onCancel={this.handleOnCancel} onSubmit={this.handleFormSubmit} onFieldChange={this.handleFieldChange} formData={formData} submitting={submitting} bitbucketVariant={bitbucketVariant} onBitbucketVariantChange={this.handleBitbucketVariantChange} + validationError={validationError} /> ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx index c50f5d2ed4e..1aca5499b39 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx @@ -51,6 +51,7 @@ export interface AlmBindingDefinitionFormProps { onBitbucketVariantChange: ( bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud ) => void; + validationError?: string; } export default class AlmBindingDefinitionFormRenderer extends React.PureComponent< @@ -99,7 +100,13 @@ export default class AlmBindingDefinitionFormRenderer extends React.PureComponen }; render() { - const { isUpdate, alreadyHaveInstanceConfigured, canSubmit, submitting } = this.props; + const { + isUpdate, + alreadyHaveInstanceConfigured, + canSubmit, + submitting, + validationError + } = this.props; const header = translate('settings.almintegration.form.header', isUpdate ? 'edit' : 'create'); const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { @@ -125,6 +132,16 @@ export default class AlmBindingDefinitionFormRenderer extends React.PureComponen </Alert> )} {this.renderForm()} + {validationError && !canSubmit && ( + <Alert variant="error"> + <p className="spacer-bottom"> + {translate('settings.almintegration.configuration_invalid')} + </p> + <ul className="list-styled"> + <li>{validationError}</li> + </ul> + </Alert> + )} </div> <div className="modal-foot"> diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx index 09f5f58c8a3..b4ca5b1e7b4 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx @@ -26,11 +26,13 @@ import { createBitbucketServerConfiguration, createGithubConfiguration, createGitlabConfiguration, + deleteConfiguration, updateAzureConfiguration, updateBitbucketCloudConfiguration, updateBitbucketServerConfiguration, updateGithubConfiguration, - updateGitlabConfiguration + updateGitlabConfiguration, + validateAlmSettings } from '../../../../../api/alm-settings'; import { mockAzureBindingDefinition, @@ -53,7 +55,9 @@ jest.mock('../../../../../api/alm-settings', () => ({ updateBitbucketCloudConfiguration: jest.fn().mockResolvedValue({}), updateBitbucketServerConfiguration: jest.fn().mockResolvedValue({}), updateGithubConfiguration: jest.fn().mockResolvedValue({}), - updateGitlabConfiguration: jest.fn().mockResolvedValue({}) + updateGitlabConfiguration: jest.fn().mockResolvedValue({}), + validateAlmSettings: jest.fn().mockResolvedValue(undefined), + deleteConfiguration: jest.fn().mockResolvedValue(undefined) })); beforeEach(() => { @@ -89,12 +93,31 @@ it('should handle form submit', async () => { const wrapper = shallowRender({ afterSubmit }); wrapper.instance().setState({ formData }); - await waitAndUpdate(wrapper); await wrapper.instance().handleFormSubmit(); - await waitAndUpdate(wrapper); expect(afterSubmit).toHaveBeenCalledWith(formData); }); +it('should handle validation error during submit, and cancellation', async () => { + const afterSubmit = jest.fn(); + const formData = mockGithubBindingDefinition(); + const error = 'This a test error message'; + (validateAlmSettings as jest.Mock).mockResolvedValueOnce(error); + + const wrapper = shallowRender({ afterSubmit, enforceValidation: true }); + + wrapper.instance().setState({ formData }); + await wrapper.instance().handleFormSubmit(); + expect(validateAlmSettings).toHaveBeenCalledWith(formData.key); + expect(wrapper.state().validationError).toBe(error); + expect(afterSubmit).not.toHaveBeenCalledWith(); + + wrapper + .find(AlmBindingDefinitionFormRenderer) + .props() + .onCancel(); + expect(deleteConfiguration).toHaveBeenCalledWith(formData.key); +}); + it.each([ [AlmKeys.Azure, undefined, createAzureConfiguration], [AlmKeys.Azure, mockAzureBindingDefinition(), updateAzureConfiguration], diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormRenderer-test.tsx index a28503cd58c..e60c01849c8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormRenderer-test.tsx @@ -34,6 +34,9 @@ it('should render correctly', () => { expect(shallowRender({ alreadyHaveInstanceConfigured: true })).toMatchSnapshot('second instance'); expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); expect(shallowRender({ isUpdate: true })).toMatchSnapshot('editing'); + expect(shallowRender({ validationError: 'this is a validation error' })).toMatchSnapshot( + 'with validation error' + ); }); it.each([[AlmKeys.Azure], [AlmKeys.GitHub], [AlmKeys.GitLab], [AlmKeys.BitbucketServer]])( diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormRenderer-test.tsx.snap index ef718f37281..fceec04e547 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormRenderer-test.tsx.snap @@ -464,3 +464,76 @@ exports[`should render correctly: submitting 1`] = ` </form> </Modal> `; + +exports[`should render correctly: with validation error 1`] = ` +<Modal + contentLabel="settings.almintegration.form.header.create" + onRequestClose={[MockFunction]} + shouldCloseOnOverlayClick={false} + size="medium" +> + <form + className="views-form" + onSubmit={[Function]} + > + <div + className="modal-head" + > + <h2> + settings.almintegration.form.header.create + </h2> + </div> + <div + className="modal-body modal-container" + > + <GithubForm + formData={ + Object { + "appId": "123456", + "clientId": "client1", + "clientSecret": "**clientsecret**", + "key": "key", + "privateKey": "asdf1234", + "url": "http://github.enterprise.com", + } + } + onFieldChange={[MockFunction]} + /> + <Alert + variant="error" + > + <p + className="spacer-bottom" + > + settings.almintegration.configuration_invalid + </p> + <ul + className="list-styled" + > + <li> + this is a validation error + </li> + </ul> + </Alert> + </div> + <div + className="modal-foot" + > + <SubmitButton + disabled={true} + > + settings.almintegration.form.save + <DeferredSpinner + className="spacer-left" + loading={false} + /> + </SubmitButton> + <ResetButtonLink + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </div> + </form> +</Modal> +`; |