Quellcode durchsuchen

SONAR-14801 Adding App password form for Bitbucket cloud onboarding

tags/9.0.0.45539
Mathieu Suen vor 3 Jahren
Ursprung
Commit
ac3d6f0496
27 geänderte Dateien mit 1202 neuen und 754 gelöschten Zeilen
  1. 8
    2
      server/sonar-web/src/main/js/api/alm-integrations.ts
  2. 97
    0
      server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx
  3. 76
    0
      server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx
  4. 37
    67
      server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
  5. 5
    8
      server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
  6. 18
    0
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  7. 25
    81
      server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
  8. 6
    10
      server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
  9. 263
    107
      server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
  10. 65
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreate-test.tsx
  11. 49
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx
  12. 24
    42
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
  13. 2
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx
  14. 13
    106
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
  15. 2
    5
      server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
  16. 95
    36
      server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx
  17. 33
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap
  18. 101
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap
  19. 55
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
  20. 2
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
  21. 13
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  22. 3
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
  23. 2
    35
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
  24. 184
    245
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap
  25. 1
    0
      server/sonar-web/src/main/js/apps/create/project/types.ts
  26. 10
    0
      server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
  27. 13
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 8
- 2
server/sonar-web/src/main/js/api/alm-integrations.ts Datei anzeigen

@@ -30,8 +30,14 @@ import {
} from '../types/alm-integration';
import { ProjectBase } from './components';

export function setAlmPersonalAccessToken(almSetting: string, pat: string): Promise<void> {
return post('/api/alm_integrations/set_pat', { almSetting, pat }).catch(throwGlobalError);
export function setAlmPersonalAccessToken(
almSetting: string,
pat: string,
username?: string
): Promise<void> {
return post('/api/alm_integrations/set_pat', { almSetting, pat, username }).catch(
throwGlobalError
);
}

export function checkPersonalAccessTokenIsValid(

+ 97
- 0
server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx Datei anzeigen

@@ -0,0 +1,97 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { BitbucketProjectRepositories, BitbucketRepository } from '../../../types/alm-integration';
import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender';

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

interface State {
settings: AlmSettingsInstance;
loading: boolean;
projectRepositories?: BitbucketProjectRepositories;
searchResults?: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
showPersonalAccessTokenForm: boolean;
}

export default class BitbucketCloudProjectCreate 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,
showPersonalAccessTokenForm: true
};
}

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

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

handlePersonalAccessTokenCreated = async () => {
this.setState({ showPersonalAccessTokenForm: false });
this.cleanUrl();
await this.fetchData();
};

cleanUrl = () => {
const { location, router } = this.props;
delete location.query.resetPat;
router.replace(location);
};

async fetchData() {}

render() {
const { canAdmin, loadingBindings, location } = this.props;
const { settings, loading, showPersonalAccessTokenForm } = this.state;
return (
<BitbucketCloudProjectCreateRenderer
settings={settings}
canAdmin={canAdmin}
loading={loading || loadingBindings}
onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
resetPat={Boolean(location.query.resetPat)}
showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat)
}
/>
);
}
}

+ 76
- 0
server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx Datei anzeigen

@@ -0,0 +1,76 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 CreateProjectPageHeader from './CreateProjectPageHeader';
import PersonalAccessTokenForm from './PersonalAccessTokenForm';
import WrongBindingCountAlert from './WrongBindingCountAlert';

export interface BitbucketCloudProjectCreateRendererProps {
settings?: AlmSettingsInstance;
canAdmin?: boolean;
loading: boolean;
onPersonalAccessTokenCreated: () => void;
resetPat: boolean;
showPersonalAccessTokenForm: boolean;
}

export default function BitbucketCloudProjectCreateRenderer(
props: BitbucketCloudProjectCreateRendererProps
) {
const { settings, canAdmin, loading, resetPat, showPersonalAccessTokenForm } = 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/bitbucket.svg`}
/>
{translate('onboarding.create_project.bitbucketcloud.title')}
</span>
}
/>
{loading && <i className="spinner" />}

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

{!loading &&
settings &&
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
almSetting={settings}
resetPat={resetPat}
onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
/>
) : (
<p>Placeholder for next step</p>
))}
</>
);
}

+ 37
- 67
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx Datei anzeigen

@@ -20,12 +20,10 @@
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import {
checkPersonalAccessTokenIsValid,
getBitbucketServerProjects,
getBitbucketServerRepositories,
importBitbucketServerProject,
searchForBitbucketServerRepositories,
setAlmPersonalAccessToken
searchForBitbucketServerRepositories
} from '../../../api/alm-integrations';
import {
BitbucketProject,
@@ -36,7 +34,7 @@ import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
import { DEFAULT_BBS_PAGE_SIZE } from './constants';

interface Props extends Pick<WithRouterProps, 'location'> {
interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
canAdmin: boolean;
bitbucketSettings: AlmSettingsInstance[];
loadingBindings: boolean;
@@ -47,14 +45,12 @@ interface State {
bitbucketSetting?: AlmSettingsInstance;
importing: boolean;
loading: boolean;
patIsValid?: boolean;
projects?: BitbucketProject[];
projectRepositories?: BitbucketProjectRepositories;
searching: boolean;
searchResults?: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
submittingToken?: boolean;
tokenValidationFailed: boolean;
showPersonalAccessTokenForm: boolean;
}

export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
@@ -69,13 +65,12 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
importing: false,
loading: false,
searching: false,
tokenValidationFailed: false
showPersonalAccessTokenForm: true
};
}

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

componentDidUpdate(prevProps: Props) {
@@ -91,38 +86,27 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
}

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

const patIsValid = await this.checkPersonalAccessToken().catch(() => false);
if (!showPersonalAccessTokenForm) {
this.setState({ loading: true });
const projects = await this.fetchBitbucketProjects().catch(() => undefined);

let projects;
if (patIsValid) {
projects = await this.fetchBitbucketProjects().catch(() => undefined);
}

let projectRepositories;
if (projects && projects.length > 0) {
projectRepositories = await this.fetchBitbucketRepositories(projects).catch(() => undefined);
}

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

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

if (!bitbucketSetting) {
return Promise.resolve(false);
let projectRepositories;
if (projects && projects.length > 0) {
projectRepositories = await this.fetchBitbucketRepositories(projects).catch(
() => undefined
);
}

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

return checkPersonalAccessTokenIsValid(bitbucketSetting.key).then(({ status }) => status);
};

fetchBitbucketProjects = (): Promise<BitbucketProject[] | undefined> => {
@@ -184,29 +168,16 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
});
};

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

if (!bitbucketSetting || token.length < 1) {
return;
}
cleanUrl = () => {
const { location, router } = this.props;
delete location.query.resetPat;
router.replace(location);
};

this.setState({ submittingToken: true, tokenValidationFailed: false });
setAlmPersonalAccessToken(bitbucketSetting.key, token)
.then(this.checkPersonalAccessToken)
.then(patIsValid => {
if (this.mounted) {
this.setState({ submittingToken: false, patIsValid, tokenValidationFailed: !patIsValid });
if (patIsValid) {
this.fetchInitialData();
}
}
})
.catch(() => {
if (this.mounted) {
this.setState({ submittingToken: false });
}
});
handlePersonalAccessTokenCreated = async () => {
this.setState({ showPersonalAccessTokenForm: false });
this.cleanUrl();
await this.fetchInitialData();
};

handleImportRepository = () => {
@@ -271,14 +242,12 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
bitbucketSetting,
importing,
loading,
patIsValid,
projectRepositories,
projects,
searching,
searchResults,
selectedRepository,
submittingToken,
tokenValidationFailed
showPersonalAccessTokenForm
} = this.state;

return (
@@ -288,18 +257,19 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
importing={importing}
loading={loading || loadingBindings}
onImportRepository={this.handleImportRepository}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
onProjectCreate={this.props.onProjectCreate}
onSearch={this.handleSearch}
onSelectRepository={this.handleSelectRepository}
projectRepositories={projectRepositories}
projects={projects}
resetPat={Boolean(location.query.resetPat)}
searchResults={searchResults}
searching={searching}
selectedRepository={selectedRepository}
showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken}
tokenValidationFailed={tokenValidationFailed}
showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat)
}
/>
);
}

+ 5
- 8
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx Datei anzeigen

@@ -41,16 +41,15 @@ export interface BitbucketProjectCreateRendererProps {
onImportRepository: () => void;
onSearch: (query: string) => void;
onSelectRepository: (repo: BitbucketRepository) => void;
onPersonalAccessTokenCreate: (token: string) => void;
onPersonalAccessTokenCreated: () => void;
onProjectCreate: (projectKeys: string[]) => void;
projects?: BitbucketProject[];
projectRepositories?: BitbucketProjectRepositories;
resetPat: boolean;
searching: boolean;
searchResults?: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
showPersonalAccessTokenForm?: boolean;
submittingToken?: boolean;
tokenValidationFailed: boolean;
}

export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCreateRendererProps) {
@@ -65,8 +64,7 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
searching,
searchResults,
showPersonalAccessTokenForm,
submittingToken,
tokenValidationFailed
resetPat
} = props;

return (
@@ -109,9 +107,8 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
almSetting={bitbucketSetting}
onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
submitting={submittingToken}
validationFailed={tokenValidationFailed}
onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
resetPat={resetPat}
/>
) : (
<BitbucketImportRepositoryForm

+ 18
- 0
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx Datei anzeigen

@@ -28,6 +28,7 @@ import { withAppState } from '../../../components/hoc/withAppState';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import AzureProjectCreate from './AzureProjectCreate';
import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate';
import BitbucketProjectCreate from './BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './GitHubProjectCreate';
@@ -44,6 +45,7 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
interface State {
azureSettings: AlmSettingsInstance[];
bitbucketSettings: AlmSettingsInstance[];
bitbucketCloudSettings: AlmSettingsInstance[];
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
@@ -54,6 +56,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
state: State = {
azureSettings: [],
bitbucketSettings: [],
bitbucketCloudSettings: [],
githubSettings: [],
gitlabSettings: [],
loading: true
@@ -76,6 +79,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
this.setState({
azureSettings: almSettings.filter(s => s.alm === AlmKeys.Azure),
bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.BitbucketServer),
bitbucketCloudSettings: almSettings.filter(s => s.alm === AlmKeys.BitbucketCloud),
githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab),
loading: false
@@ -112,6 +116,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
const {
azureSettings,
bitbucketSettings,
bitbucketCloudSettings,
githubSettings,
gitlabSettings,
loading
@@ -138,6 +143,19 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
router={router}
/>
);
}
case CreateProjectModes.BitbucketCloud: {
return (
<BitbucketCloudProjectCreate
canAdmin={!!canAdmin}
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
router={router}
settings={bitbucketCloudSettings}
/>
);
}

+ 25
- 81
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx Datei anzeigen

@@ -19,15 +19,9 @@
*/
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import { translate } from 'sonar-ui-common/helpers/l10n';
import {
checkPersonalAccessTokenIsValid,
getGitlabProjects,
importGitlabProject,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
import { getGitlabProjects, importGitlabProject } from '../../../api/alm-integrations';
import { GitlabProject } from '../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { AlmSettingsInstance } from '../../../types/alm-settings';
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';

interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
@@ -43,12 +37,11 @@ interface State {
loadingMore: boolean;
projects?: GitlabProject[];
projectsPaging: T.Paging;
submittingToken: boolean;
tokenIsValid: boolean;
tokenValidationErrorMessage?: string;
resetPat: boolean;
searching: boolean;
searchQuery: string;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm: boolean;
}

const GITLAB_PROJECTS_PAGESIZE = 30;
@@ -63,17 +56,16 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loading: false,
loadingMore: false,
projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE },
tokenIsValid: false,
resetPat: false,
showPersonalAccessTokenForm: true,
searching: false,
searchQuery: '',
settings: props.settings.length === 1 ? props.settings[0] : undefined,
submittingToken: false
settings: props.settings.length === 1 ? props.settings[0] : undefined
};
}

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

componentDidUpdate(prevProps: Props) {
@@ -90,50 +82,30 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
}

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

const { status, error } = await this.checkPersonalAccessToken();

let result;
if (status) {
result = await this.fetchProjects();
}

if (this.mounted) {
if (result) {
if (!showPersonalAccessTokenForm) {
this.setState({ loading: true });
const result = await this.fetchProjects();
if (this.mounted && result) {
const { projects, projectsPaging } = result;

this.setState({
tokenIsValid: status,
loading: false,
projects,
projectsPaging
});
} else {
this.setState({
loading: false,
tokenValidationErrorMessage: !status ? error : undefined
loading: false
});
}
}
};

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

if (!settings) {
return Promise.resolve({
status: false,
error: translate('onboarding.create_project.pat_incorrect', AlmKeys.GitLab)
});
}

return checkPersonalAccessTokenIsValid(settings.key);
};

handleError = () => {
if (this.mounted) {
this.setState({ tokenIsValid: false });
this.setState({ resetPat: true, showPersonalAccessTokenForm: true });
}

return undefined;
@@ -141,7 +113,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat

fetchProjects = async (pageIndex = 1, query?: string) => {
const { settings } = this.state;

if (!settings) {
return Promise.resolve(undefined);
}
@@ -228,37 +199,10 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
router.replace(location);
};

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

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

this.setState({ submittingToken: true, tokenValidationErrorMessage: undefined });

try {
await setAlmPersonalAccessToken(settings.key, token);

const { status, error } = await this.checkPersonalAccessToken();

if (this.mounted) {
this.setState({
submittingToken: false,
tokenIsValid: status,
tokenValidationErrorMessage: error
});

if (status) {
this.cleanUrl();
await this.fetchInitialData();
}
}
} catch (e) {
if (this.mounted) {
this.setState({ submittingToken: false });
}
}
handlePersonalAccessTokenCreated = async () => {
this.setState({ showPersonalAccessTokenForm: false, resetPat: false });
this.cleanUrl();
await this.fetchInitialData();
};

render() {
@@ -269,12 +213,11 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loadingMore,
projects,
projectsPaging,
tokenIsValid,
resetPat,
searching,
searchQuery,
settings,
submittingToken,
tokenValidationErrorMessage
showPersonalAccessTokenForm
} = this.state;

return (
@@ -286,15 +229,16 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loadingMore={loadingMore}
onImport={this.handleImport}
onLoadMore={this.handleLoadMore}
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
onSearch={this.handleSearch}
projects={projects}
projectsPaging={projectsPaging}
resetPat={resetPat || Boolean(location.query.resetPat)}
searching={searching}
searchQuery={searchQuery}
showPersonalAccessTokenForm={!tokenIsValid || Boolean(location.query.resetPat)}
submittingToken={submittingToken}
tokenValidationErrorMessage={tokenValidationErrorMessage}
showPersonalAccessTokenForm={
showPersonalAccessTokenForm || Boolean(location.query.resetPat)
}
/>
);
}

+ 6
- 10
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx Datei anzeigen

@@ -34,16 +34,15 @@ export interface GitlabProjectCreateRendererProps {
loadingMore: boolean;
onImport: (gitlabProjectId: string) => void;
onLoadMore: () => void;
onPersonalAccessTokenCreate: (pat: string) => void;
onPersonalAccessTokenCreated: () => void;
onSearch: (searchQuery: string) => void;
projects?: GitlabProject[];
projectsPaging: T.Paging;
resetPat: boolean;
searching: boolean;
searchQuery: string;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
submittingToken?: boolean;
tokenValidationErrorMessage?: string;
}

export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
@@ -54,12 +53,11 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
loadingMore,
projects,
projectsPaging,
resetPat,
searching,
searchQuery,
settings,
showPersonalAccessTokenForm,
submittingToken,
tokenValidationErrorMessage
showPersonalAccessTokenForm
} = props;

return (
@@ -89,10 +87,8 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
almSetting={settings}
onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
submitting={submittingToken}
validationFailed={Boolean(tokenValidationErrorMessage)}
validationErrorMessage={tokenValidationErrorMessage}
resetPat={resetPat}
onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
/>
) : (
<GitlabProjectSelectionForm

+ 263
- 107
server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx Datei anzeigen

@@ -26,19 +26,33 @@ import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import {
checkPersonalAccessTokenIsValid,
setAlmPersonalAccessToken
} from '../../../api/alm-integrations';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';

export interface PersonalAccessTokenFormProps {
interface Props {
almSetting: AlmSettingsInstance;
onPersonalAccessTokenCreate: (token: string) => void;
submitting?: boolean;
resetPat: boolean;
onPersonalAccessTokenCreated: () => void;
}

interface State {
validationFailed: boolean;
validationErrorMessage?: string;
touched: boolean;
password: string;
username?: string;
submitting: boolean;
checkingPat: boolean;
}

function getPatUrl(alm: AlmKeys, url: string) {
function getPatUrl(alm: AlmKeys, url = '') {
if (alm === AlmKeys.BitbucketServer) {
return `${url.replace(/\/$/, '')}/plugins/servlet/access-tokens/add`;
} else if (alm === AlmKeys.BitbucketCloud) {
return 'https://bitbucket.org/account/settings/app-passwords/new';
} else {
// GitLab
return url.endsWith('/api/v4')
@@ -47,97 +61,236 @@ function getPatUrl(alm: AlmKeys, url: string) {
}
}

export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormProps) {
const {
almSetting: { alm, url },
submitting = false,
validationFailed,
validationErrorMessage
} = props;
const [touched, setTouched] = React.useState(false);

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

const isInvalid = validationFailed && !touched;
const errorMessage =
validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect', alm);

return (
<div className="display-flex-start">
<form
className="width-50"
onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
const value = new FormData(e.currentTarget).get('personal_access_token') as string;
props.onPersonalAccessTokenCreate(value);
}}>
<h2 className="big">{translate('onboarding.create_project.pat_form.title', alm)}</h2>
<p className="big-spacer-top big-spacer-bottom">
{translate('onboarding.create_project.pat_form.help', alm)}
</p>

<ValidationInput
error={isInvalid ? errorMessage : undefined}
id="personal_access_token"
isInvalid={isInvalid}
isValid={false}
label={translate('onboarding.create_project.enter_pat')}
required={true}>
<input
autoFocus={true}
className={classNames('input-super-large', {
'is-invalid': isInvalid
})}
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={() => {
setTouched(true);
}}
type="text"
/>
</ValidationInput>

<SubmitButton disabled={isInvalid || submitting || !touched}>
{translate('save')}
</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>

<Alert className="big-spacer-left width-50" display="block" variant="info">
<h3>{translate('onboarding.create_project.pat_help.title')}</h3>

<p className="big-spacer-top big-spacer-bottom">
<FormattedMessage
id="onboarding.create_project.pat_help.instructions"
defaultMessage={translate('onboarding.create_project.pat_help.instructions')}
values={{ alm: translate('onboarding.alm', alm) }}
/>
</p>

{url && (
<div className="text-middle">
<img
alt="" // Should be ignored by screen readers
className="spacer-right"
height="16"
src={`${getBaseUrl()}/images/alm/${alm}.svg`}
export default class PersonalAccessTokenForm extends React.PureComponent<Props, State> {
mounted = false;

constructor(props: Props) {
super(props);

this.state = {
checkingPat: false,
touched: false,
password: '',
submitting: false,
validationFailed: false
};
}

async componentDidMount() {
const {
almSetting: { key },
resetPat
} = this.props;
this.mounted = true;

// We don't need to check PAT if we want to reset
if (!resetPat) {
this.setState({ checkingPat: true });
const { patIsValid, error } = await checkPersonalAccessTokenIsValid(key)
.then(({ status, error }) => ({ patIsValid: status, error }))
.catch(() => ({ patIsValid: status, error: translate('default_error_message') }));
if (patIsValid) {
this.props.onPersonalAccessTokenCreated();
}
if (this.mounted) {
// This is the initial message when no token was provided
if (error === `personal access token for '${key}' is missing`) {
this.setState({
checkingPat: false
});
} else {
this.setState({
checkingPat: false,
validationFailed: true,
validationErrorMessage: error
});
}
}
}
}

componentWillUnmount() {
this.mounted = false;
}

handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
touched: true,
username: event.target.value
});
};

handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
touched: true,
password: event.target.value
});
};

handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
const { password, username } = this.state;
const {
almSetting: { key }
} = this.props;

e.preventDefault();
if (password) {
this.setState({ submitting: true });

await setAlmPersonalAccessToken(key, password, username).catch(() => {
/* Set will not check pat validity. We need to check again so we will catch issue after */
});

const { status, error } = await checkPersonalAccessTokenIsValid(key)
.then(({ status, error }) => ({ status, error }))
.catch(() => ({ status: false, error: translate('default_error_message') }));

if (this.mounted && status) {
// Let's reset status,
this.setState({
checkingPat: false,
touched: false,
password: '',
submitting: false,
username: '',
validationFailed: false
});
this.props.onPersonalAccessTokenCreated();
} else if (this.mounted) {
this.setState({
submitting: false,
touched: false,
validationFailed: true,
validationErrorMessage: error
});
}
}
};

render() {
const {
almSetting: { alm, url }
} = this.props;
const {
checkingPat,
submitting,
touched,
password,
username,
validationFailed,
validationErrorMessage
} = this.state;

if (checkingPat) {
return <DeferredSpinner className="spacer-left" loading={true} />;
}

const suffixTranslationKey = alm === AlmKeys.BitbucketCloud ? '.bitbucketcloud' : '';

const isInvalid = validationFailed && !touched;
const canSubmit = Boolean(password) && (alm !== AlmKeys.BitbucketCloud || Boolean(username));
const submitButtonDiabled = isInvalid || submitting || !canSubmit;

const errorMessage =
validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect', alm);

return (
<div className="display-flex-start">
<form className="width-50" onSubmit={this.handleSubmit}>
<h2 className="big">{translate('onboarding.create_project.pat_form.title', alm)}</h2>
<p className="big-spacer-top big-spacer-bottom">
{translate('onboarding.create_project.pat_form.help', alm)}
</p>

{alm === AlmKeys.BitbucketCloud && (
<ValidationInput
error={undefined}
id="enter_username_validation"
isInvalid={false}
isValid={false}
label={translate('onboarding.create_project.enter_username')}
required={true}>
<input
autoFocus={true}
className={classNames('input-super-large', {
'is-invalid': isInvalid
})}
id="username"
minLength={1}
name="username"
value={username}
onChange={this.handleUsernameChange}
type="text"
/>
</ValidationInput>
)}

<ValidationInput
error={errorMessage}
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label={translate(`onboarding.create_project.enter_pat${suffixTranslationKey}`)}
required={true}>
<input
autoFocus={alm !== AlmKeys.BitbucketCloud}
className={classNames('input-super-large', {
'is-invalid': isInvalid
})}
id="personal_access_token"
minLength={1}
value={password}
onChange={this.handlePasswordChange}
type="text"
/>
</ValidationInput>

<ValidationInput
error={errorMessage}
id="personal_access_token_submit"
isInvalid={isInvalid}
isValid={false}
label={null}>
<SubmitButton disabled={submitButtonDiabled}>{translate('save')}</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</ValidationInput>
</form>

<Alert className="big-spacer-left width-50" display="block" variant="info">
<h3>{translate(`onboarding.create_project.pat_help${suffixTranslationKey}.title`)}</h3>

<p className="big-spacer-top big-spacer-bottom">
<FormattedMessage
id="onboarding.create_project.pat_help.instructions"
defaultMessage={translate(
`onboarding.create_project.pat_help${suffixTranslationKey}.instructions`
)}
values={{ alm: translate('onboarding.alm', alm) }}
/>
<a href={getPatUrl(alm, url)} rel="noopener noreferrer" target="_blank">
{translate('onboarding.create_project.pat_help.link')}
</a>
</div>
)}

<p className="big-spacer-top big-spacer-bottom">
{translate('onboarding.create_project.pat_help.instructions2', alm)}
</p>

<ul>
{alm === AlmKeys.BitbucketServer && (
<>
</p>

{(url || alm === AlmKeys.BitbucketCloud) && (
<div className="text-middle">
<img
alt="" // Should be ignored by screen readers
className="spacer-right"
height="16"
src={`${getBaseUrl()}/images/alm/${
alm === AlmKeys.BitbucketCloud ? AlmKeys.BitbucketServer : alm
}.svg`}
/>
<a href={getPatUrl(alm, url)} rel="noopener noreferrer" target="_blank">
{translate(`onboarding.create_project.pat_help${suffixTranslationKey}.link`)}
</a>
</div>
)}

<p className="big-spacer-top big-spacer-bottom">
{translate('onboarding.create_project.pat_help.instructions2', alm)}
</p>

<ul>
{alm === AlmKeys.BitbucketServer && (
<li>
<FormattedMessage
defaultMessage={translate(
@@ -153,6 +306,8 @@ export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormPr
}}
/>
</li>
)}
{(alm === AlmKeys.BitbucketServer || alm === AlmKeys.BitbucketCloud) && (
<li>
<FormattedMessage
defaultMessage={translate(
@@ -168,17 +323,18 @@ export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormPr
}}
/>
</li>
</>
)}
{alm === AlmKeys.GitLab && (
<li className="spacer-bottom">
<strong>
{translate('onboarding.create_project.pat_help.gitlab.read_api_permission')}
</strong>
</li>
)}
</ul>
</Alert>
</div>
);
)}

{alm === AlmKeys.GitLab && (
<li className="spacer-bottom">
<strong>
{translate('onboarding.create_project.pat_help.gitlab.read_api_permission')}
</strong>
</li>
)}
</ul>
</Alert>
</div>
);
}
}

+ 65
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreate-test.tsx Datei anzeigen

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { checkPersonalAccessTokenIsValid } from '../../../../api/alm-integrations';
import { mockBitbucketCloudAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import BitbucketCloudProjectCreate from '../BitbucketCloudProjectCreate';

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

it('Should render correctly', async () => {
let wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
(checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('Need App password');
});

it('Should handle app password correctly', async () => {
const wrapper = shallowRender();

await waitAndUpdate(wrapper);
await wrapper.instance().handlePersonalAccessTokenCreated();
expect(wrapper.state().showPersonalAccessTokenForm).toBe(false);
});

function shallowRender(props?: Partial<BitbucketCloudProjectCreate['props']>) {
return shallow<BitbucketCloudProjectCreate>(
<BitbucketCloudProjectCreate
onProjectCreate={jest.fn()}
loadingBindings={false}
location={mockLocation()}
canAdmin={true}
router={mockRouter()}
settings={[mockBitbucketCloudAlmSettingsInstance()]}
{...props}
/>
);
}

+ 49
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx Datei anzeigen

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { mockBitbucketCloudAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import BitbucketCloudProjectCreateRenderer, {
BitbucketCloudProjectCreateRendererProps
} from '../BitbucketCloudProjectCreateRender';

it('Should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ settings: undefined })).toMatchSnapshot('Wrong config');
expect(shallowRender({ loading: true })).toMatchSnapshot('Loading...');
expect(
shallowRender({
showPersonalAccessTokenForm: true
})
).toMatchSnapshot('Need App password');
});

function shallowRender(props?: Partial<BitbucketCloudProjectCreateRendererProps>) {
return shallow(
<BitbucketCloudProjectCreateRenderer
onPersonalAccessTokenCreated={jest.fn()}
loading={false}
settings={mockBitbucketCloudAlmSettingsInstance()}
resetPat={false}
showPersonalAccessTokenForm={false}
{...props}
/>
);
}

+ 24
- 42
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx Datei anzeigen

@@ -21,16 +21,17 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
checkPersonalAccessTokenIsValid,
getBitbucketServerProjects,
getBitbucketServerRepositories,
importBitbucketServerProject,
searchForBitbucketServerRepositories,
setAlmPersonalAccessToken
searchForBitbucketServerRepositories
} from '../../../../api/alm-integrations';
import { mockBitbucketRepository } from '../../../../helpers/mocks/alm-integrations';
import {
mockBitbucketProject,
mockBitbucketRepository
} from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation } from '../../../../helpers/testMocks';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
import BitbucketProjectCreate from '../BitbucketProjectCreate';

@@ -39,7 +40,6 @@ jest.mock('../../../../api/alm-integrations', () => {
'../../../../helpers/mocks/alm-integrations'
);
return {
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
getBitbucketServerProjects: jest.fn().mockResolvedValue({
projects: [
mockBitbucketProject({ key: 'project1', name: 'Project 1' }),
@@ -53,7 +53,6 @@ jest.mock('../../../../api/alm-integrations', () => {
]
}),
importBitbucketServerProject: jest.fn().mockResolvedValue({ project: { key: 'baz' } }),
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null),
searchForBitbucketServerRepositories: jest.fn().mockResolvedValue({
repositories: [
mockBitbucketRepository(),
@@ -65,50 +64,21 @@ jest.mock('../../../../api/alm-integrations', () => {

beforeEach(jest.clearAllMocks);

it('should render correctly', () => {
it('should render correctly', async () => {
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({ status: true });
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().patIsValid).toBe(true);
});
expect(shallowRender({ bitbucketSettings: [] })).toMatchSnapshot('No setting');

it('should correctly handle an invalid PAT', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: 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({ status: false });
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().submittingToken).toBe(false);
expect(wrapper.state().tokenValidationFailed).toBe(true);
(getBitbucketServerRepositories as jest.Mock).mockRejectedValueOnce({});
await wrapper.instance().handlePersonalAccessTokenCreated();
expect(wrapper).toMatchSnapshot('No repository');
});

it('should correctly fetch projects and repos', async () => {
const wrapper = shallowRender();
await wrapper.instance().handlePersonalAccessTokenCreated();

// Opens first project on mount.
await waitAndUpdate(wrapper);
expect(getBitbucketServerProjects).toBeCalledWith('foo');
expect(wrapper.state().projects).toHaveLength(2);

@@ -159,6 +129,17 @@ it('should correctly handle search', async () => {
expect(wrapper.state().searchResults).toHaveLength(2);
});

it('should behave correctly when no setting', async () => {
const wrapper = shallowRender({ bitbucketSettings: [] });
await wrapper.instance().handleSearch('');
await wrapper.instance().handleImportRepository();
await wrapper.instance().fetchBitbucketRepositories([mockBitbucketProject()]);

expect(searchForBitbucketServerRepositories).not.toHaveBeenCalled();
expect(importBitbucketServerProject).not.toHaveBeenCalled();
expect(getBitbucketServerRepositories).not.toHaveBeenCalled();
});

function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
return shallow<BitbucketProjectCreate>(
<BitbucketProjectCreate
@@ -166,6 +147,7 @@ function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.BitbucketServer, key: 'foo' })]}
loadingBindings={false}
location={mockLocation()}
router={mockRouter()}
onProjectCreate={jest.fn()}
{...props}
/>

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx Datei anzeigen

@@ -52,14 +52,14 @@ function shallowRender(props: Partial<BitbucketProjectCreateRendererProps> = {})
importing={false}
loading={false}
onImportRepository={jest.fn()}
onPersonalAccessTokenCreate={jest.fn()}
onPersonalAccessTokenCreated={jest.fn()}
onProjectCreate={jest.fn()}
onSearch={jest.fn()}
onSelectRepository={jest.fn()}
projectRepositories={{ foo: { allShown: true, repositories: [mockBitbucketRepository()] } }}
projects={[mockBitbucketProject({ key: 'foo' })]}
resetPat={false}
searching={false}
tokenValidationFailed={false}
{...props}
/>
);

+ 13
- 106
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx Datei anzeigen

@@ -20,12 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
checkPersonalAccessTokenIsValid,
getGitlabProjects,
importGitlabProject,
setAlmPersonalAccessToken
} from '../../../../api/alm-integrations';
import { getGitlabProjects, importGitlabProject } from '../../../../api/alm-integrations';
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
@@ -33,8 +28,6 @@ import { AlmKeys } from '../../../../types/alm-settings';
import GitlabProjectCreate from '../GitlabProjectCreate';

jest.mock('../../../../api/alm-integrations', () => ({
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null),
getGitlabProjects: jest.fn().mockRejectedValue('error'),
importGitlabProject: jest.fn().mockRejectedValue('error')
}));
@@ -47,84 +40,7 @@ it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should correctly check PAT on mount', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalledWith(almSettingKey);
});

it('should correctly check PAT when settings are added after mount', async () => {
const wrapper = shallowRender({ settings: [] });
await waitAndUpdate(wrapper);

wrapper.setProps({
settings: [mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: 'otherKey' })]
});

expect(checkPersonalAccessTokenIsValid).toBeCalledWith('otherKey');
});

it('should correctly handle a valid PAT', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });
(getGitlabProjects as jest.Mock).mockResolvedValueOnce({
projects: [mockGitlabProject()],
projectsPaging: {
pageIndex: 1,
pageSize: 10,
total: 1
}
});
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().tokenIsValid).toBe(true);
});

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

describe('setting a new PAT', () => {
const routerReplace = jest.fn();
const wrapper = shallowRender({ router: mockRouter({ replace: routerReplace }) });

beforeEach(() => {
jest.clearAllMocks();
});

it('should correctly handle it if invalid', async () => {
const error = 'error message';
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false, error });

wrapper.instance().handlePersonalAccessTokenCreate('invalidtoken');
expect(setAlmPersonalAccessToken).toBeCalledWith(almSettingKey, 'invalidtoken');
expect(wrapper.state().submittingToken).toBe(true);
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().submittingToken).toBe(false);
expect(wrapper.state().tokenValidationErrorMessage).toBe(error);
});

it('should correctly handle it if valid', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });

wrapper.instance().handlePersonalAccessTokenCreate('validtoken');
expect(setAlmPersonalAccessToken).toBeCalledWith(almSettingKey, 'validtoken');
expect(wrapper.state().submittingToken).toBe(true);
await waitAndUpdate(wrapper);
expect(checkPersonalAccessTokenIsValid).toBeCalled();
expect(wrapper.state().submittingToken).toBe(false);
expect(wrapper.state().tokenValidationErrorMessage).toBeUndefined();

expect(routerReplace).toBeCalled();
});
});

it('should fetch more projects and preserve search', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });

const projects = [
mockGitlabProject({ id: '1' }),
mockGitlabProject({ id: '2' }),
@@ -153,7 +69,7 @@ it('should fetch more projects and preserve search', async () => {

const wrapper = shallowRender();

await waitAndUpdate(wrapper);
await wrapper.instance().handlePersonalAccessTokenCreated();
wrapper.setState({ searchQuery: 'query' });

wrapper.instance().handleLoadMore();
@@ -167,8 +83,6 @@ it('should fetch more projects and preserve search', async () => {
});

it('should search for projects', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });

const projects = [
mockGitlabProject({ id: '1' }),
mockGitlabProject({ id: '2' }),
@@ -197,11 +111,10 @@ it('should search for projects', async () => {
const query = 'query';

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
await wrapper.instance().handlePersonalAccessTokenCreated();

wrapper.instance().handleSearch(query);
expect(wrapper.state().searching).toBe(true);

await waitAndUpdate(wrapper);
expect(wrapper.state().searching).toBe(false);
expect(wrapper.state().searchQuery).toBe(query);
@@ -211,8 +124,6 @@ it('should search for projects', async () => {
});

it('should import', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });

const projects = [mockGitlabProject({ id: '1' }), mockGitlabProject({ id: '2' })];
(getGitlabProjects as jest.Mock).mockResolvedValueOnce({
projects,
@@ -231,7 +142,7 @@ it('should import', async () => {
const onProjectCreate = jest.fn();

const wrapper = shallowRender({ onProjectCreate });
await waitAndUpdate(wrapper);
await wrapper.instance().handlePersonalAccessTokenCreated();

wrapper.instance().handleImport(projects[1].id);
expect(wrapper.state().importingGitlabProjectId).toBe(projects[1].id);
@@ -245,17 +156,13 @@ it('should import', async () => {
it('should do nothing with missing settings', async () => {
const wrapper = shallowRender({ settings: [] });

await waitAndUpdate(wrapper);

wrapper.instance().handleLoadMore();
wrapper.instance().handleSearch('whatever');
wrapper.instance().handlePersonalAccessTokenCreate('token');
wrapper.instance().handleImport('gitlab project id');
await wrapper.instance().handleLoadMore();
await wrapper.instance().handleSearch('whatever');
await wrapper.instance().handlePersonalAccessTokenCreated();
await wrapper.instance().handleImport('gitlab project id');

expect(checkPersonalAccessTokenIsValid).not.toHaveBeenCalled();
expect(getGitlabProjects).not.toHaveBeenCalled();
expect(importGitlabProject).not.toHaveBeenCalled();
expect(setAlmPersonalAccessToken).not.toHaveBeenCalled();
});

it('should handle errors when fetching projects', async () => {
@@ -263,8 +170,10 @@ it('should handle errors when fetching projects', async () => {

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
await wrapper.instance().handlePersonalAccessTokenCreated();

expect(wrapper.state().tokenIsValid).toBe(false);
expect(wrapper.state().resetPat).toBe(true);
expect(wrapper.state().showPersonalAccessTokenForm).toBe(true);
});

it('should handle errors when importing a project', async () => {
@@ -279,14 +188,12 @@ it('should handle errors when importing a project', async () => {
});

const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.state().tokenIsValid).toBe(true);
await wrapper.instance().handlePersonalAccessTokenCreated();

await wrapper.instance().handleImport('gitlabId');
await waitAndUpdate(wrapper);

expect(wrapper.state().tokenIsValid).toBe(false);
expect(wrapper.state().showPersonalAccessTokenForm).toBe(true);
});

function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) {

+ 2
- 5
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx Datei anzeigen

@@ -35,9 +35,6 @@ it('should render correctly', () => {
expect(shallowRender({ showPersonalAccessTokenForm: false })).toMatchSnapshot(
'project selection form'
);
expect(shallowRender({ tokenValidationErrorMessage: 'error' })).toMatchSnapshot(
'pat validation error'
);
});

function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
@@ -48,14 +45,14 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
loadingMore={false}
onImport={jest.fn()}
onLoadMore={jest.fn()}
onPersonalAccessTokenCreate={jest.fn()}
onPersonalAccessTokenCreated={jest.fn()}
onSearch={jest.fn()}
projects={undefined}
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }}
searching={false}
searchQuery=""
resetPat={false}
showPersonalAccessTokenForm={true}
submittingToken={false}
settings={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })}
{...props}
/>

+ 95
- 36
server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx Datei anzeigen

@@ -20,37 +20,59 @@
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 { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
checkPersonalAccessTokenIsValid,
setAlmPersonalAccessToken
} from '../../../../api/alm-integrations';
import {
mockAlmSettingsInstance,
mockBitbucketCloudAlmSettingsInstance
} from '../../../../helpers/mocks/alm-settings';
import { AlmKeys } from '../../../../types/alm-settings';
import PersonalAccessTokenForm, { PersonalAccessTokenFormProps } from '../PersonalAccessTokenForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('bitbucket');
expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed');
expect(
shallowRender({ validationFailed: true, validationErrorMessage: 'error' })
).toMatchSnapshot('validation failed, custom error message');
expect(
shallowRender({
almSetting: mockAlmSettingsInstance({ alm: AlmKeys.GitLab, url: 'https://gitlab.com/api/v4' })
})
).toMatchSnapshot('gitlab');
expect(
shallowRender({
almSetting: mockAlmSettingsInstance({
alm: AlmKeys.GitLab,
url: 'https://gitlabapi.unexpectedurl.org'
})
import PersonalAccessTokenForm from '../PersonalAccessTokenForm';

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

it('should render correctly', async () => {
expect(shallowRender()).toMatchSnapshot('no token needed');

(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
let wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('bitbucket');

(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
wrapper = shallowRender({ almSetting: mockBitbucketCloudAlmSettingsInstance() });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('bitbucket cloud');

(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
wrapper = shallowRender({
almSetting: mockAlmSettingsInstance({ alm: AlmKeys.GitLab, url: 'https://gitlab.com/api/v4' })
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('gitlab');

(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
wrapper = shallowRender({
almSetting: mockAlmSettingsInstance({
alm: AlmKeys.GitLab,
url: 'https://gitlabapi.unexpectedurl.org'
})
).toMatchSnapshot('gitlab with non-standard api path');
});
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('gitlab with non-standard api path');
});

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

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

@@ -60,25 +82,62 @@ it('should correctly handle form interactions', () => {

// Expect correct calls to be made when submitting.
submit(wrapper.find('form'));
expect(onPersonalAccessTokenCreate).toBeCalled();
expect(onPersonalAccessTokenCreated).toBeCalled();
expect(setAlmPersonalAccessToken).toBeCalledWith('key', 'token', undefined);
});

it('should correctly handle form for bitbucket interactions', async () => {
const onPersonalAccessTokenCreated = jest.fn();
const wrapper = shallowRender({
almSetting: mockBitbucketCloudAlmSettingsInstance(),
onPersonalAccessTokenCreated
});

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

// 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 });
change(wrapper.find('#personal_access_token'), 'token');
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);

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

// Expect correct calls to be made when submitting.
submit(wrapper.find('form'));
expect(onPersonalAccessTokenCreated).toBeCalled();
expect(setAlmPersonalAccessToken).toBeCalledWith('key', 'token', 'username');
});

it('should show error when issue', async () => {
(checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
const wrapper = shallowRender({
almSetting: mockBitbucketCloudAlmSettingsInstance()
});

await waitAndUpdate(wrapper);

(checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});

change(wrapper.find('#personal_access_token'), 'token');
change(wrapper.find('#username'), 'username');

// Expect correct calls to be made when submitting.
submit(wrapper.find('form'));
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('issue submitting token');
});

function shallowRender(props: Partial<PersonalAccessTokenFormProps> = {}) {
return shallow<PersonalAccessTokenFormProps>(
function shallowRender(props: Partial<PersonalAccessTokenForm['props']> = {}) {
return shallow<PersonalAccessTokenForm>(
<PersonalAccessTokenForm
almSetting={mockAlmSettingsInstance({
alm: AlmKeys.BitbucketServer,
url: 'http://www.example.com'
})}
onPersonalAccessTokenCreate={jest.fn()}
validationFailed={false}
onPersonalAccessTokenCreated={jest.fn()}
resetPat={false}
{...props}
/>
);

+ 33
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap Datei anzeigen

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

exports[`Should render correctly 1`] = `
<BitbucketCloudProjectCreateRenderer
canAdmin={true}
loading={false}
onPersonalAccessTokenCreated={[Function]}
resetPat={false}
settings={
Object {
"alm": "bitbucketcloud",
"key": "key",
}
}
showPersonalAccessTokenForm={true}
/>
`;

exports[`Should render correctly: Need App password 1`] = `
<BitbucketCloudProjectCreateRenderer
canAdmin={true}
loading={false}
onPersonalAccessTokenCreated={[Function]}
resetPat={false}
settings={
Object {
"alm": "bitbucketcloud",
"key": "key",
}
}
showPersonalAccessTokenForm={true}
/>
`;

+ 101
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap Datei anzeigen

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

exports[`Should render correctly 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/bitbucket.svg"
/>
onboarding.create_project.bitbucketcloud.title
</span>
}
/>
<p>
Placeholder for next step
</p>
</Fragment>
`;

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

exports[`Should render correctly: Need App password 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/bitbucket.svg"
/>
onboarding.create_project.bitbucketcloud.title
</span>
}
/>
<PersonalAccessTokenForm
almSetting={
Object {
"alm": "bitbucketcloud",
"key": "key",
}
}
onPersonalAccessTokenCreated={[MockFunction]}
resetPat={false}
/>
</Fragment>
`;

exports[`Should render correctly: Wrong config 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/bitbucket.svg"
/>
onboarding.create_project.bitbucketcloud.title
</span>
}
/>
<WrongBindingCountAlert
alm="bitbucketcloud"
canAdmin={false}
/>
</Fragment>
`;

+ 55
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap Datei anzeigen

@@ -10,14 +10,66 @@ exports[`should render correctly 1`] = `
}
canAdmin={false}
importing={false}
loading={true}
loading={false}
onImportRepository={[Function]}
onPersonalAccessTokenCreate={[Function]}
onPersonalAccessTokenCreated={[Function]}
onProjectCreate={[MockFunction]}
onSearch={[Function]}
onSelectRepository={[Function]}
resetPat={false}
searching={false}
showPersonalAccessTokenForm={true}
/>
`;

exports[`should render correctly: No repository 1`] = `
<BitbucketProjectCreateRenderer
bitbucketSetting={
Object {
"alm": "bitbucket",
"key": "foo",
}
}
canAdmin={false}
importing={false}
loading={false}
onImportRepository={[Function]}
onPersonalAccessTokenCreated={[Function]}
onProjectCreate={[MockFunction]}
onSearch={[Function]}
onSelectRepository={[Function]}
projects={
Array [
Object {
"id": 1,
"key": "project1",
"name": "Project 1",
},
Object {
"id": 2,
"key": "project2",
"name": "Project",
},
]
}
resetPat={false}
searching={false}
showPersonalAccessTokenForm={false}
/>
`;

exports[`should render correctly: No setting 1`] = `
<BitbucketProjectCreateRenderer
canAdmin={false}
importing={false}
loading={false}
onImportRepository={[Function]}
onPersonalAccessTokenCreated={[Function]}
onProjectCreate={[MockFunction]}
onSearch={[Function]}
onSelectRepository={[Function]}
resetPat={false}
searching={false}
showPersonalAccessTokenForm={true}
tokenValidationFailed={false}
/>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap Datei anzeigen

@@ -281,8 +281,8 @@ exports[`should render correctly: pat form 1`] = `
"key": "key",
}
}
onPersonalAccessTokenCreate={[MockFunction]}
validationFailed={false}
onPersonalAccessTokenCreated={[MockFunction]}
resetPat={false}
/>
</Fragment>
`;

+ 13
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap Datei anzeigen

@@ -115,6 +115,19 @@ exports[`should render correctly if the BBS method is selected 1`] = `
}
}
onProjectCreate={[Function]}
router={
Object {
"createHref": [MockFunction],
"createPath": [MockFunction],
"go": [MockFunction],
"goBack": [MockFunction],
"goForward": [MockFunction],
"isActive": [MockFunction],
"push": [MockFunction],
"replace": [MockFunction],
"setRouteLeaveHook": [MockFunction],
}
}
/>
</div>
</Fragment>

+ 3
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap Datei anzeigen

@@ -3,11 +3,11 @@
exports[`should render correctly 1`] = `
<GitlabProjectCreateRenderer
canAdmin={false}
loading={true}
loading={false}
loadingMore={false}
onImport={[Function]}
onLoadMore={[Function]}
onPersonalAccessTokenCreate={[Function]}
onPersonalAccessTokenCreated={[Function]}
onSearch={[Function]}
projectsPaging={
Object {
@@ -16,6 +16,7 @@ exports[`should render correctly 1`] = `
"total": 0,
}
}
resetPat={false}
searchQuery=""
searching={false}
settings={
@@ -25,6 +26,5 @@ exports[`should render correctly 1`] = `
}
}
showPersonalAccessTokenForm={true}
submittingToken={false}
/>
`;

+ 2
- 35
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap Datei anzeigen

@@ -95,41 +95,8 @@ exports[`should render correctly: pat form 1`] = `
"key": "key",
}
}
onPersonalAccessTokenCreate={[MockFunction]}
submitting={false}
validationFailed={false}
/>
</Fragment>
`;

exports[`should render correctly: pat validation error 1`] = `
<Fragment>
<CreateProjectPageHeader
title={
<span
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="24"
src="/images/alm/gitlab.svg"
/>
onboarding.create_project.gitlab.title
</span>
}
/>
<PersonalAccessTokenForm
almSetting={
Object {
"alm": "gitlab",
"key": "key",
}
}
onPersonalAccessTokenCreate={[MockFunction]}
submitting={false}
validationErrorMessage="error"
validationFailed={true}
onPersonalAccessTokenCreated={[MockFunction]}
resetPat={false}
/>
</Fragment>
`;

+ 184
- 245
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap Datei anzeigen

@@ -19,7 +19,8 @@ exports[`should render correctly: bitbucket 1`] = `
onboarding.create_project.pat_form.help.bitbucket
</p>
<ValidationInput
id="personal_access_token"
error="onboarding.create_project.pat_incorrect.bitbucket"
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -27,23 +28,31 @@ exports[`should render correctly: bitbucket 1`] = `
>
<input
autoFocus={true}
className="input-super-large"
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ValidationInput
error="onboarding.create_project.pat_incorrect.bitbucket"
id="personal_access_token_submit"
isInvalid={true}
isValid={false}
label={null}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-left width-50"
@@ -120,7 +129,7 @@ exports[`should render correctly: bitbucket 1`] = `
</div>
`;

exports[`should render correctly: gitlab 1`] = `
exports[`should render correctly: bitbucket cloud 1`] = `
<div
className="display-flex-start"
>
@@ -131,39 +140,65 @@ exports[`should render correctly: gitlab 1`] = `
<h2
className="big"
>
onboarding.create_project.pat_form.title.gitlab
onboarding.create_project.pat_form.title.bitbucketcloud
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_form.help.gitlab
onboarding.create_project.pat_form.help.bitbucketcloud
</p>
<ValidationInput
id="personal_access_token"
id="enter_username_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
label="onboarding.create_project.enter_username"
required={true}
>
<input
autoFocus={true}
className="input-super-large"
className="input-super-large is-invalid"
id="username"
minLength={1}
name="username"
onChange={[Function]}
type="text"
/>
</ValidationInput>
<ValidationInput
error="onboarding.create_project.pat_incorrect.bitbucketcloud"
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat.bitbucketcloud"
required={true}
>
<input
autoFocus={false}
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ValidationInput
error="onboarding.create_project.pat_incorrect.bitbucketcloud"
id="personal_access_token_submit"
isInvalid={true}
isValid={false}
label={null}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-left width-50"
@@ -171,17 +206,17 @@ exports[`should render correctly: gitlab 1`] = `
variant="info"
>
<h3>
onboarding.create_project.pat_help.title
onboarding.create_project.pat_help.bitbucketcloud.title
</h3>
<p
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions"
defaultMessage="onboarding.create_project.pat_help.bitbucketcloud.instructions"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"alm": "onboarding.alm.gitlab",
"alm": "onboarding.alm.bitbucketcloud",
}
}
/>
@@ -193,35 +228,41 @@ exports[`should render correctly: gitlab 1`] = `
alt=""
className="spacer-right"
height="16"
src="/images/alm/gitlab.svg"
src="/images/alm/bitbucket.svg"
/>
<a
href="https://gitlab.com/profile/personal_access_tokens"
href="https://bitbucket.org/account/settings/app-passwords/new"
rel="noopener noreferrer"
target="_blank"
>
onboarding.create_project.pat_help.link
onboarding.create_project.pat_help.bitbucketcloud.link
</a>
</div>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_help.instructions2.gitlab
onboarding.create_project.pat_help.instructions2.bitbucketcloud
</p>
<ul>
<li
className="spacer-bottom"
>
<strong>
onboarding.create_project.pat_help.gitlab.read_api_permission
</strong>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
id="onboarding.create_project.pat_help.bbs_permission_repos"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
</li>
</ul>
</Alert>
</div>
`;

exports[`should render correctly: gitlab with non-standard api path 1`] = `
exports[`should render correctly: gitlab 1`] = `
<div
className="display-flex-start"
>
@@ -240,7 +281,8 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
onboarding.create_project.pat_form.help.gitlab
</p>
<ValidationInput
id="personal_access_token"
error="onboarding.create_project.pat_incorrect.gitlab"
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -248,23 +290,31 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
>
<input
autoFocus={true}
className="input-super-large"
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ValidationInput
error="onboarding.create_project.pat_incorrect.gitlab"
id="personal_access_token_submit"
isInvalid={true}
isValid={false}
label={null}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-left width-50"
@@ -297,7 +347,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
src="/images/alm/gitlab.svg"
/>
<a
href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"
href="https://gitlab.com/profile/personal_access_tokens"
rel="noopener noreferrer"
target="_blank"
>
@@ -322,7 +372,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
</div>
`;

exports[`should render correctly: submitting 1`] = `
exports[`should render correctly: gitlab with non-standard api path 1`] = `
<div
className="display-flex-start"
>
@@ -333,15 +383,16 @@ exports[`should render correctly: submitting 1`] = `
<h2
className="big"
>
onboarding.create_project.pat_form.title.bitbucket
onboarding.create_project.pat_form.title.gitlab
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_form.help.bitbucket
onboarding.create_project.pat_form.help.gitlab
</p>
<ValidationInput
id="personal_access_token"
error="onboarding.create_project.pat_incorrect.gitlab"
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -349,23 +400,31 @@ exports[`should render correctly: submitting 1`] = `
>
<input
autoFocus={true}
className="input-super-large"
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ValidationInput
error="onboarding.create_project.pat_incorrect.gitlab"
id="personal_access_token_submit"
isInvalid={true}
isValid={false}
label={null}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={true}
/>
</form>
<Alert
className="big-spacer-left width-50"
@@ -383,7 +442,7 @@ exports[`should render correctly: submitting 1`] = `
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"alm": "onboarding.alm.bitbucket",
"alm": "onboarding.alm.gitlab",
}
}
/>
@@ -395,10 +454,10 @@ exports[`should render correctly: submitting 1`] = `
alt=""
className="spacer-right"
height="16"
src="/images/alm/bitbucket.svg"
src="/images/alm/gitlab.svg"
/>
<a
href="http://www.example.com/plugins/servlet/access-tokens/add"
href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"
rel="noopener noreferrer"
target="_blank"
>
@@ -408,41 +467,29 @@ exports[`should render correctly: submitting 1`] = `
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_help.instructions2.bitbucket
onboarding.create_project.pat_help.instructions2.gitlab
</p>
<ul>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
id="onboarding.create_project.pat_help.bbs_permission_projects"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
</li>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
id="onboarding.create_project.pat_help.bbs_permission_repos"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
<li
className="spacer-bottom"
>
<strong>
onboarding.create_project.pat_help.gitlab.read_api_permission
</strong>
</li>
</ul>
</Alert>
</div>
`;

exports[`should render correctly: validation failed 1`] = `
exports[`should render correctly: no token needed 1`] = `
<DeferredSpinner
className="spacer-left"
loading={true}
/>
`;

exports[`should show error when issue: issue submitting token 1`] = `
<div
className="display-flex-start"
>
@@ -453,161 +500,66 @@ exports[`should render correctly: validation failed 1`] = `
<h2
className="big"
>
onboarding.create_project.pat_form.title.bitbucket
onboarding.create_project.pat_form.title.bitbucketcloud
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_form.help.bitbucket
onboarding.create_project.pat_form.help.bitbucketcloud
</p>
<ValidationInput
error="onboarding.create_project.pat_incorrect.bitbucket"
id="personal_access_token"
isInvalid={true}
id="enter_username_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
label="onboarding.create_project.enter_username"
required={true}
>
<input
autoFocus={true}
className="input-super-large is-invalid"
id="personal_access_token"
id="username"
minLength={1}
name="personal_access_token"
name="username"
onChange={[Function]}
type="text"
value="username"
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-left width-50"
display="block"
variant="info"
>
<h3>
onboarding.create_project.pat_help.title
</h3>
<p
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"alm": "onboarding.alm.bitbucket",
}
}
/>
</p>
<div
className="text-middle"
>
<img
alt=""
className="spacer-right"
height="16"
src="/images/alm/bitbucket.svg"
/>
<a
href="http://www.example.com/plugins/servlet/access-tokens/add"
rel="noopener noreferrer"
target="_blank"
>
onboarding.create_project.pat_help.link
</a>
</div>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_help.instructions2.bitbucket
</p>
<ul>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
id="onboarding.create_project.pat_help.bbs_permission_projects"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
</li>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
id="onboarding.create_project.pat_help.bbs_permission_repos"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
</li>
</ul>
</Alert>
</div>
`;

exports[`should render correctly: validation failed, custom error message 1`] = `
<div
className="display-flex-start"
>
<form
className="width-50"
onSubmit={[Function]}
>
<h2
className="big"
>
onboarding.create_project.pat_form.title.bitbucket
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_form.help.bitbucket
</p>
<ValidationInput
error="error"
id="personal_access_token"
isInvalid={true}
error="default_error_message"
id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
label="onboarding.create_project.enter_pat.bitbucketcloud"
required={true}
>
<input
autoFocus={true}
autoFocus={false}
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
name="personal_access_token"
onChange={[Function]}
type="text"
value="token"
/>
</ValidationInput>
<ValidationInput
error="default_error_message"
id="personal_access_token_submit"
isInvalid={true}
isValid={false}
label={null}
>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</ValidationInput>
<SubmitButton
disabled={true}
>
save
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-left width-50"
@@ -615,17 +567,17 @@ exports[`should render correctly: validation failed, custom error message 1`] =
variant="info"
>
<h3>
onboarding.create_project.pat_help.title
onboarding.create_project.pat_help.bitbucketcloud.title
</h3>
<p
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.instructions"
defaultMessage="onboarding.create_project.pat_help.bitbucketcloud.instructions"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
"alm": "onboarding.alm.bitbucket",
"alm": "onboarding.alm.bitbucketcloud",
}
}
/>
@@ -640,32 +592,19 @@ exports[`should render correctly: validation failed, custom error message 1`] =
src="/images/alm/bitbucket.svg"
/>
<a
href="http://www.example.com/plugins/servlet/access-tokens/add"
href="https://bitbucket.org/account/settings/app-passwords/new"
rel="noopener noreferrer"
target="_blank"
>
onboarding.create_project.pat_help.link
onboarding.create_project.pat_help.bitbucketcloud.link
</a>
</div>
<p
className="big-spacer-top big-spacer-bottom"
>
onboarding.create_project.pat_help.instructions2.bitbucket
onboarding.create_project.pat_help.instructions2.bitbucketcloud
</p>
<ul>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
id="onboarding.create_project.pat_help.bbs_permission_projects"
values={
Object {
"perm": <strong>
onboarding.create_project.pat_help.read_permission
</strong>,
}
}
/>
</li>
<li>
<FormattedMessage
defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/types.ts Datei anzeigen

@@ -21,6 +21,7 @@ export enum CreateProjectModes {
Manual = 'manual',
AzureDevOps = 'azure',
BitbucketServer = 'bitbucket',
BitbucketCloud = 'bitbucketcloud',
GitHub = 'github',
GitLab = 'gitlab'
}

+ 10
- 0
server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts Datei anzeigen

@@ -44,6 +44,16 @@ export function mockAlmSettingsInstance(
};
}

export function mockBitbucketCloudAlmSettingsInstance(
overrides: Partial<AlmSettingsInstance> = {}
): AlmSettingsInstance {
return {
alm: AlmKeys.BitbucketCloud,
key: 'key',
...overrides
};
}

export function mockAzureBindingDefinition(
overrides: Partial<AzureBindingDefinition> = {}
): AzureBindingDefinition {

+ 13
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Datei anzeigen

@@ -3200,6 +3200,7 @@ footer.web_api=Web API
#------------------------------------------------------------------------------
onboarding.alm.azure=Azure DevOps
onboarding.alm.bitbucket=Bitbucket Server
onboarding.alm.bitbucketcloud=Bitbucket Cloud
onboarding.alm.gitlab=GitLab

onboarding.project_analysis.header=Analyze your project
@@ -3243,9 +3244,11 @@ onboarding.create_application.key.description=If specified, this value is used a

onboarding.create_project.pat_form.title.azure=Allow SonarQube to access and list your Azure DevOps repositories
onboarding.create_project.pat_form.title.bitbucket=Grant access to your repositories
onboarding.create_project.pat_form.title.bitbucketcloud=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.
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.bitbucketcloud=SonarQube needs an app password to access and list your repositories from Bitbucket Cloud.
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
@@ -3262,17 +3265,26 @@ 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.enter_pat.bitbucketcloud=Enter your app password
onboarding.create_project.enter_username=Enter your Bitbucket username
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.bitbucketcloud=Your app password 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.bitbucketcloud.title=How to create an app password?

onboarding.create_project.pat_help.instructions.azure=Create and provide an Azure DevOps {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.bitbucketcloud.instructions=Click the following link to generate an app password, and copy-paste it into the app password field.

onboarding.create_project.pat_help.instructions2.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
onboarding.create_project.pat_help.instructions2.bitbucketcloud=Set a name, for example "SonarQube", and select the following permissions:

onboarding.create_project.pat_help.link=Create personal access token
onboarding.create_project.pat_help.bitbucketcloud.link=Add app password
onboarding.create_project.pat_help.bbs_permission_projects=Projects: {perm}
onboarding.create_project.pat_help.bbs_permission_repos=Repositories: {perm}
onboarding.create_project.pat_help.read_permission=Read
@@ -3292,6 +3304,7 @@ onboarding.create_project.azure.title=Which Azure DevOps repository do you want
onboarding.create_project.azure.no_projects=No projects could be fetched from Azure DevOps. Contact your system administrator, or {link}.
onboarding.create_project.azure.no_repositories=Could not fetch repositories for this project. Contact your system administrator, or {link}.
onboarding.create_project.azure.no_results=No repositories match your search query.
onboarding.create_project.bitbucketcloud.title=Which Bitbucket Cloud 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

Laden…
Abbrechen
Speichern