aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>2023-12-19 15:36:18 +0100
committersonartech <sonartech@sonarsource.com>2023-12-19 20:02:55 +0000
commit0db27c3d415c58b03966ea0ad9b52b8811fc9d77 (patch)
tree7964578543d9a9ee5227ae4d824fa416e3eacfc0 /server
parent9eb4153032acdcb1b0812d6a50301089fd9cbde2 (diff)
downloadsonarqube-0db27c3d415c58b03966ea0ad9b52b8811fc9d77.tar.gz
sonarqube-0db27c3d415c58b03966ea0ad9b52b8811fc9d77.zip
MMF-3504 Open issue in IDE: send user token to SonarLint if needed
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts2
-rw-r--r--server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx34
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx73
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TutorialsApp-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts46
-rw-r--r--server/sonar-web/src/main/js/helpers/sonarlint.ts77
-rw-r--r--server/sonar-web/src/main/js/types/sonarlint.ts6
20 files changed, 203 insertions, 121 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts
index 5acdf231a27..fb133924dd0 100644
--- a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts
@@ -22,6 +22,8 @@ import { mockUserToken } from '../../helpers/mocks/token';
import { NewUserToken, TokenType, UserToken } from '../../types/token';
import { generateToken, getTokens, revokeToken } from '../user-tokens';
+jest.mock('../../api/user-tokens');
+
const defaultTokens = [
mockUserToken({
name: 'local-scanner',
diff --git a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx
index 3d210044326..5db0bbc858c 100644
--- a/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx
+++ b/server/sonar-web/src/main/js/app/components/SonarLintConnection.tsx
@@ -17,23 +17,18 @@
* 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 { FormattedMessage } from 'react-intl';
import { useSearchParams } from 'react-router-dom';
-import { generateToken, getTokens } from '../../api/user-tokens';
import Link from '../../components/common/Link';
import { Button } from '../../components/controls/buttons';
import { ClipboardButton } from '../../components/controls/clipboard';
import { whenLoggedIn } from '../../components/hoc/whenLoggedIn';
import CheckIcon from '../../components/icons/CheckIcon';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { portIsValid, sendUserToken } from '../../helpers/sonarlint';
-import {
- computeTokenExpirationDate,
- getAvailableExpirationOptions,
- getNextTokenName,
-} from '../../helpers/tokens';
-import { NewUserToken, TokenExpiration } from '../../types/token';
+import { generateSonarLintUserToken, portIsValid, sendUserToken } from '../../helpers/sonarlint';
+import { NewUserToken } from '../../types/token';
import { LoggedInUser } from '../../types/users';
import './SonarLintConnection.css';
@@ -48,22 +43,7 @@ interface Props {
currentUser: LoggedInUser;
}
-const TOKEN_PREFIX = 'SonarLint';
-
-const getNextAvailableTokenName = async (login: string, tokenNameBase: string) => {
- const tokens = await getTokens(login);
-
- return getNextTokenName(tokenNameBase, tokens);
-};
-
-async function computeExpirationDate() {
- const options = await getAvailableExpirationOptions();
- const maxOption = options[options.length - 1];
-
- return computeTokenExpirationDate(maxOption.value || TokenExpiration.OneYear);
-}
-
-export function SonarLintConnection({ currentUser }: Props) {
+export function SonarLintConnection({ currentUser }: Readonly<Props>) {
const [searchParams] = useSearchParams();
const [status, setStatus] = React.useState(Status.request);
const [newToken, setNewToken] = React.useState<NewUserToken | undefined>(undefined);
@@ -74,11 +54,7 @@ export function SonarLintConnection({ currentUser }: Props) {
const { login } = currentUser;
const authorize = React.useCallback(async () => {
- const newTokenName = await getNextAvailableTokenName(login, `${TOKEN_PREFIX}-${ideName}`);
- const expirationDate = await computeExpirationDate();
- const token = await generateToken({ name: newTokenName, login, expirationDate }).catch(
- () => undefined,
- );
+ const token = await generateSonarLintUserToken({ ideName, login }).catch(() => undefined);
if (!token) {
setStatus(Status.tokenError);
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx
index 20e9dd54c1c..3395626b02b 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/SonarLintConnection-test.tsx
@@ -28,8 +28,6 @@ import { renderApp } from '../../../helpers/testReactTestingUtils';
import { CurrentUser } from '../../../types/users';
import SonarLintConnection from '../SonarLintConnection';
-jest.mock('../../../api/user-tokens');
-
jest.mock('../../../helpers/handleRequiredAuthentication', () => ({
__esModule: true,
default: jest.fn(),
diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
index 83ad1f33153..d5a4e206ed3 100644
--- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
+++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
@@ -35,8 +35,6 @@ import { TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';
-jest.mock('../../../api/user-tokens');
-
jest.mock('../../../helpers/preferences', () => ({
getKeyboardShortcutEnabled: jest.fn().mockResolvedValue(true),
setKeyboardShortcutEnabled: jest.fn(),
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx
index f391b2ff87a..0a2aedace88 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx
@@ -32,19 +32,25 @@ import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import DocumentationLink from '../../../components/common/DocumentationLink';
import { translate } from '../../../helpers/l10n';
-import { openIssue as openSonarLintIssue, probeSonarLintServers } from '../../../helpers/sonarlint';
+import {
+ generateSonarLintUserToken,
+ openIssue as openSonarLintIssue,
+ probeSonarLintServers,
+} from '../../../helpers/sonarlint';
import { Ide } from '../../../types/sonarlint';
+import { UserBase } from '../../../types/users';
export interface Props {
branchName?: string;
issueKey: string;
+ login: UserBase['login'];
projectKey: string;
pullRequestID?: string;
}
interface State {
+ disabled?: boolean;
ides: Ide[];
- mounted: boolean;
}
const showError = () =>
@@ -63,36 +69,53 @@ const showError = () =>
const showSuccess = () => addGlobalSuccessMessage(translate('issues.open_in_ide.success'));
+const DELAY_AFTER_TOKEN_CREATION = 3000;
+
export function IssueOpenInIdeButton({
branchName,
issueKey,
+ login,
projectKey,
pullRequestID,
}: Readonly<Props>) {
- const [state, setState] = React.useState<State>({ ides: [], mounted: false });
-
- React.useEffect(() => {
- setState({ ...state, mounted: true });
-
- return () => {
- setState({ ...state, mounted: false });
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const [state, setState] = React.useState<State>({ disabled: false, ides: [] });
const cleanState = () => {
- if (state.mounted) {
- setState({ ...state, ides: [] });
- }
+ setState({ ...state, ides: [] });
};
- const openIssue = (ide: Ide) => {
- setState({ ...state, ides: [] });
+ const openIssue = async (ide: Ide) => {
+ setState({ ...state, disabled: true, ides: [] }); // close the dropdown, disable the button
+
+ let token: { name?: string; token?: string } = {};
+
+ try {
+ if (ide.needsToken) {
+ token = await generateSonarLintUserToken({ ideName: ide.ideName, login });
+ }
+
+ await openSonarLintIssue({
+ branchName,
+ calledPort: ide.port,
+ issueKey,
+ login,
+ projectKey,
+ pullRequestID,
+ tokenName: token.name,
+ tokenValue: token.token,
+ });
+
+ showSuccess();
+ } catch (e) {
+ showError();
+ }
- return openSonarLintIssue(ide.port, projectKey, issueKey, branchName, pullRequestID)
- .then(showSuccess)
- .catch(showError)
- .finally(cleanState);
+ setTimeout(
+ () => {
+ setState({ ...state, disabled: false });
+ },
+ ide.needsToken ? DELAY_AFTER_TOKEN_CREATION : 0,
+ );
};
const onClick = async () => {
@@ -104,7 +127,7 @@ export function IssueOpenInIdeButton({
showError();
} else if (ides.length === 1) {
openIssue(ides[0]);
- } else if (state.mounted) {
+ } else {
setState({ ...state, ides });
}
};
@@ -138,7 +161,11 @@ export function IssueOpenInIdeButton({
placement={PopupPlacement.BottomLeft}
zLevel={PopupZLevel.Global}
>
- <ButtonSecondary className="sw-whitespace-nowrap" onClick={onClick}>
+ <ButtonSecondary
+ className="sw-whitespace-nowrap"
+ disabled={state.disabled}
+ onClick={onClick}
+ >
{translate('open_in_ide')}
</ButtonSecondary>
</DropdownToggler>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx
index 004d5647a5c..4f8c57dd3f0 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx
@@ -23,15 +23,20 @@ import userEvent from '@testing-library/user-event';
import { addGlobalErrorMessage, addGlobalSuccessMessage } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
+import UserTokensMock from '../../../../api/mocks/UserTokensMock';
import DocumentationLink from '../../../../components/common/DocumentationLink';
import {
openIssue as openSonarLintIssue,
probeSonarLintServers,
} from '../../../../helpers/sonarlint';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { Ide } from '../../../../types/sonarlint';
import { IssueOpenInIdeButton, Props } from '../IssueOpenInIdeButton';
jest.mock('../../../../helpers/sonarlint', () => ({
+ generateSonarLintUserToken: jest
+ .fn()
+ .mockResolvedValue({ name: 'token name', token: 'token value' }),
openIssue: jest.fn().mockResolvedValue(undefined),
probeSonarLintServers: jest.fn(),
}));
@@ -42,13 +47,24 @@ jest.mock('design-system', () => ({
addGlobalSuccessMessage: jest.fn(),
}));
-const MOCK_IDES = [
+const MOCK_IDES: Ide[] = [
{ description: 'IDE description', ideName: 'Some IDE', port: 1234 },
- { description: '', ideName: 'Some other IDE', port: 42000 },
+ { description: '', ideName: 'Some other IDE', needsToken: true, port: 42000 },
];
+
const MOCK_ISSUE_KEY = 'issue-key';
const MOCK_PROJECT_KEY = 'project-key';
+let tokenMock: UserTokensMock;
+
+beforeAll(() => {
+ tokenMock = new UserTokensMock();
+});
+
+afterEach(() => {
+ tokenMock.reset();
+});
+
beforeEach(() => {
jest.clearAllMocks();
});
@@ -113,13 +129,16 @@ it('handles button click with one ide found', async () => {
expect(probeSonarLintServers).toHaveBeenCalledWith();
- expect(openSonarLintIssue).toHaveBeenCalledWith(
- MOCK_IDES[0].port,
- MOCK_PROJECT_KEY,
- MOCK_ISSUE_KEY,
- undefined,
- undefined,
- );
+ expect(openSonarLintIssue).toHaveBeenCalledWith({
+ branchName: undefined,
+ calledPort: MOCK_IDES[0].port,
+ issueKey: MOCK_ISSUE_KEY,
+ login: 'login-1',
+ projectKey: MOCK_PROJECT_KEY,
+ pullRequestID: undefined,
+ tokenName: undefined,
+ tokenValue: undefined,
+ });
expect(addGlobalSuccessMessage).toHaveBeenCalledWith('issues.open_in_ide.success');
@@ -159,13 +178,16 @@ it('handles button click with several ides found', async () => {
await user.click(secondIde);
});
- expect(openSonarLintIssue).toHaveBeenCalledWith(
- MOCK_IDES[1].port,
- MOCK_PROJECT_KEY,
- MOCK_ISSUE_KEY,
- undefined,
- undefined,
- );
+ expect(openSonarLintIssue).toHaveBeenCalledWith({
+ branchName: undefined,
+ calledPort: MOCK_IDES[1].port,
+ issueKey: MOCK_ISSUE_KEY,
+ login: 'login-1',
+ projectKey: MOCK_PROJECT_KEY,
+ pullRequestID: undefined,
+ tokenName: 'token name',
+ tokenValue: 'token value',
+ });
expect(addGlobalSuccessMessage).toHaveBeenCalledWith('issues.open_in_ide.success');
@@ -174,6 +196,11 @@ it('handles button click with several ides found', async () => {
function renderComponentIssueOpenInIdeButton(props: Partial<Props> = {}) {
return renderComponent(
- <IssueOpenInIdeButton issueKey={MOCK_ISSUE_KEY} projectKey={MOCK_PROJECT_KEY} {...props} />,
+ <IssueOpenInIdeButton
+ issueKey={MOCK_ISSUE_KEY}
+ login="login-1"
+ projectKey={MOCK_PROJECT_KEY}
+ {...props}
+ />,
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
index 2120bf9569e..fefa0b15588 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
@@ -172,6 +172,7 @@ export function IssueSourceViewerHeader(props: Readonly<Props>) {
<IssueOpenInIdeButton
branchName={branchName}
issueKey={issueKey}
+ login={currentUser.login}
projectKey={project}
pullRequestID={pullRequestID}
/>
diff --git a/server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TutorialsApp-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TutorialsApp-test.tsx
index ad5c80cb80d..175211f9897 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TutorialsApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/components/__tests__/TutorialsApp-test.tsx
@@ -26,8 +26,6 @@ import { byLabelText, byRole } from '../../../../helpers/testSelector';
import { Permissions } from '../../../../types/permissions';
import routes from '../../routes';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../helpers/handleRequiredAuthentication', () => jest.fn());
let settingsMock: SettingsServiceMock;
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
index 8a5c1240f2f..a9e090eaf1a 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
@@ -37,8 +37,6 @@ import { TaskStatuses } from '../../../types/tasks';
import { ChangePasswordResults, CurrentUser } from '../../../types/users';
import UsersApp from '../UsersApp';
-jest.mock('../../../api/user-tokens');
-
const userHandler = new UsersServiceMock();
const tokenHandler = new UserTokensMock();
const systemHandler = new SystemServiceMock();
diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
index 636bce0f5de..4bcd0c3493e 100644
--- a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
@@ -38,8 +38,6 @@ import { SettingsKey } from '../../../types/settings';
import TutorialSelection from '../TutorialSelection';
import { TutorialModes } from '../types';
-jest.mock('../../../api/user-tokens');
-
jest.mock('../../../helpers/urls', () => ({
...jest.requireActual('../../../helpers/urls'),
getHostUrl: jest.fn().mockReturnValue('http://host.url'),
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
index c5f2fe19608..b5553601c5d 100644
--- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
@@ -31,8 +31,6 @@ import { getCopyToClipboardValue, getTutorialBuildButtons } from '../../test-uti
import { OSs } from '../../types';
import AzurePipelinesTutorial, { AzurePipelinesTutorialProps } from '../AzurePipelinesTutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
index 9b98664c4a0..94e3fe32bd9 100644
--- a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
@@ -40,8 +40,6 @@ import BitbucketPipelinesTutorial, {
BitbucketPipelinesTutorialProps,
} from '../BitbucketPipelinesTutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
index 0526f6ae83c..3b8390fda40 100644
--- a/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/components/__tests__/EditTokenModal-test.tsx
@@ -31,8 +31,6 @@ import { Permissions } from '../../../../types/permissions';
import { TokenType } from '../../../../types/token';
import EditTokenModal from '../EditTokenModal';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
index 1e2c5c3cd7d..c828104edbc 100644
--- a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
@@ -38,8 +38,6 @@ import {
import { GradleBuildDSL, TutorialModes } from '../../types';
import GitHubActionTutorial, { GitHubActionTutorialProps } from '../GitHubActionTutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
index d770389339b..86204862f64 100644
--- a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
@@ -34,8 +34,6 @@ import {
import { GradleBuildDSL, TutorialModes } from '../../types';
import GitLabCITutorial, { GitLabCITutorialProps } from '../GitLabCITutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
index 9c7ef8cda9d..a29b6c583f7 100644
--- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
@@ -36,8 +36,6 @@ import {
import { GradleBuildDSL } from '../../types';
import JenkinsTutorial, { JenkinsTutorialProps } from '../JenkinsTutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
index 7b69fde3d6c..b106b35eb9b 100644
--- a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
@@ -33,8 +33,6 @@ import {
} from '../../test-utils';
import OtherTutorial from '../OtherTutorial';
-jest.mock('../../../../api/user-tokens');
-
jest.mock('../../../../api/settings', () => ({
getAllValues: jest.fn().mockResolvedValue([]),
}));
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts
index af8b0b8c078..5d0838ca95f 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts
@@ -46,7 +46,7 @@ describe('buildPortRange', () => {
});
describe('probeSonarLintServers', () => {
- const sonarLintResponse = { ideName: 'BlueJ IDE', description: 'Hello World' };
+ const sonarLintResponse = { description: 'Hello World', ideName: 'BlueJ IDE', needsToken: true };
window.fetch = jest.fn((input: string) => {
const calledPort = new URL(input).port;
@@ -96,7 +96,10 @@ describe('openHotspot', () => {
describe('openIssue', () => {
it('should send the correct request to the IDE to open an issue', async () => {
let branchName: string | undefined = undefined;
+ let login: string | undefined = undefined;
let pullRequestID: string | undefined = undefined;
+ let tokenName: string | undefined = undefined;
+ let tokenValue: string | undefined = undefined;
const issueKey = 'my-issue-key';
const resp = new Response();
@@ -111,6 +114,12 @@ describe('openIssue', () => {
expect(calledUrl.searchParams.get('branch') ?? undefined).toStrictEqual(branchName);
// eslint-disable-next-line jest/no-conditional-in-test
expect(calledUrl.searchParams.get('pullRequest') ?? undefined).toStrictEqual(pullRequestID);
+ // eslint-disable-next-line jest/no-conditional-in-test
+ expect(calledUrl.searchParams.get('login') ?? undefined).toStrictEqual(login);
+ // eslint-disable-next-line jest/no-conditional-in-test
+ expect(calledUrl.searchParams.get('tokenName') ?? undefined).toStrictEqual(tokenName);
+ // eslint-disable-next-line jest/no-conditional-in-test
+ expect(calledUrl.searchParams.get('tokenValue') ?? undefined).toStrictEqual(tokenValue);
} catch (error) {
return Promise.reject(error);
}
@@ -118,27 +127,32 @@ describe('openIssue', () => {
return Promise.resolve(resp);
});
- let result = await openIssue(SONARLINT_PORT_START, PROJECT_KEY, issueKey);
-
- expect(result).toBe(resp);
+ type OpenIssueParams = Parameters<typeof openIssue>[0];
+ type PartialOpenIssueParams = Partial<OpenIssueParams>;
+ let params: PartialOpenIssueParams = {};
- branchName = 'branch-1';
+ const testWith = async (args: PartialOpenIssueParams) => {
+ params = { ...params, ...args };
+ const result = await openIssue(params as OpenIssueParams);
+ expect(result).toBe(resp);
+ };
- result = await openIssue(SONARLINT_PORT_START, PROJECT_KEY, issueKey, branchName);
+ await testWith({
+ calledPort: SONARLINT_PORT_START,
+ issueKey,
+ projectKey: PROJECT_KEY,
+ });
- expect(result).toBe(resp);
+ branchName = 'branch-1';
+ await testWith({ branchName });
pullRequestID = 'pr-1';
+ await testWith({ pullRequestID });
- result = await openIssue(
- SONARLINT_PORT_START,
- PROJECT_KEY,
- issueKey,
- branchName,
- pullRequestID,
- );
-
- expect(result).toBe(resp);
+ login = 'login-1';
+ tokenName = 'token-name';
+ tokenValue = 'token-value';
+ await testWith({ login, tokenName, tokenValue });
});
});
diff --git a/server/sonar-web/src/main/js/helpers/sonarlint.ts b/server/sonar-web/src/main/js/helpers/sonarlint.ts
index 07dbf341ab5..c61a058e1b5 100644
--- a/server/sonar-web/src/main/js/helpers/sonarlint.ts
+++ b/server/sonar-web/src/main/js/helpers/sonarlint.ts
@@ -18,10 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { generateToken, getTokens } from '../api/user-tokens';
import { getHostUrl } from '../helpers/urls';
import { Ide } from '../types/sonarlint';
-import { NewUserToken } from '../types/token';
+import { NewUserToken, TokenExpiration } from '../types/token';
+import { UserBase } from '../types/users';
import { checkStatus, isSuccessStatus } from './request';
+import {
+ computeTokenExpirationDate,
+ getAvailableExpirationOptions,
+ getNextTokenName,
+} from './tokens';
export const SONARLINT_PORT_START = 64120;
export const SONARLINT_PORT_RANGE = 11;
@@ -33,9 +40,9 @@ export async function probeSonarLintServers(): Promise<Array<Ide>> {
fetch(buildSonarLintEndpoint(p, '/status'))
.then((r) => r.json())
.then((json) => {
- const { ideName, description } = json;
+ const { description, ideName, needsToken } = json;
- return { port: p, ideName, description } as Ide;
+ return { description, ideName, needsToken, port: p } as Ide;
})
.catch(() => undefined),
);
@@ -55,13 +62,57 @@ export function openHotspot(calledPort: number, projectKey: string, hotspotKey:
return fetch(showUrl.toString()).then((response: Response) => checkStatus(response, true));
}
-export function openIssue(
- calledPort: number,
- projectKey: string,
- issueKey: string,
- branchName?: string,
- pullRequestID?: string,
-) {
+const computeSonarLintTokenExpirationDate = async () => {
+ const options = await getAvailableExpirationOptions();
+ const maxOption = options[options.length - 1];
+
+ return computeTokenExpirationDate(maxOption.value || TokenExpiration.OneYear);
+};
+
+const getNextAvailableSonarLintTokenName = async ({
+ ideName,
+ login,
+}: {
+ ideName: string;
+ login: string;
+}) => {
+ const tokens = await getTokens(login);
+
+ return getNextTokenName(`SonarLint-${ideName}`, tokens);
+};
+
+export const generateSonarLintUserToken = async ({
+ ideName,
+ login,
+}: {
+ ideName: string;
+ login: UserBase['login'];
+}) => {
+ const name = await getNextAvailableSonarLintTokenName({ ideName, login });
+ const expirationDate = await computeSonarLintTokenExpirationDate();
+
+ return generateToken({ expirationDate, login, name });
+};
+
+export function openIssue({
+ branchName,
+ calledPort,
+ issueKey,
+ login,
+ projectKey,
+ pullRequestID,
+ tokenName,
+ tokenValue,
+}: {
+ branchName?: string;
+ calledPort: number;
+ issueKey: string;
+ login?: string;
+ projectKey: string;
+ pullRequestID?: string;
+ tokenName?: string;
+ tokenValue?: string;
+}) {
const showUrl = new URL(buildSonarLintEndpoint(calledPort, '/issues/show'));
showUrl.searchParams.set('server', getHostUrl());
@@ -76,6 +127,12 @@ export function openIssue(
showUrl.searchParams.set('pullRequest', pullRequestID);
}
+ if (login !== undefined && tokenName !== undefined && tokenValue !== undefined) {
+ showUrl.searchParams.set('login', login);
+ showUrl.searchParams.set('tokenName', tokenName);
+ showUrl.searchParams.set('tokenValue', tokenValue);
+ }
+
return fetch(showUrl.toString()).then((response: Response) => checkStatus(response, true));
}
diff --git a/server/sonar-web/src/main/js/types/sonarlint.ts b/server/sonar-web/src/main/js/types/sonarlint.ts
index 0c67ef05e38..cf176e51f4e 100644
--- a/server/sonar-web/src/main/js/types/sonarlint.ts
+++ b/server/sonar-web/src/main/js/types/sonarlint.ts
@@ -17,8 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
export interface Ide {
- port: number;
- ideName: string;
description: string;
+ ideName: string;
+ needsToken?: boolean;
+ port: number;
}