GitlabBindingDefinition,
isBitbucketCloudBindingDefinition
} from '../../../../types/alm-settings';
+import { BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT } from '../../constants';
import AlmBindingDefinitionFormRenderer from './AlmBindingDefinitionFormRenderer';
interface 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() {
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';
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;
) {
const {
autoFocus,
+ error,
help,
id,
+ isInvalid = false,
isTextArea,
maxLength,
optional,
)}
{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 && (
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 {
export default function BitbucketCloudForm(props: BitbucketCloudFormProps) {
const { formData } = props;
+ const workspaceIDIsInvalid = Boolean(
+ formData.workspace && !BITBUCKET_CLOUD_WORKSPACE_ID_FORMAT.test(formData.workspace)
+ );
return (
<>
/>
}
id="workspace.bitbucketcloud"
+ error={
+ workspaceIDIsInvalid
+ ? translate('settings.almintegration.form.workspace.bitbucketcloud.error')
+ : undefined
+ }
+ isInvalid={workspaceIDIsInvalid}
maxLength={80}
onFieldChange={props.onFieldChange}
propKey="workspace"
);
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);
});
});
-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);
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', () => {
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> = {}) {
<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>
`;
<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"
</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"
<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>
`;
<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>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
<Fragment>
<AlmBindingDefinitionFormField
autoFocus={true}
/>
}
id="workspace.bitbucketcloud"
+ isInvalid={false}
maxLength={80}
onFieldChange={[MockFunction]}
propKey="workspace"
/>
</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>
+`;
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>
);
}
*/
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>
+ );
+}
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
+exports[`should render correctly: default 1`] = `
<div>
<label
htmlFor="field-id"
</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"
</div>
`;
-exports[`should render when valid 1`] = `
+exports[`should render correctly: error under the input 1`] = `
<div>
<label
htmlFor="field-id"
<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"
>
</div>
`;
-exports[`should render with error 1`] = `
+exports[`should render correctly: with error 1`] = `
<div>
<label
htmlFor="field-id"
</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>
+`;
*/
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;
@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;
}
}
+ 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);
}
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)
almSettingDto.setClientSecret(clientSecret);
}
+ almSettingsSupport.checkBitbucketCloudWorkspaceIDFormat(workspace);
+
dbClient.almSettingDao().update(dbSession, almSettingDto
.setKey(isNotBlank(newKey) ? newKey : key)
.setClientId(clientId)
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;
.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();
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;
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;
.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();
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.