Browse Source

SONAR-14932 Trigger alm settings validation before closing the modal

tags/9.0.0.45539
Philippe Perrin 3 years ago
parent
commit
f3180f874f

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View File

alreadyHaveInstanceConfigured={false} alreadyHaveInstanceConfigured={false}
onCancel={this.handleOnCancelCreation} onCancel={this.handleOnCancelCreation}
afterSubmit={this.handleAfterSubmit} afterSubmit={this.handleAfterSubmit}
enforceValidation={true}
/> />
)} )}
</div> </div>

+ 51
- 25
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx View File

*/ */
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getAlmSettings } from '../../../../api/alm-settings'; import { getAlmSettings } from '../../../../api/alm-settings';
import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks'; import { mockLocation, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings'; import { AlmKeys } from '../../../../types/alm-settings';
import AlmBindingDefinitionForm from '../../../settings/components/almIntegration/AlmBindingDefinitionForm';
import CreateProjectModeSelection from '../CreateProjectModeSelection';
import { CreateProjectPage } from '../CreateProjectPage'; import { CreateProjectPage } from '../CreateProjectPage';
import { CreateProjectModes } from '../types'; import { CreateProjectModes } from '../types';


expect(getAlmSettings).toBeCalled(); 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( expect(
shallowRender({ shallowRender({
location: mockLocation({ query: { mode: CreateProjectModes.Manual } })
location: mockLocation({ query: { mode } })
}) })
).toMatchSnapshot(); ).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 () => { it('should submit alm configuration creation properly for BBC', async () => {

+ 96
- 5
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap View File

// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`should render correctly 1`] = `
<Fragment> <Fragment>
<Helmet <Helmet
</Fragment> </Fragment>
`; `;


exports[`should render correctly if the Azure method is selected 1`] = `
exports[`should render correctly for azure mode 1`] = `
<Fragment> <Fragment>
<Helmet <Helmet
defer={true} defer={true}
</Fragment> </Fragment>
`; `;


exports[`should render correctly if the BBS method is selected 1`] = `
exports[`should render correctly for bitbucket mode 1`] = `
<Fragment> <Fragment>
<Helmet <Helmet
defer={true} defer={true}
</Fragment> </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> <Fragment>
<Helmet <Helmet
defer={true} defer={true}
</Fragment> </Fragment>
`; `;


exports[`should render correctly if the GitLab method is selected 1`] = `
exports[`should render correctly for gitlab mode 1`] = `
<Fragment> <Fragment>
<Helmet <Helmet
defer={true} defer={true}
</Fragment> </Fragment>
`; `;


exports[`should render correctly if the manual method is selected 1`] = `
exports[`should render correctly for manual mode 1`] = `
<Fragment> <Fragment>
<Helmet <Helmet
defer={true} defer={true}

+ 60
- 16
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx View File

createBitbucketServerConfiguration, createBitbucketServerConfiguration,
createGithubConfiguration, createGithubConfiguration,
createGitlabConfiguration, createGitlabConfiguration,
deleteConfiguration,
updateAzureConfiguration, updateAzureConfiguration,
updateBitbucketCloudConfiguration, updateBitbucketCloudConfiguration,
updateBitbucketServerConfiguration, updateBitbucketServerConfiguration,
updateGithubConfiguration, updateGithubConfiguration,
updateGitlabConfiguration
updateGitlabConfiguration,
validateAlmSettings
} from '../../../../api/alm-settings'; } from '../../../../api/alm-settings';
import { import {
AlmBindingDefinition, AlmBindingDefinition,
alm: AlmKeys; alm: AlmKeys;
bindingDefinition?: AlmBindingDefinition; bindingDefinition?: AlmBindingDefinition;
alreadyHaveInstanceConfigured: boolean; alreadyHaveInstanceConfigured: boolean;
onCancel?: () => void;
afterSubmit?: (data: AlmBindingDefinitionBase) => void;
onCancel: () => void;
afterSubmit: (data: AlmBindingDefinitionBase) => void;
enforceValidation?: boolean;
} }


interface State { interface State {
touched: boolean; touched: boolean;
submitting: boolean; submitting: boolean;
bitbucketVariant?: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud; bitbucketVariant?: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud;
alreadySavedFormData?: AlmBindingDefinition;
validationError?: string;
} }


const BINDING_PER_ALM = { const BINDING_PER_ALM = {
}; };


handleFormSubmit = async () => { 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 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 }); this.setState({ submitting: true });


try { try {
await apiMethod; 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); this.props.afterSubmit(formData);
} }
} finally { } finally {
if (this.mounted) { 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 = ( handleBitbucketVariantChange = (
bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud
) => { ) => {


render() { render() {
const { alm, bindingDefinition, alreadyHaveInstanceConfigured } = this.props; const { alm, bindingDefinition, alreadyHaveInstanceConfigured } = this.props;
const { formData, submitting, bitbucketVariant } = this.state;
const { formData, submitting, bitbucketVariant, validationError } = this.state;


const isUpdate = !!bindingDefinition; const isUpdate = !!bindingDefinition;


isUpdate={isUpdate} isUpdate={isUpdate}
canSubmit={this.canSubmit()} canSubmit={this.canSubmit()}
alreadyHaveInstanceConfigured={alreadyHaveInstanceConfigured} alreadyHaveInstanceConfigured={alreadyHaveInstanceConfigured}
onCancel={() => this.props.onCancel && this.props.onCancel()}
onCancel={this.handleOnCancel}
onSubmit={this.handleFormSubmit} onSubmit={this.handleFormSubmit}
onFieldChange={this.handleFieldChange} onFieldChange={this.handleFieldChange}
formData={formData} formData={formData}
submitting={submitting} submitting={submitting}
bitbucketVariant={bitbucketVariant} bitbucketVariant={bitbucketVariant}
onBitbucketVariantChange={this.handleBitbucketVariantChange} onBitbucketVariantChange={this.handleBitbucketVariantChange}
validationError={validationError}
/> />
); );
} }

+ 18
- 1
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormRenderer.tsx View File

onBitbucketVariantChange: ( onBitbucketVariantChange: (
bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud bitbucketVariant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud
) => void; ) => void;
validationError?: string;
} }


export default class AlmBindingDefinitionFormRenderer extends React.PureComponent< export default class AlmBindingDefinitionFormRenderer extends React.PureComponent<
}; };


render() { 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 header = translate('settings.almintegration.form.header', isUpdate ? 'edit' : 'create');


const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
</Alert> </Alert>
)} )}
{this.renderForm()} {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>


<div className="modal-foot"> <div className="modal-foot">

+ 27
- 4
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx View File

createBitbucketServerConfiguration, createBitbucketServerConfiguration,
createGithubConfiguration, createGithubConfiguration,
createGitlabConfiguration, createGitlabConfiguration,
deleteConfiguration,
updateAzureConfiguration, updateAzureConfiguration,
updateBitbucketCloudConfiguration, updateBitbucketCloudConfiguration,
updateBitbucketServerConfiguration, updateBitbucketServerConfiguration,
updateGithubConfiguration, updateGithubConfiguration,
updateGitlabConfiguration
updateGitlabConfiguration,
validateAlmSettings
} from '../../../../../api/alm-settings'; } from '../../../../../api/alm-settings';
import { import {
mockAzureBindingDefinition, mockAzureBindingDefinition,
updateBitbucketCloudConfiguration: jest.fn().mockResolvedValue({}), updateBitbucketCloudConfiguration: jest.fn().mockResolvedValue({}),
updateBitbucketServerConfiguration: jest.fn().mockResolvedValue({}), updateBitbucketServerConfiguration: jest.fn().mockResolvedValue({}),
updateGithubConfiguration: 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(() => { beforeEach(() => {
const wrapper = shallowRender({ afterSubmit }); const wrapper = shallowRender({ afterSubmit });


wrapper.instance().setState({ formData }); wrapper.instance().setState({ formData });
await waitAndUpdate(wrapper);
await wrapper.instance().handleFormSubmit(); await wrapper.instance().handleFormSubmit();
await waitAndUpdate(wrapper);
expect(afterSubmit).toHaveBeenCalledWith(formData); 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([ it.each([
[AlmKeys.Azure, undefined, createAzureConfiguration], [AlmKeys.Azure, undefined, createAzureConfiguration],
[AlmKeys.Azure, mockAzureBindingDefinition(), updateAzureConfiguration], [AlmKeys.Azure, mockAzureBindingDefinition(), updateAzureConfiguration],

+ 3
- 0
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormRenderer-test.tsx View File

expect(shallowRender({ alreadyHaveInstanceConfigured: true })).toMatchSnapshot('second instance'); expect(shallowRender({ alreadyHaveInstanceConfigured: true })).toMatchSnapshot('second instance');
expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
expect(shallowRender({ isUpdate: true })).toMatchSnapshot('editing'); 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]])( it.each([[AlmKeys.Azure], [AlmKeys.GitHub], [AlmKeys.GitLab], [AlmKeys.BitbucketServer]])(

+ 73
- 0
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormRenderer-test.tsx.snap View File

</form> </form>
</Modal> </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>
`;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

settings.almintegration.check_configuration=Check configuration settings.almintegration.check_configuration=Check configuration
settings.almintegration.checking_configuration=Checking configuration settings.almintegration.checking_configuration=Checking configuration
settings.almintegration.configuration_valid=Configuration valid settings.almintegration.configuration_valid=Configuration valid
settings.almintegration.configuration_invalid=You have the following errors in your configuration:
settings.almintegration.could_not_validate=Could not validate this configuration. settings.almintegration.could_not_validate=Could not validate this configuration.
settings.almintegration.delete.header=Delete configuration settings.almintegration.delete.header=Delete configuration
settings.almintegration.delete.message=Are you sure you want to delete the {id} configuration? settings.almintegration.delete.message=Are you sure you want to delete the {id} configuration?

Loading…
Cancel
Save