From b3cca30a9450f9064d698312b055b254b0bc2ab6 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 11 Dec 2019 11:53:19 +0100 Subject: [PATCH] SONAR-12719 Hotspot resolution form --- .../src/main/js/api/security-hotspots.ts | 12 +- .../SecurityHotspotsAppRenderer.tsx | 1 + .../components/HotspotActions.tsx | 68 ++++ .../components/HotspotActionsForm.tsx | 89 ++++++ .../components/HotspotActionsFormRenderer.tsx | 81 +++++ .../components/HotspotViewerRenderer.tsx | 15 +- .../__tests__/HotspotActions-test.tsx | 70 ++++ .../__tests__/HotspotActionsForm-test.tsx | 101 ++++++ .../HotspotActionsFormRenderer-test.tsx | 47 +++ .../__tests__/HotspotViewerRenderer-test.tsx | 7 +- .../HotspotActions-test.tsx.snap | 112 +++++++ .../HotspotActionsForm-test.tsx.snap | 11 + .../HotspotActionsFormRenderer-test.tsx.snap | 238 ++++++++++++++ .../__snapshots__/HotspotViewer-test.tsx.snap | 4 +- .../HotspotViewerRenderer-test.tsx.snap | 301 +++++++++++++++++- .../main/js/apps/securityHotspots/styles.css | 7 + .../src/main/js/types/security-hotspots.ts | 22 ++ .../resources/org/sonar/l10n/core.properties | 15 + 18 files changed, 1186 insertions(+), 15 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts index d9ed71e170a..9971cdd6c2e 100644 --- a/server/sonar-web/src/main/js/api/security-hotspots.ts +++ b/server/sonar-web/src/main/js/api/security-hotspots.ts @@ -17,9 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON } from 'sonar-ui-common/helpers/request'; +import { getJSON, post } from 'sonar-ui-common/helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -import { DetailedHotspot, HotspotSearchResponse } from '../types/security-hotspots'; +import { + DetailedHotspot, + HotspotSearchResponse, + HotspotSetStatusRequest +} from '../types/security-hotspots'; + +export function setSecurityHotspotStatus(data: HotspotSetStatusRequest): Promise { + return post('/api/hotspots/change_status', data).catch(throwGlobalError); +} export function getSecurityHotspots(data: { projectKey: string; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx index 078a1d5b51b..528951bfd7c 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx @@ -42,6 +42,7 @@ export interface SecurityHotspotsAppRendererProps { export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { const { hotspots, loading, securityCategories, selectedHotspotKey } = props; + return (
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx new file mode 100644 index 00000000000..9f40f72c4d1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Button } from 'sonar-ui-common/components/controls/buttons'; +import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; +import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler'; +import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; +import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import HotspotActionsForm from './HotspotActionsForm'; + +export interface HotspotActionsProps { + hotspotKey: string; +} + +const ESCAPE_KEY = 'Escape'; + +export default function HotspotActions(props: HotspotActionsProps) { + const [open, setOpen] = React.useState(false); + + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === ESCAPE_KEY) { + setOpen(false); + } + }; + + document.addEventListener('keydown', handleKeyDown, false); + + return () => { + document.removeEventListener('keydown', handleKeyDown, false); + }; + }); + + return ( +
+ + + {open && ( + setOpen(false)}> + + setOpen(false)} /> + + + )} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx new file mode 100644 index 00000000000..e34a2ad1acc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { setSecurityHotspotStatus } from '../../../api/security-hotspots'; +import { + HotspotResolution, + HotspotSetStatusRequest, + HotspotStatus, + HotspotStatusOptions +} from '../../../types/security-hotspots'; +import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; + +interface Props { + hotspotKey: string; + onSubmit: () => void; +} + +interface State { + selectedOption: HotspotStatusOptions; + submitting: boolean; +} + +export default class HotspotActionsForm extends React.Component { + state: State = { + selectedOption: HotspotStatusOptions.FIXED, + submitting: false + }; + + handleSelectOption = (selectedOption: HotspotStatusOptions) => { + this.setState({ selectedOption }); + }; + + handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + + const { hotspotKey } = this.props; + const { selectedOption } = this.state; + + const status = + selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW + ? HotspotStatus.TO_REVIEW + : HotspotStatus.REVIEWED; + const data: HotspotSetStatusRequest = { hotspot: hotspotKey, status }; + if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { + data.resolution = HotspotResolution[selectedOption]; + } + + this.setState({ submitting: true }); + return setSecurityHotspotStatus(data) + .then(() => { + this.props.onSubmit(); + }) + .finally(() => { + this.setState({ submitting: false }); + }); + }; + + render() { + const { hotspotKey } = this.props; + const { selectedOption, submitting } = this.state; + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx new file mode 100644 index 00000000000..666c8a809c2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import Radio from 'sonar-ui-common/components/controls/Radio'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { HotspotStatusOptions } from '../../../types/security-hotspots'; + +export interface HotspotActionsFormRendererProps { + hotspotKey: string; + onSelectOption: (option: HotspotStatusOptions) => void; + onSubmit: (event: React.SyntheticEvent) => void; + selectedOption: HotspotStatusOptions; + submitting: boolean; +} + +export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) { + const { selectedOption, submitting } = props; + + return ( +
+

{translate('hotspots.form.title')}

+
+ {renderOption({ + option: HotspotStatusOptions.FIXED, + selectedOption, + onClick: props.onSelectOption + })} + {renderOption({ + option: HotspotStatusOptions.SAFE, + selectedOption, + onClick: props.onSelectOption + })} + {renderOption({ + option: HotspotStatusOptions.ADDITIONAL_REVIEW, + selectedOption, + onClick: props.onSelectOption + })} +
+
+ {submitting && } + {translate('hotspots.form.submit')} +
+ + ); +} + +function renderOption(params: { + option: HotspotStatusOptions; + onClick: (option: HotspotStatusOptions) => void; + selectedOption: HotspotStatusOptions; +}) { + const { onClick, option, selectedOption } = params; + return ( +
+ +

{translate('hotspots.status_option', option)}

+
+
+ {translate('hotspots.status_option', option, 'description')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx index 9764ff4a34d..334b48c80ad 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx @@ -20,24 +20,31 @@ import * as React from 'react'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; +import { isLoggedIn } from '../../../helpers/users'; import { DetailedHotspot } from '../../../types/security-hotspots'; +import HotspotActions from './HotspotActions'; import HotspotViewerTabs from './HotspotViewerTabs'; export interface HotspotViewerRendererProps { + currentUser: T.CurrentUser; hotspot?: DetailedHotspot; loading: boolean; securityCategories: T.StandardSecurityCategories; } -export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) { - const { hotspot, loading, securityCategories } = props; +export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { + const { currentUser, hotspot, loading, securityCategories } = props; return ( {hotspot && (
-

{hotspot.message}

+
+

{hotspot.message}

+ {isLoggedIn(currentUser) && } +
{translate('hotspot.category')} @@ -67,3 +74,5 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) ); } + +export default withCurrentUser(HotspotViewerRenderer); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx new file mode 100644 index 00000000000..7f6f2387fc9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { Button } from 'sonar-ui-common/components/controls/buttons'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import HotspotActions, { HotspotActionsProps } from '../HotspotActions'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should open when clicked', async () => { + const wrapper = shallowRender(); + + wrapper.find(Button).simulate('click'); + + await waitAndUpdate(wrapper); + + expect(wrapper).toMatchSnapshot(); +}); + +it('should register an eventlistener', () => { + let useEffectCleanup: void | (() => void | undefined) = () => + fail('useEffect should clean after itself'); + jest.spyOn(React, 'useEffect').mockImplementationOnce(f => { + useEffectCleanup = f() || useEffectCleanup; + }); + let listenerCallback = (_event: { key: string }) => + fail('Effect should have registered callback'); + const addEventListener = jest.fn((_event, callback) => { + listenerCallback = callback; + }); + jest.spyOn(document, 'addEventListener').mockImplementation(addEventListener); + const removeEventListener = jest.spyOn(document, 'removeEventListener'); + const wrapper = shallowRender(); + + wrapper.find(Button).simulate('click'); + expect(wrapper).toMatchSnapshot('Dropdown open'); + + listenerCallback({ key: 'whatever' }); + expect(wrapper).toMatchSnapshot('Dropdown still open'); + + listenerCallback({ key: 'Escape' }); + expect(wrapper).toMatchSnapshot('Dropdown closed'); + + useEffectCleanup(); + expect(removeEventListener).toBeCalledWith('keydown', listenerCallback, false); +}); + +function shallowRender(props: Partial = {}) { + return shallow(); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx new file mode 100644 index 00000000000..a19f3f27bfd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; +import { + HotspotResolution, + HotspotStatus, + HotspotStatusOptions +} from '../../../../types/security-hotspots'; +import HotspotActionsForm from '../HotspotActionsForm'; + +jest.mock('../../../../api/security-hotspots', () => ({ + setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined) +})); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should handle option selection', () => { + const wrapper = shallowRender(); + expect(wrapper.state().selectedOption).toBe(HotspotStatusOptions.FIXED); + wrapper.instance().handleSelectOption(HotspotStatusOptions.SAFE); + expect(wrapper.state().selectedOption).toBe(HotspotStatusOptions.SAFE); +}); + +it('should handle submit', async () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); + await waitAndUpdate(wrapper); + + const preventDefault = jest.fn(); + const promise = wrapper.instance().handleSubmit({ preventDefault } as any); + expect(preventDefault).toBeCalled(); + + expect(wrapper.state().submitting).toBe(true); + await promise; + expect(wrapper.state().submitting).toBe(false); + expect(setSecurityHotspotStatus).toBeCalledWith({ + hotspot: 'key', + status: HotspotStatus.TO_REVIEW + }); + expect(onSubmit).toBeCalled(); + + // SAFE + wrapper.setState({ selectedOption: HotspotStatusOptions.SAFE }); + await waitAndUpdate(wrapper); + await wrapper.instance().handleSubmit({ preventDefault } as any); + expect(setSecurityHotspotStatus).toBeCalledWith({ + hotspot: 'key', + status: HotspotStatus.REVIEWED, + resolution: HotspotResolution.SAFE + }); + + // FIXED + wrapper.setState({ selectedOption: HotspotStatusOptions.FIXED }); + await waitAndUpdate(wrapper); + await wrapper.instance().handleSubmit({ preventDefault } as any); + expect(setSecurityHotspotStatus).toBeCalledWith({ + hotspot: 'key', + status: HotspotStatus.REVIEWED, + resolution: HotspotResolution.FIXED + }); +}); + +it('should handle submit failure', async () => { + const onSubmit = jest.fn(); + (setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure'); + const wrapper = shallowRender({ onSubmit }); + const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any); + expect(wrapper.state().submitting).toBe(true); + await promise.catch(() => {}); + expect(wrapper.state().submitting).toBe(false); + expect(onSubmit).not.toBeCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx new file mode 100644 index 00000000000..b7372b04035 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { HotspotStatusOptions } from '../../../../types/security-hotspots'; +import HotspotActionsForm from '../HotspotActionsForm'; +import HotspotActionsFormRenderer, { + HotspotActionsFormRendererProps +} from '../HotspotActionsFormRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting'); + expect(shallowRender({ selectedOption: HotspotStatusOptions.SAFE })).toMatchSnapshot( + 'safe option selected' + ); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx index 700d0cf0aea..70eaf778cd8 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx @@ -20,8 +20,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots'; -import { mockUser } from '../../../../helpers/testMocks'; -import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer'; +import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks'; +import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; it('should render correctly', () => { const wrapper = shallowRender(); @@ -30,11 +30,14 @@ it('should render correctly', () => { expect( shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) }) ).toMatchSnapshot('deleted assignee'); + expect(shallowRender()).toMatchSnapshot('anonymous user'); + expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in'); }); function shallowRender(props?: Partial) { return shallow( + + + + + + +
+`; + +exports[`should register an eventlistener: Dropdown closed 1`] = ` +
+ +
+`; + +exports[`should register an eventlistener: Dropdown open 1`] = ` +
+ + + + + + +
+`; + +exports[`should register an eventlistener: Dropdown still open 1`] = ` +
+ + + + + + +
+`; + +exports[`should render correctly 1`] = ` +
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap new file mode 100644 index 00000000000..ef7582d5434 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap new file mode 100644 index 00000000000..0c9633ff166 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
+

+ hotspots.form.title +

+
+
+ +

+ hotspots.status_option.FIXED +

+
+
+ hotspots.status_option.FIXED.description +
+
+
+ +

+ hotspots.status_option.SAFE +

+
+
+ hotspots.status_option.SAFE.description +
+
+
+ +

+ hotspots.status_option.ADDITIONAL_REVIEW +

+
+
+ hotspots.status_option.ADDITIONAL_REVIEW.description +
+
+
+
+ + hotspots.form.submit + +
+
+`; + +exports[`should render correctly: Submitting 1`] = ` +
+

+ hotspots.form.title +

+
+
+ +

+ hotspots.status_option.FIXED +

+
+
+ hotspots.status_option.FIXED.description +
+
+
+ +

+ hotspots.status_option.SAFE +

+
+
+ hotspots.status_option.SAFE.description +
+
+
+ +

+ hotspots.status_option.ADDITIONAL_REVIEW +

+
+
+ hotspots.status_option.ADDITIONAL_REVIEW.description +
+
+
+
+ + + hotspots.form.submit + +
+ +`; + +exports[`should render correctly: safe option selected 1`] = ` +
+

+ hotspots.form.title +

+
+
+ +

+ hotspots.status_option.FIXED +

+
+
+ hotspots.status_option.FIXED.description +
+
+
+ +

+ hotspots.status_option.SAFE +

+
+
+ hotspots.status_option.SAFE.description +
+
+
+ +

+ hotspots.status_option.ADDITIONAL_REVIEW +

+
+
+ hotspots.status_option.ADDITIONAL_REVIEW.description +
+
+
+
+ + hotspots.form.submit + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap index edce37a1791..39de7c9e177 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` - -

- '3' is a magic number. -

+
+

+ '3' is a magic number. +

+
+
+ + hotspot.category + + + SQL injection + +
+
+
+ + hotspot.status + + + issue.status.RESOLVED + + + hotspot.assigned_to + + + John Doe + +
+ This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> +
+
+`; + +exports[`should render correctly: anonymous user 1`] = ` + +
+
+
+

+ '3' is a magic number. +

+
@@ -146,9 +289,13 @@ exports[`should render correctly: deleted assignee 1`] = `
-

- '3' is a magic number. -

+
+

+ '3' is a magic number. +

+
@@ -276,3 +423,145 @@ exports[`should render correctly: no hotspot 1`] = ` timeout={100} /> `; + +exports[`should render correctly: user logged in 1`] = ` + +
+
+
+

+ '3' is a magic number. +

+ +
+
+ + hotspot.category + + + SQL injection + +
+
+
+ + hotspot.status + + + issue.status.RESOLVED + + + hotspot.assigned_to + + + John Doe + +
+ This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "RESOLVED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + } + } + /> +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/styles.css b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css index 187f9e9a340..43cfe55e449 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/styles.css +++ b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css @@ -50,3 +50,10 @@ overflow-y: auto; background-color: white; } + +/* +* Align description with label by offsetting by width of radio + margin +*/ +#security_hotspots .radio-button-description { + margin-left: 23px; +} diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index 76f816e19f9..cabf1267559 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -23,6 +23,22 @@ export enum RiskExposure { HIGH = 'HIGH' } +export enum HotspotStatus { + TO_REVIEW = 'TO_REVIEW', + REVIEWED = 'REVIEWED' +} + +export enum HotspotResolution { + FIXED = 'FIXED', + SAFE = 'SAFE' +} + +export enum HotspotStatusOptions { + FIXED = 'FIXED', + SAFE = 'SAFE', + ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW' +} + export interface RawHotspot { assignee?: string; author?: string; @@ -72,3 +88,9 @@ export interface HotspotSearchResponse { hotspots: RawHotspot[]; paging: T.Paging; } + +export interface HotspotSetStatusRequest { + hotspot: string; + status: HotspotStatus; + resolution?: HotspotResolution; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index c565392d3ab..4d815040df9 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -658,6 +658,21 @@ hotspot.assigned_to=Assigned to: hotspot.tabs.risk_description=What's the risk? hotspot.tabs.vulnerability_description=Are you vulnerable? hotspot.tabs.fix_recommendations=How can you fix it? +hotspots.review_hotspot=Review Hotspot + +hotspots.form.title=Mark Security Hotspot as: + +hotspots.form.assign_to=Assign to: +hotspots.form.select_user=Select a user... +hotspots.form.comment=Comment +hotspots.form.submit=Apply changes + +hotspots.status_option.FIXED=Fixed +hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices. +hotspots.status_option.SAFE=Safe +hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. +hotspots.status_option.ADDITIONAL_REVIEW=Needs additional review +hotspots.status_option.ADDITIONAL_REVIEW.description=Someone else needs to review this Security Hotspot. #------------------------------------------------------------------------------ # -- 2.39.5