@@ -257,6 +257,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { | |||
alreadyHaveInstanceConfigured={false} | |||
onCancel={this.handleOnCancelCreation} | |||
afterSubmit={this.handleAfterSubmit} | |||
enforceValidation={true} | |||
/> | |||
)} | |||
</div> |
@@ -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 () => { |
@@ -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} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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"> |
@@ -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], |
@@ -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]])( |
@@ -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> | |||
`; |
@@ -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? |