aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2022-06-03 12:01:13 +0200
committersonartech <sonartech@sonarsource.com>2022-06-07 20:03:10 +0000
commit1589b18a3ae1a5c26a7dca1d906783f8879901c0 (patch)
treede62a50f8ad091ba767aa10d0274bf455d64dc6f /server
parent88e9418985e774b9208f3f4a401058f467a171b5 (diff)
downloadsonarqube-1589b18a3ae1a5c26a7dca1d906783f8879901c0.tar.gz
sonarqube-1589b18a3ae1a5c26a7dca1d906783f8879901c0.zip
SONAR-16257 Validate Bitbucket Cloud Workspace IDs
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap143
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap92
-rw-r--r--server/sonar-web/src/main/js/components/controls/ValidationInput.tsx84
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx85
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap71
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java12
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java1
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java2
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java39
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java39
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();