Browse Source

SONAR-14932 Trigger alm settings validation before closing the modal

tags/9.0.0.45539
Philippe Perrin 2 years ago
parent
commit
f3180f874f

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

@@ -257,6 +257,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
alreadyHaveInstanceConfigured={false}
onCancel={this.handleOnCancelCreation}
afterSubmit={this.handleAfterSubmit}
enforceValidation={true}
/>
)}
</div>

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

@@ -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 () => {

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

@@ -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}

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

@@ -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}
/>
);
}

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

@@ -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">

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

@@ -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],

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

@@ -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]])(

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

@@ -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>
`;

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

@@ -1112,6 +1112,7 @@ settings.almintegration.create.tooltip.link=Enterprise Edition
settings.almintegration.check_configuration=Check configuration
settings.almintegration.checking_configuration=Checking configuration
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.delete.header=Delete configuration
settings.almintegration.delete.message=Are you sure you want to delete the {id} configuration?

Loading…
Cancel
Save