Bladeren bron

SONAR-16257 Validate Bitbucket Cloud Workspace IDs

tags/9.5.0.56709
Wouter Admiraal 2 jaren geleden
bovenliggende
commit
1589b18a3a
17 gewijzigde bestanden met toevoegingen van 527 en 135 verwijderingen
  1. 11
    2
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx
  2. 24
    11
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx
  3. 10
    0
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx
  4. 24
    2
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx
  5. 3
    0
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx
  6. 4
    2
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx
  7. 103
    40
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap
  8. 91
    1
      server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap
  9. 61
    23
      server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
  10. 38
    47
      server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx
  11. 64
    7
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
  12. 12
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java
  13. 1
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java
  14. 2
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java
  15. 39
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java
  16. 39
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java
  17. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 11
- 2
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionForm.tsx Bestand weergeven

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

+ 24
- 11
server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormField.tsx Bestand weergeven

@@ -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 && (

+ 10
- 0
server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketCloudForm.tsx Bestand weergeven

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

+ 24
- 2
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionForm-test.tsx Bestand weergeven

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


+ 3
- 0
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmBindingDefinitionFormField-test.tsx Bestand weergeven

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

+ 4
- 2
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketCloudForm-test.tsx Bestand weergeven

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

+ 103
- 40
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionFormField-test.tsx.snap Bestand weergeven

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

+ 91
- 1
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketCloudForm-test.tsx.snap Bestand weergeven

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

+ 61
- 23
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx Bestand weergeven

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

+ 38
- 47
server/sonar-web/src/main/js/components/controls/__tests__/ValidationInput-test.tsx Bestand weergeven

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

+ 64
- 7
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap Bestand weergeven

@@ -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"
@@ -45,6 +44,33 @@ exports[`should render when valid 1`] = `
<MandatoryFieldMarker />
</span>
</label>
<div
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"
>
@@ -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>
`;

+ 12
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/AlmSettingsSupport.java Bestand weergeven

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

+ 1
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudAction.java Bestand weergeven

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

+ 2
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudAction.java Bestand weergeven

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

+ 39
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/CreateBitbucketCloudActionTest.java Bestand weergeven

@@ -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;
@@ -141,6 +143,43 @@ public class CreateBitbucketCloudActionTest {
.isInstanceOf(ForbiddenException.class);
}

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

+ 39
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/UpdateBitbucketCloudActionTest.java Bestand weergeven

@@ -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;
@@ -188,6 +190,43 @@ public class UpdateBitbucketCloudActionTest {
.isInstanceOf(ForbiddenException.class);
}

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

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Bestand weergeven

@@ -1177,6 +1177,7 @@ settings.almintegration.form.name.github.help=Give your configuration a clear an
settings.almintegration.form.name.gitlab=Configuration name
settings.almintegration.form.name.gitlab.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitLab instance for a project.
settings.almintegration.form.workspace.bitbucketcloud=Workspace ID
settings.almintegration.form.workspace.bitbucketcloud.error=Workspace ID's can only contain lowercase letters, numbers, dashes, and underscores.
settings.almintegration.form.workspace.bitbucketcloud.help=The workspace ID is part of your bitbucket cloud URL {example}
settings.almintegration.form.oauth_key.bitbucketcloud.help=Bitbucket automatically creates an OAuth key when you create your OAuth consumer. You can find it in your Bitbucket Cloud workspace settings under OAuth consumers.
settings.almintegration.form.oauth_secret.bitbucketcloud.help=Bitbucket automatically creates an OAuth secret when you create your OAuth consumer. You can find it in your Bitbucket Cloud workspace settings under OAuth consumers.

Laden…
Annuleren
Opslaan