Преглед изворни кода

SONAR-14057 Add PAT form for azure onboarding

tags/8.6.0.39681
Jeremy Davis пре 3 година
родитељ
комит
753af65e1a
19 измењених фајлова са 1153 додато и 8 уклоњено
  1. 5
    3
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  2. 134
    0
      server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx
  3. 143
    0
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
  4. 87
    0
      server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
  5. 31
    0
      server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx
  6. 1
    0
      server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
  7. 29
    3
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  8. 72
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx
  9. 92
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
  10. 49
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
  11. 15
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
  12. 8
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
  13. 229
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap
  14. 17
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
  15. 104
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
  16. 85
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
  17. 38
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  18. 1
    0
      server/sonar-web/src/main/js/apps/create/project/types.ts
  19. 13
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 5
- 3
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx Прегледај датотеку

@@ -48,10 +48,10 @@ interface State {
/*
* ALMs for which the import feature has been implemented
*/
const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];

const almSettingsValidators = {
[AlmKeys.Azure]: (_: AlmSettingsInstance) => true,
[AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
[AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
@@ -73,7 +73,9 @@ export class GlobalNavPlus extends React.PureComponent<Props, State> {
this.setState({ governanceReady: true });
}
},
() => {}
() => {
/* error handled globally */
}
);
}
}

+ 134
- 0
server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx Прегледај датотеку

@@ -0,0 +1,134 @@
/*
* 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 { FormattedMessage } from 'react-intl';
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
import DetachIcon from 'sonar-ui-common/components/icons/DetachIcon';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { AlmSettingsInstance } from '../../../types/alm-settings';

export interface AzurePersonalAccessTokenFormProps {
almSetting: AlmSettingsInstance;
onPersonalAccessTokenCreate: (token: string) => void;
submitting?: boolean;
validationFailed: boolean;
}

function getAzurePatUrl(url: string) {
return `${url.replace(/\/$/, '')}/_usersSettings/tokens`;
}

export default function AzurePersonalAccessTokenForm(props: AzurePersonalAccessTokenFormProps) {
const {
almSetting: { alm, url },
submitting = false,
validationFailed
} = props;

const [touched, setTouched] = React.useState(false);
React.useEffect(() => {
setTouched(false);
}, [submitting]);

const [token, setToken] = React.useState('');

const isInvalid = (validationFailed && !touched) || (touched && !token);

let errorMessage;
if (!token) {
errorMessage = translate('onboarding.create_project.pat_form.pat_required');
} else if (isInvalid) {
errorMessage = translate('onboarding.create_project.pat_incorrect', alm);
}

return (
<div className="boxed-group abs-width-600">
<div className="boxed-group-inner">
<h2>{translate('onboarding.create_project.pat_form.title', alm)}</h2>

<div className="big-spacer-top big-spacer-bottom">
<FormattedMessage
id="onboarding.create_project.pat_help.instructions"
defaultMessage={translate('onboarding.create_project.pat_help.instructions', alm)}
values={{
link: url ? (
<a
className="link-with-icon"
href={getAzurePatUrl(url)}
rel="noopener noreferrer"
target="_blank">
<DetachIcon className="little-spacer-right" />
<span>
{translate('onboarding.create_project.pat_help.instructions.link', alm)}
</span>
</a>
) : (
translate('onboarding.create_project.pat_help.instructions.link', alm)
),
scope: (
<strong>
<em>Code (Read & Write)</em>
</strong>
)
}}
/>
</div>

<form
onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
props.onPersonalAccessTokenCreate(token);
}}>
<ValidationInput
error={errorMessage}
id="personal_access_token"
isInvalid={isInvalid}
isValid={false}
label={translate('onboarding.create_project.enter_pat')}
required={true}>
<input
autoFocus={true}
className={classNames('width-100 little-spacer-bottom', {
'is-invalid': isInvalid
})}
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
setTouched(true);
}}
type="text"
value={token}
/>
</ValidationInput>

<SubmitButton disabled={isInvalid || submitting || !touched}>
{translate('onboarding.create_project.pat_form.list_repositories')}
</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
</div>
</div>
);
}

+ 143
- 0
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx Прегледај датотеку

@@ -0,0 +1,143 @@
/*
* 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 React from 'react';
import { WithRouterProps } from 'react-router';
import {
checkPersonalAccessTokenIsValid,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
import { AlmSettingsInstance } from '../../../types/alm-settings';
import AzureCreateProjectRenderer from './AzureProjectCreateRenderer';

interface Props extends Pick<WithRouterProps, 'location'> {
canAdmin: boolean;
loadingBindings: boolean;
onProjectCreate: (projectKeys: string[]) => void;
settings: AlmSettingsInstance[];
}

interface State {
loading: boolean;
patIsValid?: boolean;
settings?: AlmSettingsInstance;
submittingToken?: boolean;
tokenValidationFailed: boolean;
}

export default class AzureProjectCreate extends React.PureComponent<Props, State> {
mounted = false;

constructor(props: Props) {
super(props);
this.state = {
// For now, we only handle a single instance. So we always use the first
// one from the list.
settings: props.settings[0],
loading: false,
tokenValidationFailed: false
};
}

componentDidMount() {
this.mounted = true;
this.fetchInitialData();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
this.setState(
{ settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined },
() => this.fetchInitialData()
);
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchInitialData = async () => {
this.setState({ loading: true });

const patIsValid = await this.checkPersonalAccessToken().catch(() => false);

if (this.mounted) {
this.setState({
patIsValid,
loading: false
});
}
};

checkPersonalAccessToken = () => {
const { settings } = this.state;

if (!settings) {
return Promise.resolve(false);
}

return checkPersonalAccessTokenIsValid(settings.key);
};

handlePersonalAccessTokenCreate = async (token: string) => {
const { settings } = this.state;

if (!settings || token.length < 1) {
return;
}

this.setState({ submittingToken: true, tokenValidationFailed: false });

try {
await setAlmPersonalAccessToken(settings.key, token);
const patIsValid = await this.checkPersonalAccessToken();

if (this.mounted) {
this.setState({ submittingToken: false, patIsValid, tokenValidationFailed: !patIsValid });

if (patIsValid) {
this.cleanUrl();
await this.fetchInitialData();
}
}
} catch (e) {
if (this.mounted) {
this.setState({ submittingToken: false });
}
}
};

render() {
const { canAdmin, loadingBindings, location } = this.props;
const { loading, patIsValid, settings, submittingToken, tokenValidationFailed } = this.state;

return (
<AzureCreateProjectRenderer
canAdmin={canAdmin}
loading={loading || loadingBindings}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
settings={settings}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken}
tokenValidationFailed={tokenValidationFailed}
/>
);
}
}

+ 87
- 0
server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx Прегледај датотеку

@@ -0,0 +1,87 @@
/*
* 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 React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm';
import AzureProjectsList from './AzureProjectsList';
import CreateProjectPageHeader from './CreateProjectPageHeader';
import WrongBindingCountAlert from './WrongBindingCountAlert';

export interface AzureProjectCreateRendererProps {
canAdmin?: boolean;
loading: boolean;
onPersonalAccessTokenCreate: (token: string) => void;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
submittingToken?: boolean;
tokenValidationFailed: boolean;
}

export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) {
const {
canAdmin,
loading,
showPersonalAccessTokenForm,
settings,
submittingToken,
tokenValidationFailed
} = props;

return (
<>
<CreateProjectPageHeader
title={
<span className="text-middle">
<img
alt="" // Should be ignored by screen readers
className="spacer-right"
height="24"
src={`${getBaseUrl()}/images/alm/azure.svg`}
/>
{translate('onboarding.create_project.azure.title')}
</span>
}
/>

{loading && <i className="spinner" />}

{!loading && !settings && (
<WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />
)}

{!loading &&
settings &&
(showPersonalAccessTokenForm ? (
<div className="display-flex-justify-center">
<AzurePersonalAccessTokenForm
almSetting={settings}
onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
submitting={submittingToken}
validationFailed={tokenValidationFailed}
/>
</div>
) : (
<AzureProjectsList />
))}
</>
);
}

+ 31
- 0
server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx Прегледај датотеку

@@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { Alert } from 'sonar-ui-common/components/ui/Alert';

export interface AzureProjectsListProps {}

export default function AzureProjectsList(_props: AzureProjectsListProps) {
return (
<div>
<Alert variant="warning">Coming soon!</Alert>
</div>
);
}

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx Прегледај датотеку

@@ -112,6 +112,7 @@ export default function CreateProjectModeSelection(props: CreateProjectModeSelec
</div>
</button>

{renderAlmOption(props, AlmKeys.Azure, CreateProjectModes.AzureDevOps)}
{renderAlmOption(props, AlmKeys.Bitbucket, CreateProjectModes.BitbucketServer)}
{renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
{renderAlmOption(props, AlmKeys.GitLab, CreateProjectModes.GitLab)}

+ 29
- 3
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx Прегледај датотеку

@@ -27,6 +27,7 @@ import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { withAppState } from '../../../components/hoc/withAppState';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import AzureProjectCreate from './AzureProjectCreate';
import BitbucketProjectCreate from './BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './GitHubProjectCreate';
@@ -41,6 +42,7 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
}

interface State {
azureSettings: AlmSettingsInstance[];
bitbucketSettings: AlmSettingsInstance[];
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
@@ -49,7 +51,13 @@ interface State {

export class CreateProjectPage extends React.PureComponent<Props, State> {
mounted = false;
state: State = { bitbucketSettings: [], githubSettings: [], gitlabSettings: [], loading: true };
state: State = {
azureSettings: [],
bitbucketSettings: [],
githubSettings: [],
gitlabSettings: [],
loading: true
};

componentDidMount() {
const {
@@ -71,6 +79,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
.then(almSettings => {
if (this.mounted) {
this.setState({
azureSettings: almSettings.filter(s => s.alm === AlmKeys.Azure),
bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket),
githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab),
@@ -105,9 +114,26 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
location,
router
} = this.props;
const { bitbucketSettings, githubSettings, gitlabSettings, loading } = this.state;
const {
azureSettings,
bitbucketSettings,
githubSettings,
gitlabSettings,
loading
} = this.state;

switch (mode) {
case CreateProjectModes.AzureDevOps: {
return (
<AzureProjectCreate
canAdmin={!!canAdmin}
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
settings={azureSettings}
/>
);
}
case CreateProjectModes.BitbucketServer: {
return (
<BitbucketProjectCreate
@@ -148,7 +174,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
default: {
const almCounts = {
[AlmKeys.Azure]: 0,
[AlmKeys.Azure]: azureSettings.length,
[AlmKeys.Bitbucket]: bitbucketSettings.length,
[AlmKeys.GitHub]: githubSettings.length,
[AlmKeys.GitLab]: gitlabSettings.length

+ 72
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx Прегледај датотеку

@@ -0,0 +1,72 @@
/*
* 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import { change, submit } from 'sonar-ui-common/helpers/testUtils';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { AlmKeys } from '../../../../types/alm-settings';
import AzurePersonalAccessTokenForm, {
AzurePersonalAccessTokenFormProps
} from '../AzurePersonalAccessTokenForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed');
});

it('should correctly handle form interactions', () => {
const onPersonalAccessTokenCreate = jest.fn();
const wrapper = shallowRender({ onPersonalAccessTokenCreate });

// Submit button disabled by default.
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);

// Submit button enabled if there's a value.
change(wrapper.find('input'), 'token');
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false);

// Expect correct calls to be made when submitting.
submit(wrapper.find('form'));
expect(onPersonalAccessTokenCreate).toBeCalled();

// If validation fails, we toggle the submitting flag and call useEffect()
// to set the `touched` flag to false again. Trigger a re-render, and mock
// useEffect(). This should de-activate the submit button again.
jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
wrapper.setProps({ submitting: false });
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
});

function shallowRender(props: Partial<AzurePersonalAccessTokenFormProps> = {}) {
return shallow<AzurePersonalAccessTokenFormProps>(
<AzurePersonalAccessTokenForm
almSetting={mockAlmSettingsInstance({
alm: AlmKeys.Azure,
url: 'http://www.example.com'
})}
onPersonalAccessTokenCreate={jest.fn()}
validationFailed={false}
{...props}
/>
);
}

+ 92
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx Прегледај датотеку

@@ -0,0 +1,92 @@
/*
* 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.
*/
/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
checkPersonalAccessTokenIsValid,
setAlmPersonalAccessToken
} from '../../../../api/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
import AzureProjectCreate from '../AzureProjectCreate';

jest.mock('../../../../api/alm-integrations', () => {
return {
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true),
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null)
};
});

beforeEach(jest.clearAllMocks);

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should correctly fetch binding info on mount', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalledWith('foo');
});

it('should correctly handle a valid PAT', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true);
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().patIsValid).toBe(true);
});

it('should correctly handle an invalid PAT', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false);
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().patIsValid).toBe(false);
});

it('should correctly handle setting a new PAT', async () => {
const wrapper = shallowRender();
wrapper.instance().handlePersonalAccessTokenCreate('token');
expect(setAlmPersonalAccessToken).toBeCalledWith('foo', 'token');
expect(wrapper.state().submittingToken).toBe(true);

(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false);
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().submittingToken).toBe(false);
expect(wrapper.state().tokenValidationFailed).toBe(true);
});

function shallowRender(overrides: Partial<AzureProjectCreate['props']> = {}) {
return shallow<AzureProjectCreate>(
<AzureProjectCreate
canAdmin={true}
loadingBindings={false}
location={mockLocation()}
onProjectCreate={jest.fn()}
settings={[mockAlmSettingsInstance({ alm: AlmKeys.Azure, key: 'foo' })]}
{...overrides}
/>
);
}

+ 49
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx Прегледај датотеку

@@ -0,0 +1,49 @@
/*
* 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.
*/
/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { AlmKeys } from '../../../../types/alm-settings';
import AzureProjectCreateRenderer, {
AzureProjectCreateRendererProps
} from '../AzureProjectCreateRenderer';

it('should render correctly', () => {
expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
expect(shallowRender({ settings: undefined })).toMatchSnapshot('no settings');
expect(shallowRender({ showPersonalAccessTokenForm: true })).toMatchSnapshot('token form');
expect(shallowRender({})).toMatchSnapshot('project list');
});

function shallowRender(overrides: Partial<AzureProjectCreateRendererProps>) {
return shallow(
<AzureProjectCreateRenderer
canAdmin={true}
loading={false}
onPersonalAccessTokenCreate={jest.fn()}
tokenValidationFailed={false}
settings={mockAlmSettingsInstance({ alm: AlmKeys.Azure })}
showPersonalAccessTokenForm={false}
submittingToken={false}
{...overrides}
/>
);
}

+ 15
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx Прегледај датотеку

@@ -39,14 +39,27 @@ it('should correctly pass the selected mode up', () => {
const onSelectMode = jest.fn();
const wrapper = shallowRender({ onSelectMode });

const almButton = 'button.create-project-mode-type-alm';

click(wrapper.find('button.create-project-mode-type-manual'));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.Manual);
onSelectMode.mockClear();

click(wrapper.find(almButton).at(0));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.AzureDevOps);
onSelectMode.mockClear();

click(wrapper.find('button.create-project-mode-type-alm').at(0));
click(wrapper.find(almButton).at(1));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.BitbucketServer);
onSelectMode.mockClear();

click(wrapper.find('button.create-project-mode-type-alm').at(1));
click(wrapper.find(almButton).at(2));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitHub);
onSelectMode.mockClear();

click(wrapper.find(almButton).at(3));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitLab);
onSelectMode.mockClear();
});

function shallowRender(

+ 8
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx Прегледај датотеку

@@ -50,6 +50,14 @@ it('should render correctly if the manual method is selected', () => {
).toMatchSnapshot();
});

it('should render correctly if the Azure method is selected', () => {
expect(
shallowRender({
location: mockLocation({ query: { mode: CreateProjectModes.AzureDevOps } })
})
).toMatchSnapshot();
});

it('should render correctly if the BBS method is selected', () => {
expect(
shallowRender({

+ 229
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap Прегледај датотеку

@@ -0,0 +1,229 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<div
className="boxed-group abs-width-600"
>
<div
className="boxed-group-inner"
>
<h2>
onboarding.create_project.pat_form.title.azure
</h2>
<div
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions.azure"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"link": <a
className="link-with-icon"
href="http://www.example.com/_usersSettings/tokens"
rel="noopener noreferrer"
target="_blank"
>
<DetachIcon
className="little-spacer-right"
/>
<span>
onboarding.create_project.pat_help.instructions.link.azure
</span>
</a>,
"scope": <strong>
<em>
Code (Read & Write)
</em>
</strong>,
}
}
/>
</div>
<form
onSubmit={[Function]}
>
<ValidationInput
error="onboarding.create_project.pat_form.pat_required"
id="personal_access_token"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
required={true}
>
<input
autoFocus={true}
className="width-100 little-spacer-bottom"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
onboarding.create_project.pat_form.list_repositories
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
</div>
</div>
`;

exports[`should render correctly: submitting 1`] = `
<div
className="boxed-group abs-width-600"
>
<div
className="boxed-group-inner"
>
<h2>
onboarding.create_project.pat_form.title.azure
</h2>
<div
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions.azure"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"link": <a
className="link-with-icon"
href="http://www.example.com/_usersSettings/tokens"
rel="noopener noreferrer"
target="_blank"
>
<DetachIcon
className="little-spacer-right"
/>
<span>
onboarding.create_project.pat_help.instructions.link.azure
</span>
</a>,
"scope": <strong>
<em>
Code (Read & Write)
</em>
</strong>,
}
}
/>
</div>
<form
onSubmit={[Function]}
>
<ValidationInput
error="onboarding.create_project.pat_form.pat_required"
id="personal_access_token"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
required={true}
>
<input
autoFocus={true}
className="width-100 little-spacer-bottom"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
onboarding.create_project.pat_form.list_repositories
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={true}
/>
</form>
</div>
</div>
`;

exports[`should render correctly: validation failed 1`] = `
<div
className="boxed-group abs-width-600"
>
<div
className="boxed-group-inner"
>
<h2>
onboarding.create_project.pat_form.title.azure
</h2>
<div
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions.azure"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"link": <a
className="link-with-icon"
href="http://www.example.com/_usersSettings/tokens"
rel="noopener noreferrer"
target="_blank"
>
<DetachIcon
className="little-spacer-right"
/>
<span>
onboarding.create_project.pat_help.instructions.link.azure
</span>
</a>,
"scope": <strong>
<em>
Code (Read & Write)
</em>
</strong>,
}
}
/>
</div>
<form
onSubmit={[Function]}
>
<ValidationInput
error="onboarding.create_project.pat_form.pat_required"
id="personal_access_token"
isInvalid={true}
isValid={false}
label="onboarding.create_project.enter_pat"
required={true}
>
<input
autoFocus={true}
className="width-100 little-spacer-bottom is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
onboarding.create_project.pat_form.list_repositories
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
</div>
</div>
`;

+ 17
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap Прегледај датотеку

@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<AzureProjectCreateRenderer
canAdmin={true}
loading={true}
onPersonalAccessTokenCreate={[Function]}
settings={
Object {
"alm": "azure",
"key": "foo",
}
}
showPersonalAccessTokenForm={true}
tokenValidationFailed={false}
/>
`;

+ 104
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap Прегледај датотеку

@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: loading 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/azure.svg"
/>
onboarding.create_project.azure.title
</span>
}
/>
<i
className="spinner"
/>
</Fragment>
`;

exports[`should render correctly: no settings 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/azure.svg"
/>
onboarding.create_project.azure.title
</span>
}
/>
<WrongBindingCountAlert
alm="azure"
canAdmin={true}
/>
</Fragment>
`;

exports[`should render correctly: project list 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/azure.svg"
/>
onboarding.create_project.azure.title
</span>
}
/>
<AzureProjectsList />
</Fragment>
`;

exports[`should render correctly: token form 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/azure.svg"
/>
onboarding.create_project.azure.title
</span>
}
/>
<div
className="display-flex-justify-center"
>
<AzurePersonalAccessTokenForm
almSetting={
Object {
"alm": "azure",
"key": "key",
}
}
onPersonalAccessTokenCreate={[MockFunction]}
submitting={false}
validationFailed={false}
/>
</div>
</Fragment>
`;

+ 85
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap Прегледај датотеку

@@ -35,6 +35,37 @@ exports[`should render correctly: default 1`] = `
onboarding.create_project.select_method.manual
</div>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<img
alt=""
height={80}
src="/images/alm/azure.svg"
/>
<div
className="medium big-spacer-top"
>
onboarding.create_project.select_method.azure
</div>
<div
className="text-muted small spacer-top"
style={
Object {
"lineHeight": 1.5,
}
}
>
onboarding.create_project.alm_not_configured
<HelpTooltip
className="little-spacer-left"
overlay="onboarding.create_project.zero_alm_instances.azure"
/>
</div>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm"
disabled={false}
@@ -153,6 +184,37 @@ exports[`should render correctly: invalid configs 1`] = `
onboarding.create_project.select_method.manual
</div>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<img
alt=""
height={80}
src="/images/alm/azure.svg"
/>
<div
className="medium big-spacer-top"
>
onboarding.create_project.select_method.azure
</div>
<div
className="text-muted small spacer-top"
style={
Object {
"lineHeight": 1.5,
}
}
>
onboarding.create_project.alm_not_configured
<HelpTooltip
className="little-spacer-left"
overlay="onboarding.create_project.zero_alm_instances.azure"
/>
</div>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
disabled={true}
@@ -286,6 +348,29 @@ exports[`should render correctly: loading instances 1`] = `
onboarding.create_project.select_method.manual
</div>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<img
alt=""
height={80}
src="/images/alm/azure.svg"
/>
<div
className="medium big-spacer-top"
>
onboarding.create_project.select_method.azure
</div>
<span>
onboarding.create_project.check_alm_supported
<i
className="little-spacer-left spinner"
/>
</span>
</button>
<button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
disabled={true}

+ 38
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap Прегледај датотеку

@@ -53,6 +53,44 @@ exports[`should render correctly if no branch support 1`] = `
</Fragment>
`;

exports[`should render correctly if the Azure method is selected 1`] = `
<Fragment>
<Helmet
defer={true}
encodeSpecialCharacters={true}
title="my_account.create_new.TRK"
titleTemplate="%s"
/>
<A11ySkipTarget
anchor="create_project_main"
/>
<div
className="page page-limited huge-spacer-bottom position-relative"
id="create-project"
>
<AzureProjectCreate
canAdmin={false}
loadingBindings={true}
location={
Object {
"action": "PUSH",
"hash": "",
"key": "key",
"pathname": "/path",
"query": Object {
"mode": "azure",
},
"search": "",
"state": Object {},
}
}
onProjectCreate={[Function]}
settings={Array []}
/>
</div>
</Fragment>
`;

exports[`should render correctly if the BBS method is selected 1`] = `
<Fragment>
<Helmet

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/types.ts Прегледај датотеку

@@ -19,6 +19,7 @@
*/
export enum CreateProjectModes {
Manual = 'manual',
AzureDevOps = 'azure',
BitbucketServer = 'bitbucket',
GitHub = 'github',
GitLab = 'gitlab'

+ 13
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Прегледај датотеку

@@ -1854,6 +1854,7 @@ my_account.create_new.VW=Create Portfolio
my_account.create_new.APP=Create Application
my_account.add_project=Add Project
my_account.add_project.manual=Manually
my_account.add_project.azure=Azure DevOps
my_account.add_project.bitbucket=Bitbucket
my_account.add_project.github=GitHub
my_account.add_project.gitlab=GitLab
@@ -3202,6 +3203,7 @@ footer.web_api=Web API
# ONBOARDING
#
#------------------------------------------------------------------------------
onboarding.alm.azure=Azure DevOps Server
onboarding.alm.bitbucket=Bitbucket Server
onboarding.alm.gitlab=GitLab

@@ -3211,6 +3213,7 @@ onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to int

onboarding.create_project.setup_manually=Create a project
onboarding.create_project.select_method.manual=Manually
onboarding.create_project.select_method.azure=From Azure DevOps Server
onboarding.create_project.select_method.bitbucket=From Bitbucket Server
onboarding.create_project.select_method.github=From GitHub
onboarding.create_project.select_method.gitlab=From GitLab
@@ -3239,10 +3242,14 @@ onboarding.create_project.from_bbs=Create a project from Bitbucket Server

onboarding.create_application.key.description=If specified, this value is used as the key instead of generating it from the name of the Application. Only letters, digits, dashes and underscores can be used.

onboarding.create_project.pat_form.title.azure=Allow SonarQube to access and list your Azure DevOps Server repositories
onboarding.create_project.pat_form.title.bitbucket=Grant access to your repositories
onboarding.create_project.pat_form.title.gitlab=Grant access to your projects
onboarding.create_project.pat_form.help.azure=SonarQube needs a personal access token to access and list your repositories from Azure DevOps Server.
onboarding.create_project.pat_form.help.bitbucket=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server.
onboarding.create_project.pat_form.help.gitlab=SonarQube needs a personal access token to access and list your projects from GitLab.
onboarding.create_project.pat_form.pat_required=Please enter a personal access token
onboarding.create_project.pat_form.list_repositories=List repositories
onboarding.create_project.select_method=How do you want to create your project?
onboarding.create_project.too_many_alm_instances.bitbucket=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
onboarding.create_project.too_many_alm_instances.github=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
@@ -3253,10 +3260,14 @@ onboarding.create_project.zero_alm_instances.gitlab=You must first configure a G
onboarding.create_project.wrong_binding_count=You must have exactly 1 {alm} instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
onboarding.create_project.wrong_binding_count.admin=You must have exactly 1 {alm} instance configured in order to use this method. You can configure instances under {url}.
onboarding.create_project.enter_pat=Enter personal access token
onboarding.create_project.pat_incorrect.azure=Your personal access couldn't be validated.
onboarding.create_project.pat_incorrect.bitbucket=Your personal access couldn't be validated.
onboarding.create_project.pat_incorrect.gitlab=Your personal access couldn't be validated. Please make sure it has the right scope and that it is not expired.
onboarding.create_project.pat_help.title=How to create a personal access token?

onboarding.create_project.pat_help.instructions.azure=Create and provide an Azure DevOps Server {link}. You need to select the {scope} scope so we can display a list of your repositories which are available for analysis.
onboarding.create_project.pat_help.instructions.link.azure=personal access token

onboarding.create_project.pat_help.instructions=Click the following link to generate a token in {alm}, and copy-paste it into the personal access token field.
onboarding.create_project.pat_help.instructions2.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
onboarding.create_project.pat_help.link=Create personal access token
@@ -3274,6 +3285,8 @@ onboarding.create_project.no_bbs_repos.filter=No repositories match your filter.
onboarding.create_project.only_showing_X_first_repos=We're only displaying the first {0} repositories. If you're looking for a repository that's not in this list, use the search above.
onboarding.create_project.import_selected_repo=Set up selected repository
onboarding.create_project.go_to_project=Go to project

onboarding.create_project.azure.title=Which Azure DevOps Server repository do you want to set up?
onboarding.create_project.github.title=Which GitHub repository do you want to set up?
onboarding.create_project.github.choose_organization=Choose organization
onboarding.create_project.github.warning.title=Could not connect to GitHub

Loading…
Откажи
Сачувај