diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2020-05-11 15:14:53 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-05-15 20:03:42 +0000 |
commit | d811642a8b2381efe43fbc15da10e7774f5c2786 (patch) | |
tree | 459e8c6a67a11af70310a2f6f0f749bc2bce9cec /server/sonar-web/src/main | |
parent | a35ac1737bdb8a78e96c614372841102e4e0fc36 (diff) | |
download | sonarqube-d811642a8b2381efe43fbc15da10e7774f5c2786.tar.gz sonarqube-d811642a8b2381efe43fbc15da10e7774f5c2786.zip |
SONAR-12884 Improve the validation messages when creating a project
Diffstat (limited to 'server/sonar-web/src/main')
11 files changed, 453 insertions, 101 deletions
diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx index 1cb5f7f93b4..65212ebb525 100644 --- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx @@ -25,6 +25,10 @@ import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { createProject, doesComponentExists } from '../../../api/components'; +import ProjectKeyInput from '../../../components/common/ProjectKeyInput'; +import { validateProjectKey } from '../../../helpers/projects'; +import { ProjectKeyValidationResult } from '../../../types/component'; +import { PROJECT_NAME_MAX_LEN } from './constants'; import CreateProjectPageHeader from './CreateProjectPageHeader'; import './ManualProjectCreate.css'; @@ -153,15 +157,19 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat }; validateKey = (projectKey: string) => { - return projectKey.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(projectKey) - ? translate('onboarding.create_project.project_key.error') - : undefined; + const result = validateProjectKey(projectKey); + return result === ProjectKeyValidationResult.Valid + ? undefined + : translate('onboarding.create_project.project_key.error', result); }; validateName = (projectName: string) => { - return projectName.length === 0 || projectName.length > 255 - ? translate('onboarding.create_project.display_name.error') - : undefined; + if (projectName.length === 0) { + return translate('onboarding.create_project.display_name.error.empty'); + } else if (projectName.length > PROJECT_NAME_MAX_LEN) { + return translate('onboarding.create_project.display_name.error.too_long'); + } + return undefined; }; render() { @@ -175,8 +183,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat validating } = this.state; const { branchesEnabled } = this.props; - const projectKeyIsInvalid = touched && projectKeyError !== undefined; - const projectKeyIsValid = touched && !validating && projectKeyError === undefined; const projectNameIsInvalid = touched && projectNameError !== undefined; const projectNameIsValid = touched && projectNameError === undefined; @@ -190,30 +196,15 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat <div className="create-project-manual"> <div className="flex-1 huge-spacer-right"> <form className="manual-project-create" onSubmit={this.handleFormSubmit}> - <ValidationInput - className="form-field" - description={translate('onboarding.create_project.project_key.description')} + <ProjectKeyInput error={projectKeyError} help={translate('onboarding.create_project.project_key.help')} - id="project-key" - isInvalid={projectKeyIsInvalid} - isValid={projectKeyIsValid} label={translate('onboarding.create_project.project_key')} - required={true}> - <input - autoFocus={true} - className={classNames('input-super-large', { - 'is-invalid': projectKeyIsInvalid, - 'is-valid': projectKeyIsValid - })} - id="project-key" - maxLength={400} - minLength={1} - onChange={this.handleProjectKeyChange} - type="text" - value={projectKey} - /> - </ValidationInput> + onProjectKeyChange={this.handleProjectKeyChange} + projectKey={projectKey} + touched={touched} + validating={validating} + /> <ValidationInput className="form-field" @@ -231,7 +222,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat 'is-valid': projectNameIsValid })} id="project-name" - maxLength={255} + maxLength={PROJECT_NAME_MAX_LEN} minLength={1} onChange={this.handleProjectNameChange} type="text" diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx index 45d80de0668..33123a28e99 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx @@ -20,8 +20,15 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput'; import { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { createProject } from '../../../../api/components'; +import { createProject, doesComponentExists } from '../../../../api/components'; +import ProjectKeyInput from '../../../../components/common/ProjectKeyInput'; +import { validateProjectKey } from '../../../../helpers/projects'; +import { mockEvent } from '../../../../helpers/testMocks'; +import { ProjectKeyValidationResult } from '../../../../types/component'; +import { PROJECT_NAME_MAX_LEN } from '../constants'; import ManualProjectCreate from '../ManualProjectCreate'; jest.mock('../../../../api/components', () => ({ @@ -31,9 +38,10 @@ jest.mock('../../../../api/components', () => ({ .mockImplementation(({ component }) => Promise.resolve(component === 'exists')) })); -jest.mock('../../../../helpers/system', () => ({ - isSonarCloud: jest.fn().mockReturnValue(true) -})); +jest.mock('../../../../helpers/projects', () => { + const { ProjectKeyValidationResult } = jest.requireActual('../../../../types/component'); + return { validateProjectKey: jest.fn(() => ProjectKeyValidationResult.Valid) }; +}); beforeEach(() => { jest.clearAllMocks(); @@ -47,9 +55,14 @@ it('should correctly create a project', async () => { const onProjectCreate = jest.fn(); const wrapper = shallowRender({ onProjectCreate }); - change(wrapper.find('input#project-key'), 'bar'); + wrapper + .find(ProjectKeyInput) + .props() + .onProjectKeyChange(mockEvent({ currentTarget: { value: 'bar' } })); change(wrapper.find('input#project-name'), 'Bar'); - expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false); + expect(wrapper.find(SubmitButton).props().disabled).toBe(false); + expect(validateProjectKey).toBeCalledWith('bar'); + expect(doesComponentExists).toBeCalledWith({ component: 'bar' }); submit(wrapper.find('form')); expect(createProject).toBeCalledWith({ @@ -61,73 +74,72 @@ it('should correctly create a project', async () => { expect(onProjectCreate).toBeCalledWith(['bar']); }); -it('should not display any status when the key is not defined', () => { - const wrapper = shallowRender(); - const projectKeyInput = wrapper.find('ValidationInput').first(); - expect(projectKeyInput.prop('isInvalid')).toBe(false); - expect(projectKeyInput.prop('isValid')).toBe(false); -}); - it('should not display any status when the name is not defined', () => { const wrapper = shallowRender(); - const projectKeyInput = wrapper.find('ValidationInput').last(); - expect(projectKeyInput.prop('isInvalid')).toBe(false); - expect(projectKeyInput.prop('isValid')).toBe(false); + const projectNameInput = wrapper.find(ValidationInput); + expect(projectNameInput.props().isInvalid).toBe(false); + expect(projectNameInput.props().isValid).toBe(false); }); it('should have an error when the key is invalid', () => { + (validateProjectKey as jest.Mock).mockReturnValueOnce(ProjectKeyValidationResult.TooLong); const wrapper = shallowRender(); - change(wrapper.find('input#project-key'), 'KEy-with#speci@l_char'); - expect( - wrapper - .find('ValidationInput') - .first() - .prop('isInvalid') - ).toBe(true); + const instance = wrapper.instance(); + instance.handleProjectKeyChange(mockEvent()); + expect(wrapper.find(ProjectKeyInput).props().error).toBe( + `onboarding.create_project.project_key.error.${ProjectKeyValidationResult.TooLong}` + ); }); it('should have an error when the key already exists', async () => { const wrapper = shallowRender(); - change(wrapper.find('input#project-key'), 'exists'); - + wrapper.instance().handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists' } })); await waitAndUpdate(wrapper); - expect( - wrapper - .find('ValidationInput') - .first() - .prop('isInvalid') - ).toBe(true); + expect(wrapper.state().projectKeyError).toBe('onboarding.create_project.project_key.taken'); }); it('should ignore promise return if value has been changed in the meantime', async () => { + (validateProjectKey as jest.Mock) + .mockReturnValueOnce(ProjectKeyValidationResult.Valid) + .mockReturnValueOnce(ProjectKeyValidationResult.InvalidChar); const wrapper = shallowRender(); + const instance = wrapper.instance(); - change(wrapper.find('input#project-key'), 'exists'); - change(wrapper.find('input#project-key'), 'exists%'); + instance.handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists' } })); + instance.handleProjectKeyChange(mockEvent({ currentTarget: { value: 'exists%' } })); await waitAndUpdate(wrapper); - expect(wrapper.state('touched')).toBe(true); - expect(wrapper.state('projectKeyError')).toBe('onboarding.create_project.project_key.error'); + expect(wrapper.state().touched).toBe(true); + expect(wrapper.state().projectKeyError).toBe( + `onboarding.create_project.project_key.error.${ProjectKeyValidationResult.InvalidChar}` + ); }); it('should autofill the name based on the key', () => { const wrapper = shallowRender(); - change(wrapper.find('input#project-key'), 'bar'); - expect(wrapper.find('input#project-name').prop('value')).toBe('bar'); + wrapper.instance().handleProjectKeyChange(mockEvent({ currentTarget: { value: 'bar' } })); + expect(wrapper.find('input#project-name').props().value).toBe('bar'); }); -it('should have an error when the name is empty', () => { +it('should have an error when the name is incorrect', () => { const wrapper = shallowRender(); - change(wrapper.find('input#project-key'), 'bar'); - change(wrapper.find('input#project-name'), ''); - expect( - wrapper - .find('ValidationInput') - .last() - .prop('isInvalid') - ).toBe(true); - expect(wrapper.state('projectNameError')).toBe('onboarding.create_project.display_name.error'); + wrapper.setState({ touched: true }); + const instance = wrapper.instance(); + + instance.handleProjectNameChange(mockEvent({ currentTarget: { value: '' } })); + expect(wrapper.find(ValidationInput).props().isInvalid).toBe(true); + expect(wrapper.state().projectNameError).toBe( + 'onboarding.create_project.display_name.error.empty' + ); + + instance.handleProjectNameChange( + mockEvent({ currentTarget: { value: new Array(PROJECT_NAME_MAX_LEN + 1).fill('a').join('') } }) + ); + expect(wrapper.find(ValidationInput).props().isInvalid).toBe(true); + expect(wrapper.state().projectNameError).toBe( + 'onboarding.create_project.display_name.error.too_long' + ); }); function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap index baaac6282ea..806b2c17e3e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap @@ -15,27 +15,14 @@ exports[`should render correctly 1`] = ` className="manual-project-create" onSubmit={[Function]} > - <ValidationInput - className="form-field" - description="onboarding.create_project.project_key.description" + <ProjectKeyInput help="onboarding.create_project.project_key.help" - id="project-key" - isInvalid={false} - isValid={false} label="onboarding.create_project.project_key" - required={true} - > - <input - autoFocus={true} - className="input-super-large" - id="project-key" - maxLength={400} - minLength={1} - onChange={[Function]} - type="text" - value="" - /> - </ValidationInput> + onProjectKeyChange={[Function]} + projectKey="" + touched={false} + validating={false} + /> <ValidationInput className="form-field" description="onboarding.create_project.display_name.description" diff --git a/server/sonar-web/src/main/js/apps/create/project/constants.ts b/server/sonar-web/src/main/js/apps/create/project/constants.ts new file mode 100644 index 00000000000..967aee5d9b7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/constants.ts @@ -0,0 +1,21 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export const PROJECT_NAME_MAX_LEN = 255; diff --git a/server/sonar-web/src/main/js/components/common/ProjectKeyInput.tsx b/server/sonar-web/src/main/js/components/common/ProjectKeyInput.tsx new file mode 100644 index 00000000000..085acc16577 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/ProjectKeyInput.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import * as classNames from 'classnames'; +import * as React from 'react'; +import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { PROJECT_KEY_MAX_LEN } from '../../helpers/constants'; + +export interface ProjectKeyInputProps { + error?: string; + help?: string; + label?: string; + onProjectKeyChange: (e: React.ChangeEvent<HTMLInputElement>) => void; + placeholder?: string; + projectKey?: string; + touched: boolean; + validating?: boolean; +} + +export default function ProjectKeyInput(props: ProjectKeyInputProps) { + const { error, help, label, placeholder, projectKey, touched, validating } = props; + + const isInvalid = touched && error !== undefined; + const isValid = touched && !validating && error === undefined; + + return ( + <ValidationInput + className="form-field" + description={translate('onboarding.create_project.project_key.description')} + error={error} + help={help} + id="project-key" + isInvalid={isInvalid} + isValid={isValid} + label={label} + required={label !== undefined}> + <input + autoFocus={true} + className={classNames('input-super-large', { + 'is-invalid': isInvalid, + 'is-valid': isValid + })} + id="project-key" + maxLength={PROJECT_KEY_MAX_LEN} + minLength={1} + onChange={props.onProjectKeyChange} + placeholder={placeholder} + type="text" + value={projectKey} + /> + </ValidationInput> + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/ProjectKeyInput-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/ProjectKeyInput-test.tsx new file mode 100644 index 00000000000..f8d51f9001b --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/ProjectKeyInput-test.tsx @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput'; +import ProjectKeyInput, { ProjectKeyInputProps } from '../ProjectKeyInput'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ projectKey: 'foo' })).toMatchSnapshot('with value'); + expect( + shallowRender({ help: 'foo.help', label: 'foo.label', placeholder: 'foo.placeholder' }) + ).toMatchSnapshot('with label, help, and placeholder'); + expect(shallowRender({ touched: true })).toMatchSnapshot('valid'); + expect(shallowRender({ touched: true, error: 'bar.baz' })).toMatchSnapshot('invalid'); + expect(shallowRender({ touched: true, validating: true })).toMatchSnapshot('validating'); +}); + +it('should not display any status when the key is not defined', () => { + const wrapper = shallowRender(); + const input = wrapper.find(ValidationInput); + expect(input.props().isInvalid).toBe(false); + expect(input.props().isValid).toBe(false); +}); + +function shallowRender(props: Partial<ProjectKeyInputProps> = {}) { + return shallow<ProjectKeyInputProps>( + <ProjectKeyInput onProjectKeyChange={jest.fn()} touched={false} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap new file mode 100644 index 00000000000..4265dc8d667 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + id="project-key" + isInvalid={false} + isValid={false} + required={false} +> + <input + autoFocus={true} + className="input-super-large" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + type="text" + /> +</ValidationInput> +`; + +exports[`should render correctly: invalid 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + error="bar.baz" + id="project-key" + isInvalid={true} + isValid={false} + required={false} +> + <input + autoFocus={true} + className="input-super-large is-invalid" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + type="text" + /> +</ValidationInput> +`; + +exports[`should render correctly: valid 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + id="project-key" + isInvalid={false} + isValid={true} + required={false} +> + <input + autoFocus={true} + className="input-super-large is-valid" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + type="text" + /> +</ValidationInput> +`; + +exports[`should render correctly: validating 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + id="project-key" + isInvalid={false} + isValid={false} + required={false} +> + <input + autoFocus={true} + className="input-super-large" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + type="text" + /> +</ValidationInput> +`; + +exports[`should render correctly: with label, help, and placeholder 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + help="foo.help" + id="project-key" + isInvalid={false} + isValid={false} + label="foo.label" + required={true} +> + <input + autoFocus={true} + className="input-super-large" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + placeholder="foo.placeholder" + type="text" + /> +</ValidationInput> +`; + +exports[`should render correctly: with value 1`] = ` +<ValidationInput + className="form-field" + description="onboarding.create_project.project_key.description" + id="project-key" + isInvalid={false} + isValid={false} + required={false} +> + <input + autoFocus={true} + className="input-super-large" + id="project-key" + maxLength={400} + minLength={1} + onChange={[MockFunction]} + type="text" + value="foo" + /> +</ValidationInput> +`; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/projects-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/projects-test.ts new file mode 100644 index 00000000000..cfee705d54e --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/projects-test.ts @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ProjectKeyValidationResult } from '../../types/component'; +import { PROJECT_KEY_MAX_LEN } from '../constants'; +import { validateProjectKey } from '../projects'; + +describe('validateProjectKey', () => { + it('should correctly flag an invalid key', () => { + // Cannot have special characters except whitelist. + expect(validateProjectKey('foo/bar')).toBe(ProjectKeyValidationResult.InvalidChar); + // Cannot contain only numbers. + expect(validateProjectKey('123')).toBe(ProjectKeyValidationResult.OnlyDigits); + // Cannot be more than 400 chars long. + expect(validateProjectKey(new Array(PROJECT_KEY_MAX_LEN + 1).fill('a').join(''))).toBe( + ProjectKeyValidationResult.TooLong + ); + // Cannot be empty. + expect(validateProjectKey('')).toBe(ProjectKeyValidationResult.Empty); + }); + + it('should not flag a valid key', () => { + expect(validateProjectKey('foo:bar_baz-12.is')).toBe(ProjectKeyValidationResult.Valid); + expect(validateProjectKey('12:34')).toBe(ProjectKeyValidationResult.Valid); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index c82212bee8e..f0d60622137 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -53,3 +53,5 @@ export const RATING_COLORS = [ colors.orange, colors.red ]; + +export const PROJECT_KEY_MAX_LEN = 400; diff --git a/server/sonar-web/src/main/js/helpers/projects.ts b/server/sonar-web/src/main/js/helpers/projects.ts new file mode 100644 index 00000000000..f64688d73e8 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/projects.ts @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ProjectKeyValidationResult } from '../types/component'; +import { PROJECT_KEY_MAX_LEN } from './constants'; + +export function validateProjectKey(projectKey: string): ProjectKeyValidationResult { + // This is the regex used on the backend: + // [\p{Alnum}\-_.:]*[\p{Alpha}\-_.:]+[\p{Alnum}\-_.:]* + // See sonar-core/src/main/java/org/sonar/core/component/ComponentKeys.java + const regex = /^[\w\-.:]*[a-z\-_.:]+[\w\-.:]*$/i; + if (projectKey.length === 0) { + return ProjectKeyValidationResult.Empty; + } else if (projectKey.length > PROJECT_KEY_MAX_LEN) { + return ProjectKeyValidationResult.TooLong; + } else if (regex.test(projectKey)) { + return ProjectKeyValidationResult.Valid; + } else { + return /^[0-9]+$/.test(projectKey) + ? ProjectKeyValidationResult.OnlyDigits + : ProjectKeyValidationResult.InvalidChar; + } +} diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts index 682fe0ecc8d..ac2812362af 100644 --- a/server/sonar-web/src/main/js/types/component.ts +++ b/server/sonar-web/src/main/js/types/component.ts @@ -17,6 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +export enum Visibility { + Public = 'public', + Private = 'private' +} export enum ComponentQualifier { Application = 'APP', @@ -30,6 +34,14 @@ export enum ComponentQualifier { TestFile = 'UTS' } +export enum ProjectKeyValidationResult { + Valid = 'valid', + Empty = 'empty', + TooLong = 'too_long', + InvalidChar = 'invalid_char', + OnlyDigits = 'only_digits' +} + export function isPortfolioLike(componentQualifier?: string | ComponentQualifier) { return Boolean( componentQualifier && @@ -39,8 +51,3 @@ export function isPortfolioLike(componentQualifier?: string | ComponentQualifier ].includes(componentQualifier) ); } - -export enum Visibility { - Public = 'public', - Private = 'private' -} |