diff options
author | David Cho-Lerat <david.cho-lerat@sonarsource.com> | 2023-12-19 15:36:18 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-12-19 20:02:55 +0000 |
commit | 0db27c3d415c58b03966ea0ad9b52b8811fc9d77 (patch) | |
tree | 7964578543d9a9ee5227ae4d824fa416e3eacfc0 /server | |
parent | 9eb4153032acdcb1b0812d6a50301089fd9cbde2 (diff) | |
download | sonarqube-0db27c3d415c58b03966ea0ad9b52b8811fc9d77.tar.gz sonarqube-0db27c3d415c58b03966ea0ad9b52b8811fc9d77.zip |
MMF-3504 Open issue in IDE: send user token to SonarLint if needed
Diffstat (limited to 'server')
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; } |