diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2022-06-03 12:01:13 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-06-07 20:03:10 +0000 |
commit | 1589b18a3ae1a5c26a7dca1d906783f8879901c0 (patch) | |
tree | de62a50f8ad091ba767aa10d0274bf455d64dc6f /server | |
parent | 88e9418985e774b9208f3f4a401058f467a171b5 (diff) | |
download | sonarqube-1589b18a3ae1a5c26a7dca1d906783f8879901c0.tar.gz sonarqube-1589b18a3ae1a5c26a7dca1d906783f8879901c0.zip |
SONAR-16257 Validate Bitbucket Cloud Workspace IDs
Diffstat (limited to 'server')
16 files changed, 526 insertions, 135 deletions
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 15098df3e79..22c0ef1be02 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 @@ -43,6 +43,7 @@ import { GitlabBindingDefinition, isBitbucketCloudBindingDefinition } from '../../../../types/alm-settings'; +import { BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT } from '../../constants'; import AlmBindingDefinitionFormRenderer from './AlmBindingDefinitionFormRenderer'; interface Props { @@ -224,9 +225,17 @@ export default class AlmBindingDefinitionForm extends React.PureComponent<Props, }; canSubmit = () => { - const { formData, touched } = this.state; + const { bitbucketVariant, formData, touched } = this.state; + const allFieldsProvided = touched && !Object.values(formData).some(v => !v); - return touched && !Object.values(formData).some(v => !v); + if ( + bitbucketVariant === AlmKeys.BitbucketCloud && + isBitbucketCloudBindingDefinition(formData) + ) { + return allFieldsProvided && BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT.test(formData.workspace); + } + + return allFieldsProvided; }; render() { diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx index da9d62e4448..ade370d1e75 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx @@ -21,6 +21,9 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import { ButtonLink } from '../../../../components/controls/buttons'; +import ValidationInput, { + ValidationInputErrorPlacement +} from '../../../../components/controls/ValidationInput'; import { Alert } from '../../../../components/ui/Alert'; import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker'; import { translate } from '../../../../helpers/l10n'; @@ -29,8 +32,10 @@ import '../../styles.css'; export interface AlmBindingDefinitionFormFieldProps<B extends AlmBindingDefinitionBase> { autoFocus?: boolean; + error?: string; help?: React.ReactNode; id: string; + isInvalid?: boolean; isTextArea?: boolean; maxLength?: number; onFieldChange: (id: keyof B, value: string) => void; @@ -46,8 +51,10 @@ export function AlmBindingDefinitionFormField<B extends AlmBindingDefinitionBase ) { const { autoFocus, + error, help, id, + isInvalid = false, isTextArea, maxLength, optional, @@ -94,17 +101,23 @@ export function AlmBindingDefinitionFormField<B extends AlmBindingDefinitionBase )} {showField && !isTextArea && ( - <input - className="width-100" - autoFocus={autoFocus} - id={id} - maxLength={maxLength || 100} - name={id} - onChange={e => props.onFieldChange(propKey, e.currentTarget.value)} - size={50} - type="text" - value={value} - /> + <ValidationInput + error={error} + errorPlacement={ValidationInputErrorPlacement.Bottom} + isValid={false} + isInvalid={isInvalid}> + <input + className="width-100" + autoFocus={autoFocus} + id={id} + maxLength={maxLength || 100} + name={id} + onChange={e => props.onFieldChange(propKey, e.currentTarget.value)} + size={50} + type="text" + value={value} + /> + </ValidationInput> )} {showField && isSecret && ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx index 06fa664ef85..cdbcbfdda4b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx @@ -24,6 +24,7 @@ import { Alert } from '../../../../components/ui/Alert'; import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { translate } from '../../../../helpers/l10n'; import { AlmKeys, BitbucketCloudBindingDefinition } from '../../../../types/alm-settings'; +import { BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT } from '../../constants'; import { AlmBindingDefinitionFormField } from './AlmBindingDefinitionFormField'; export interface BitbucketCloudFormProps { @@ -33,6 +34,9 @@ export interface BitbucketCloudFormProps { export default function BitbucketCloudForm(props: BitbucketCloudFormProps) { const { formData } = props; + const workspaceIDIsInvalid = Boolean( + formData.workspace && !BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT.test(formData.workspace) + ); return ( <> @@ -62,6 +66,12 @@ export default function BitbucketCloudForm(props: BitbucketCloudFormProps) { /> } id="workspace.bitbucketcloud" + error={ + workspaceIDIsInvalid + ? translate('settings.almintegration.form.workspace.bitbucketcloud.error') + : undefined + } + isInvalid={workspaceIDIsInvalid} maxLength={80} onFieldChange={props.onFieldChange} propKey="workspace" 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 62d8fa61751..bf9adfa568b 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 @@ -147,7 +147,11 @@ it.each([ ); it('should call the proper api for BBC', async () => { - const wrapper = shallowRender({ alm: AlmKeys.BitbucketServer, bindingDefinition: undefined }); + const wrapper = shallowRender({ + // Reminder: due to the way the settings app works, we never pass AlmKeys.BitbucketCloud as `alm`. + alm: AlmKeys.BitbucketServer, + bindingDefinition: undefined + }); wrapper.instance().handleBitbucketVariantChange(AlmKeys.BitbucketCloud); @@ -179,7 +183,25 @@ it('should store bitbucket variant', async () => { }); }); -it('should (dis)allow submit by validating its state', () => { +it('should (dis)allow submit by validating its state (Bitbucket Cloud)', () => { + const wrapper = shallowRender({ + // Reminder: due to the way the settings app works, we never pass AlmKeys.BitbucketCloud as `alm`. + alm: AlmKeys.BitbucketServer, + bindingDefinition: mockBitbucketCloudBindingDefinition() + }); + expect(wrapper.instance().canSubmit()).toBe(false); + + wrapper.setState({ + formData: mockBitbucketCloudBindingDefinition({ workspace: 'foo/bar' }), + touched: true + }); + expect(wrapper.instance().canSubmit()).toBe(false); + + wrapper.setState({ formData: mockBitbucketCloudBindingDefinition() }); + expect(wrapper.instance().canSubmit()).toBe(true); +}); + +it('should (dis)allow submit by validating its state (others)', () => { const wrapper = shallowRender(); expect(wrapper.instance().canSubmit()).toBe(false); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx index 135ff9b6e7d..b55e0bf93d2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx @@ -34,6 +34,9 @@ it('should render correctly', () => { expect(shallowRender({ optional: true })).toMatchSnapshot('optional'); expect(shallowRender({ overwriteOnly: true })).toMatchSnapshot('secret'); expect(shallowRender({ isSecret: true })).toMatchSnapshot('encryptable'); + expect(shallowRender({ error: 'some error message', isInvalid: true })).toMatchSnapshot( + 'invalid with error' + ); }); it('should call onFieldChange', () => { diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx index e3ec19a5f91..03a547717fe 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx @@ -23,8 +23,10 @@ import { mockBitbucketCloudBindingDefinition } from '../../../../../helpers/mock import BitbucketCloudForm, { BitbucketCloudFormProps } from '../BitbucketCloudForm'; it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); + expect( + shallowRender({ formData: mockBitbucketCloudBindingDefinition({ workspace: 'my/workspace' }) }) + ).toMatchSnapshot('invalid workspace ID'); }); function shallowRender(props: Partial<BitbucketCloudFormProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap index 2cb687fd3f5..daf6cf08860 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap @@ -18,16 +18,22 @@ exports[`should render correctly: default 1`] = ` <div className="settings-definition-right big-padded-top display-flex-column" > - <input - className="width-100" - id="key" - maxLength={40} - name="key" - onChange={[Function]} - size={50} - type="text" - value="key" - /> + <ValidationInput + errorPlacement={1} + isInvalid={false} + isValid={false} + > + <input + className="width-100" + id="key" + maxLength={40} + name="key" + onChange={[Function]} + size={50} + type="text" + value="key" + /> + </ValidationInput> </div> </div> `; @@ -50,16 +56,22 @@ exports[`should render correctly: encryptable 1`] = ` <div className="settings-definition-right big-padded-top display-flex-column" > - <input - className="width-100" - id="key" - maxLength={40} - name="key" - onChange={[Function]} - size={50} - type="text" - value="key" - /> + <ValidationInput + errorPlacement={1} + isInvalid={false} + isValid={false} + > + <input + className="width-100" + id="key" + maxLength={40} + name="key" + onChange={[Function]} + size={50} + type="text" + value="key" + /> + </ValidationInput> <Alert className="spacer-top" variant="info" @@ -89,6 +101,45 @@ exports[`should render correctly: encryptable 1`] = ` </div> `; +exports[`should render correctly: invalid with error 1`] = ` +<div + className="settings-definition" +> + <div + className="settings-definition-left" + > + <label + className="h3" + htmlFor="key" + > + settings.almintegration.form.key + </label> + <MandatoryFieldMarker /> + </div> + <div + className="settings-definition-right big-padded-top display-flex-column" + > + <ValidationInput + error="some error message" + errorPlacement={1} + isInvalid={true} + isValid={false} + > + <input + className="width-100" + id="key" + maxLength={40} + name="key" + onChange={[Function]} + size={50} + type="text" + value="key" + /> + </ValidationInput> + </div> +</div> +`; + exports[`should render correctly: optional 1`] = ` <div className="settings-definition" @@ -106,16 +157,22 @@ exports[`should render correctly: optional 1`] = ` <div className="settings-definition-right big-padded-top display-flex-column" > - <input - className="width-100" - id="key" - maxLength={40} - name="key" - onChange={[Function]} - size={50} - type="text" - value="key" - /> + <ValidationInput + errorPlacement={1} + isInvalid={false} + isValid={false} + > + <input + className="width-100" + id="key" + maxLength={40} + name="key" + onChange={[Function]} + size={50} + type="text" + value="key" + /> + </ValidationInput> </div> </div> `; @@ -206,16 +263,22 @@ exports[`should render correctly: with help 1`] = ` <div className="settings-definition-right big-padded-top display-flex-column" > - <input - className="width-100" - id="key" - maxLength={40} - name="key" - onChange={[Function]} - size={50} - type="text" - value="key" - /> + <ValidationInput + errorPlacement={1} + isInvalid={false} + isValid={false} + > + <input + className="width-100" + id="key" + maxLength={40} + name="key" + onChange={[Function]} + size={50} + type="text" + value="key" + /> + </ValidationInput> </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap index 2bf6f6b5244..64876591a64 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: default 1`] = ` <Fragment> <AlmBindingDefinitionFormField autoFocus={true} @@ -30,6 +30,7 @@ exports[`should render correctly 1`] = ` /> } id="workspace.bitbucketcloud" + isInvalid={false} maxLength={80} onFieldChange={[MockFunction]} propKey="workspace" @@ -86,3 +87,92 @@ exports[`should render correctly 1`] = ` /> </Fragment> `; + +exports[`should render correctly: invalid workspace ID 1`] = ` +<Fragment> + <AlmBindingDefinitionFormField + autoFocus={true} + help="settings.almintegration.form.name.bitbucketcloud.help" + id="name.bitbucket" + maxLength={200} + onFieldChange={[MockFunction]} + propKey="key" + value="key" + /> + <AlmBindingDefinitionFormField + error="settings.almintegration.form.workspace.bitbucketcloud.error" + help={ + <FormattedMessage + defaultMessage="settings.almintegration.form.workspace.bitbucketcloud.help" + id="settings.almintegration.form.workspace.bitbucketcloud.help" + values={ + Object { + "example": <React.Fragment> + https://bitbucket.org/ + <strong> + {workspace} + </strong> + /{repository} + </React.Fragment>, + } + } + /> + } + id="workspace.bitbucketcloud" + isInvalid={true} + maxLength={80} + onFieldChange={[MockFunction]} + propKey="workspace" + value="my/workspace" + /> + <Alert + className="big-spacer-top" + variant="info" + > + <FormattedMessage + defaultMessage="settings.almintegration.bitbucketcloud.info" + id="settings.almintegration.bitbucketcloud.info" + values={ + Object { + "doc_link": <Link + onlyActiveOnIndex={false} + style={Object {}} + target="_blank" + to="/documentation/analysis/bitbucket-cloud-integration/" + > + learn_more + </Link>, + "oauth": <a + href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/" + rel="noopener noreferrer" + target="_blank" + > + settings.almintegration.bitbucketcloud.oauth + </a>, + "permission": <strong> + Pull Requests: Read + </strong>, + } + } + /> + </Alert> + <AlmBindingDefinitionFormField + help="settings.almintegration.form.oauth_key.bitbucketcloud.help" + id="client_id.bitbucketcloud" + maxLength={80} + onFieldChange={[MockFunction]} + propKey="clientId" + value="client1" + /> + <AlmBindingDefinitionFormField + help="settings.almintegration.form.oauth_secret.bitbucketcloud.help" + id="client_secret.bitbucketcloud" + isSecret={true} + maxLength={160} + onFieldChange={[MockFunction]} + overwriteOnly={true} + propKey="clientSecret" + value="**clientsecret**" + /> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx index 51916d231f5..8c122effe95 100644 --- a/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx +++ b/server/sonar-web/src/main/js/components/controls/ValidationInput.tsx @@ -23,39 +23,77 @@ import AlertSuccessIcon from '../icons/AlertSuccessIcon'; import MandatoryFieldMarker from '../ui/MandatoryFieldMarker'; import HelpTooltip from './HelpTooltip'; -interface Props { +export interface ValidationInputProps { description?: React.ReactNode; children: React.ReactNode; className?: string; - error: string | undefined; + error?: string; + errorPlacement?: ValidationInputErrorPlacement; help?: string; - id: string; + id?: string; isInvalid: boolean; isValid: boolean; - label: React.ReactNode; + label?: React.ReactNode; required?: boolean; } -export default function ValidationInput(props: Props) { - const hasError = props.isInvalid && props.error !== undefined; +export enum ValidationInputErrorPlacement { + Right, + Bottom +} + +export default function ValidationInput(props: ValidationInputProps) { + const { + children, + className, + description, + error, + errorPlacement = ValidationInputErrorPlacement.Right, + help, + id, + isInvalid, + isValid, + label, + required + } = props; + const hasError = isInvalid && error !== undefined; + + let childrenWithStatus: React.ReactNode; + if (errorPlacement === ValidationInputErrorPlacement.Right) { + childrenWithStatus = ( + <> + {children} + {isValid && <AlertSuccessIcon className="spacer-left text-middle" />} + {isInvalid && <AlertErrorIcon className="spacer-left text-middle" />} + {hasError && <span className="little-spacer-left text-danger text-middle">{error}</span>} + </> + ); + } else { + childrenWithStatus = ( + <> + {children} + {isValid && <AlertSuccessIcon className="spacer-left text-middle" />} + <div className="spacer-top"> + {isInvalid && <AlertErrorIcon className="text-middle" />} + {hasError && <span className="little-spacer-left text-danger text-middle">{error}</span>} + </div> + </> + ); + } + return ( - <div className={props.className}> - <label htmlFor={props.id}> - <span className="text-middle"> - <strong>{props.label}</strong> - {props.required && <MandatoryFieldMarker />} - </span> - {props.help && <HelpTooltip className="spacer-left" overlay={props.help} />} - </label> - <div className="little-spacer-top spacer-bottom"> - {props.children} - {props.isInvalid && <AlertErrorIcon className="spacer-left text-middle" />} - {hasError && ( - <span className="little-spacer-left text-danger text-middle">{props.error}</span> - )} - {props.isValid && <AlertSuccessIcon className="spacer-left text-middle" />} - </div> - {props.description && <div className="note abs-width-400">{props.description}</div>} + <div className={className}> + {id && label && ( + <label htmlFor={id}> + <span className="text-middle"> + <strong>{label}</strong> + {required && <MandatoryFieldMarker />} + </span> + {help && <HelpTooltip className="spacer-left" overlay={help} />} + </label> + )} + <div className="little-spacer-top spacer-bottom">{childrenWithStatus}</div> + {description && <div className="note abs-width-400">{description}</div>} </div> ); } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx index 6d076151443..6fbb8f8c87a 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx @@ -19,55 +19,46 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import ValidationInput from '../ValidationInput'; +import ValidationInput, { + ValidationInputErrorPlacement, + ValidationInputProps +} from '../ValidationInput'; -it('should render', () => { +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ help: 'Help message', isValid: false })).toMatchSnapshot('with help'); expect( - shallow( - <ValidationInput - description="My description" - error={undefined} - help="Help message" - id="field-id" - isInvalid={false} - isValid={false} - label="Field label" - required={true}> - <div /> - </ValidationInput> - ) - ).toMatchSnapshot(); -}); - -it('should render with error', () => { + shallowRender({ + description: <div>My description</div>, + error: 'Field error message', + isInvalid: true, + isValid: false, + required: false + }) + ).toMatchSnapshot('with error'); expect( - shallow( - <ValidationInput - description={<div>My description</div>} - error="Field error message" - id="field-id" - isInvalid={true} - isValid={false} - label="Field label"> - <div /> - </ValidationInput> - ) - ).toMatchSnapshot(); + shallowRender({ + error: 'Field error message', + errorPlacement: ValidationInputErrorPlacement.Bottom, + isInvalid: true, + isValid: false + }) + ).toMatchSnapshot('error under the input'); + expect(shallowRender({ id: undefined, label: undefined })).toMatchSnapshot('no label'); }); -it('should render when valid', () => { - expect( - shallow( - <ValidationInput - description="My description" - error={undefined} - id="field-id" - isInvalid={false} - isValid={true} - label="Field label" - required={true}> - <div /> - </ValidationInput> - ) - ).toMatchSnapshot(); -}); +function shallowRender(props: Partial<ValidationInputProps> = {}) { + return shallow<ValidationInputProps>( + <ValidationInput + description="My description" + error={undefined} + id="field-id" + isInvalid={false} + isValid={true} + label="Field label" + required={true} + {...props}> + <div /> + </ValidationInput> + ); +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap index c2d68a1d2bf..949eaf4f7a1 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render 1`] = ` +exports[`should render correctly: default 1`] = ` <div> <label htmlFor="field-id" @@ -13,15 +13,14 @@ exports[`should render 1`] = ` </strong> <MandatoryFieldMarker /> </span> - <HelpTooltip - className="spacer-left" - overlay="Help message" - /> </label> <div className="little-spacer-top spacer-bottom" > <div /> + <AlertSuccessIcon + className="spacer-left text-middle" + /> </div> <div className="note abs-width-400" @@ -31,7 +30,7 @@ exports[`should render 1`] = ` </div> `; -exports[`should render when valid 1`] = ` +exports[`should render correctly: error under the input 1`] = ` <div> <label htmlFor="field-id" @@ -49,6 +48,33 @@ exports[`should render when valid 1`] = ` className="little-spacer-top spacer-bottom" > <div /> + <div + className="spacer-top" + > + <AlertErrorIcon + className="text-middle" + /> + <span + className="little-spacer-left text-danger text-middle" + > + Field error message + </span> + </div> + </div> + <div + className="note abs-width-400" + > + My description + </div> +</div> +`; + +exports[`should render correctly: no label 1`] = ` +<div> + <div + className="little-spacer-top spacer-bottom" + > + <div /> <AlertSuccessIcon className="spacer-left text-middle" /> @@ -61,7 +87,7 @@ exports[`should render when valid 1`] = ` </div> `; -exports[`should render with error 1`] = ` +exports[`should render correctly: with error 1`] = ` <div> <label htmlFor="field-id" @@ -96,3 +122,34 @@ exports[`should render with error 1`] = ` </div> </div> `; + +exports[`should render correctly: with help 1`] = ` +<div> + <label + htmlFor="field-id" + > + <span + className="text-middle" + > + <strong> + Field label + </strong> + <MandatoryFieldMarker /> + </span> + <HelpTooltip + className="spacer-left" + overlay="Help message" + /> + </label> + <div + className="little-spacer-top spacer-bottom" + > + <div /> + </div> + <div + className="note abs-width-400" + > + My description + </div> +</div> +`; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java index b185fc563f1..c4c82245c2b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java @@ -19,6 +19,7 @@ */ package org.sonar.server.almsettings.ws; +import java.util.regex.Pattern; import org.sonar.api.server.ServerSide; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -38,6 +39,8 @@ import static org.sonar.api.web.UserRole.ADMIN; @ServerSide public class AlmSettingsSupport { + private static final Pattern WORKSPACE_ID_PATTERN = Pattern.compile("^[a-z0-9\\-_]+$"); + private final DbClient dbClient; private final UserSession userSession; private final ComponentFinder componentFinder; @@ -70,6 +73,15 @@ public class AlmSettingsSupport { } } + public void checkBitbucketCloudWorkspaceIDFormat(String workspaceId) { + if (!WORKSPACE_ID_PATTERN.matcher(workspaceId).matches()) { + throw BadRequestException.create(String.format( + "Workspace ID '%s' has an incorrect format. Should only contain lowercase letters, numbers, dashes, and underscores.", + workspaceId + )); + } + } + public ProjectDto getProjectAsAdmin(DbSession dbSession, String projectKey) { return getProject(dbSession, projectKey, ADMIN); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java index 7797287f2d8..bf69d623f15 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java @@ -91,6 +91,7 @@ public class CreateBitbucketCloudAction implements AlmSettingsWsAction { almSettingsSupport.checkAlmMultipleFeatureEnabled(BITBUCKET); almSettingsSupport.checkAlmMultipleFeatureEnabled(BITBUCKET_CLOUD); almSettingsSupport.checkAlmSettingDoesNotAlreadyExist(dbSession, key); + almSettingsSupport.checkBitbucketCloudWorkspaceIDFormat(workspace); dbClient.almSettingDao().insert(dbSession, new AlmSettingDto() .setAlm(BITBUCKET_CLOUD) .setKey(key) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java index a9d528e0e51..cc09a39028b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java @@ -102,6 +102,8 @@ public class UpdateBitbucketCloudAction implements AlmSettingsWsAction { almSettingDto.setClientSecret(clientSecret); } + almSettingsSupport.checkBitbucketCloudWorkspaceIDFormat(workspace); + dbClient.almSettingDao().update(dbSession, almSettingDto .setKey(isNotBlank(newKey) ? newKey : key) .setClientId(clientId) diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java index fe4c6de35f7..c2a6f93e228 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java @@ -32,9 +32,11 @@ import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.groups.Tuple.tuple; import static org.mockito.Mockito.mock; @@ -142,6 +144,43 @@ public class CreateBitbucketCloudActionTest { } @Test + public void fail_when_workspace_id_format_is_incorrect() { + when(multipleAlmFeatureProvider.enabled()).thenReturn(false); + String workspace = "workspace/name"; + UserDto user = db.users().insertUser(); + userSession.logIn(user).setSystemAdministrator(); + + TestRequest request = ws.newRequest() + .setParam("key", "another new key") + .setParam("workspace", workspace) + .setParam("clientId", "id") + .setParam("clientSecret", "secret"); + + assertThatThrownBy(request::execute) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining(String.format( + "Workspace ID '%s' has an incorrect format. Should only contain lowercase letters, numbers, dashes, and underscores.", + workspace + )); + } + + @Test + public void do_not_fail_when_workspace_id_format_is_correct() { + when(multipleAlmFeatureProvider.enabled()).thenReturn(false); + String workspace = "work-space_123"; + UserDto user = db.users().insertUser(); + userSession.logIn(user).setSystemAdministrator(); + + TestRequest request = ws.newRequest() + .setParam("key", "yet another new key") + .setParam("workspace", workspace) + .setParam("clientId", "id") + .setParam("clientSecret", "secret"); + + assertThatNoException().isThrownBy(request::execute); + } + + @Test public void definition() { WebService.Action def = ws.getDef(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java index d2fac95ca41..754ce105ebb 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java @@ -29,6 +29,7 @@ import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.user.UserDto; import org.sonar.server.almsettings.MultipleAlmFeatureProvider; import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.tester.UserSessionRule; @@ -37,6 +38,7 @@ import org.sonar.server.ws.WsActionTester; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.groups.Tuple.tuple; import static org.mockito.ArgumentMatchers.any; @@ -189,6 +191,43 @@ public class UpdateBitbucketCloudActionTest { } @Test + public void fail_when_workspace_id_format_is_incorrect() { + String workspace = "workspace/name"; + UserDto user = db.users().insertUser(); + userSession.logIn(user).setSystemAdministrator(); + AlmSettingDto almSettingDto = db.almSettings().insertBitbucketAlmSetting(); + + TestRequest request = ws.newRequest() + .setParam("key", almSettingDto.getKey()) + .setParam("workspace", workspace) + .setParam("clientId", "id") + .setParam("clientSecret", "secret"); + + assertThatThrownBy(request::execute) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining(String.format( + "Workspace ID '%s' has an incorrect format. Should only contain lowercase letters, numbers, dashes, and underscores.", + workspace + )); + } + + @Test + public void do_not_fail_when_workspace_id_format_is_correct() { + String workspace = "work-space_123"; + UserDto user = db.users().insertUser(); + userSession.logIn(user).setSystemAdministrator(); + AlmSettingDto almSettingDto = db.almSettings().insertBitbucketAlmSetting(); + + TestRequest request = ws.newRequest() + .setParam("key", almSettingDto.getKey()) + .setParam("workspace", workspace) + .setParam("clientId", "id") + .setParam("clientSecret", "secret"); + + assertThatNoException().isThrownBy(request::execute); + } + + @Test public void definition() { WebService.Action def = ws.getDef(); |