Browse Source

SONAR-12719 Hotspot resolution form

tags/8.2.0.32929
Jeremy Davis 4 years ago
parent
commit
b3cca30a94
18 changed files with 1186 additions and 15 deletions
  1. 10
    2
      server/sonar-web/src/main/js/api/security-hotspots.ts
  2. 1
    0
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
  3. 68
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx
  4. 89
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx
  5. 81
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx
  6. 12
    3
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx
  7. 70
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx
  8. 101
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx
  9. 47
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
  10. 5
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  11. 112
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap
  12. 11
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
  13. 238
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
  14. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  15. 295
    6
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  16. 7
    0
      server/sonar-web/src/main/js/apps/securityHotspots/styles.css
  17. 22
    0
      server/sonar-web/src/main/js/types/security-hotspots.ts
  18. 15
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 10
- 2
server/sonar-web/src/main/js/api/security-hotspots.ts View File

@@ -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<void> {
return post('/api/hotspots/change_status', data).catch(throwGlobalError);
}

export function getSecurityHotspots(data: {
projectKey: string;

+ 1
- 0
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx View File

@@ -42,6 +42,7 @@ export interface SecurityHotspotsAppRendererProps {

export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
const { hotspots, loading, securityCategories, selectedHotspotKey } = props;

return (
<div id="security_hotspots">
<FilterBar />

+ 68
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx View File

@@ -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 (
<div className="dropdown">
<Button onClick={() => setOpen(!open)}>
{translate('hotspots.review_hotspot')}
<DropdownIcon className="little-spacer-left" />
</Button>

{open && (
<OutsideClickHandler onClickOutside={() => setOpen(false)}>
<DropdownOverlay placement={PopupPlacement.BottomRight}>
<HotspotActionsForm hotspotKey={props.hotspotKey} onSubmit={() => setOpen(false)} />
</DropdownOverlay>
</OutsideClickHandler>
)}
</div>
);
}

+ 89
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx View File

@@ -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<Props, State> {
state: State = {
selectedOption: HotspotStatusOptions.FIXED,
submitting: false
};

handleSelectOption = (selectedOption: HotspotStatusOptions) => {
this.setState({ selectedOption });
};

handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
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 (
<HotspotActionsFormRenderer
hotspotKey={hotspotKey}
onSelectOption={this.handleSelectOption}
onSubmit={this.handleSubmit}
selectedOption={selectedOption}
submitting={submitting}
/>
);
}
}

+ 81
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx View File

@@ -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<HTMLFormElement>) => void;
selectedOption: HotspotStatusOptions;
submitting: boolean;
}

export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) {
const { selectedOption, submitting } = props;

return (
<form className="abs-width-400" onSubmit={props.onSubmit}>
<h2>{translate('hotspots.form.title')}</h2>
<div className="display-flex-column big-spacer-bottom">
{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
})}
</div>
<div className="text-right">
{submitting && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitting}>{translate('hotspots.form.submit')}</SubmitButton>
</div>
</form>
);
}

function renderOption(params: {
option: HotspotStatusOptions;
onClick: (option: HotspotStatusOptions) => void;
selectedOption: HotspotStatusOptions;
}) {
const { onClick, option, selectedOption } = params;
return (
<div className="big-spacer-top">
<Radio checked={selectedOption === option} onCheck={onClick} value={option}>
<h3>{translate('hotspots.status_option', option)}</h3>
</Radio>
<div className="radio-button-description">
{translate('hotspots.status_option', option, 'description')}
</div>
</div>
);
}

+ 12
- 3
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx View File

@@ -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 (
<DeferredSpinner loading={loading}>
{hotspot && (
<div className="big-padded">
<div className="big-spacer-bottom">
<h1>{hotspot.message}</h1>
<div className="display-flex-space-between">
<h1>{hotspot.message}</h1>
{isLoggedIn(currentUser) && <HotspotActions hotspotKey={hotspot.key} />}
</div>
<div className="text-muted">
<span>{translate('hotspot.category')}</span>
<span className="little-spacer-left">
@@ -67,3 +74,5 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps)
</DeferredSpinner>
);
}

export default withCurrentUser(HotspotViewerRenderer);

+ 70
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx View File

@@ -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<HotspotActionsProps> = {}) {
return shallow(<HotspotActions hotspotKey="key" {...props} />);
}

+ 101
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx View File

@@ -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<HotspotActionsForm['props']> = {}) {
return shallow<HotspotActionsForm>(
<HotspotActionsForm hotspotKey="key" onSubmit={jest.fn()} {...props} />
);
}

+ 47
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx View File

@@ -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<HotspotActionsFormRendererProps> = {}) {
return shallow<HotspotActionsForm>(
<HotspotActionsFormRenderer
hotspotKey="key"
onSelectOption={jest.fn()}
onSubmit={jest.fn()}
selectedOption={HotspotStatusOptions.FIXED}
submitting={false}
{...props}
/>
);
}

+ 5
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx View File

@@ -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<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
currentUser={mockCurrentUser()}
hotspot={mockDetailledHotspot()}
loading={false}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}

+ 112
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap View File

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

exports[`should open when clicked 1`] = `
<div
className="dropdown"
>
<Button
onClick={[Function]}
>
hotspots.review_hotspot
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
hotspotKey="key"
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should register an eventlistener: Dropdown closed 1`] = `
<div
className="dropdown"
>
<Button
onClick={[Function]}
>
hotspots.review_hotspot
<DropdownIcon
className="little-spacer-left"
/>
</Button>
</div>
`;

exports[`should register an eventlistener: Dropdown open 1`] = `
<div
className="dropdown"
>
<Button
onClick={[Function]}
>
hotspots.review_hotspot
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
hotspotKey="key"
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should register an eventlistener: Dropdown still open 1`] = `
<div
className="dropdown"
>
<Button
onClick={[Function]}
>
hotspots.review_hotspot
<DropdownIcon
className="little-spacer-left"
/>
</Button>
<OutsideClickHandler
onClickOutside={[Function]}
>
<DropdownOverlay
placement="bottom-right"
>
<HotspotActionsForm
hotspotKey="key"
onSubmit={[Function]}
/>
</DropdownOverlay>
</OutsideClickHandler>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="dropdown"
>
<Button
onClick={[Function]}
>
hotspots.review_hotspot
<DropdownIcon
className="little-spacer-left"
/>
</Button>
</div>
`;

+ 11
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<HotspotActionsFormRenderer
hotspotKey="key"
onSelectOption={[Function]}
onSubmit={[Function]}
selectedOption="FIXED"
submitting={false}
/>
`;

+ 238
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<form
className="abs-width-400"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={true}
onCheck={[MockFunction]}
value="FIXED"
>
<h3>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="SAFE"
>
<h3>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: Submitting 1`] = `
<form
className="abs-width-400"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={true}
onCheck={[MockFunction]}
value="FIXED"
>
<h3>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="SAFE"
>
<h3>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="text-right"
>
<i
className="spinner spacer-right"
/>
<SubmitButton
disabled={true}
>
hotspots.form.submit
</SubmitButton>
</div>
</form>
`;

exports[`should render correctly: safe option selected 1`] = `
<form
className="abs-width-400"
onSubmit={[MockFunction]}
>
<h2>
hotspots.form.title
</h2>
<div
className="display-flex-column big-spacer-bottom"
>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="FIXED"
>
<h3>
hotspots.status_option.FIXED
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.FIXED.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={true}
onCheck={[MockFunction]}
value="SAFE"
>
<h3>
hotspots.status_option.SAFE
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.SAFE.description
</div>
</div>
<div
className="big-spacer-top"
>
<Radio
checked={false}
onCheck={[MockFunction]}
value="ADDITIONAL_REVIEW"
>
<h3>
hotspots.status_option.ADDITIONAL_REVIEW
</h3>
</Radio>
<div
className="radio-button-description"
>
hotspots.status_option.ADDITIONAL_REVIEW.description
</div>
</div>
</div>
<div
className="text-right"
>
<SubmitButton
disabled={false}
>
hotspots.form.submit
</SubmitButton>
</div>
</form>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<HotspotViewerRenderer
<Connect(withCurrentUser(HotspotViewerRenderer))
loading={true}
securityCategories={
Object {
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = `
`;

exports[`should render correctly 2`] = `
<HotspotViewerRenderer
<Connect(withCurrentUser(HotspotViewerRenderer))
hotspot={
Object {
"id": "I am a detailled hotspot",

+ 295
- 6
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -11,9 +11,152 @@ exports[`should render correctly 1`] = `
<div
className="big-spacer-bottom"
>
<h1>
'3' is a magic number.
</h1>
<div
className="display-flex-space-between"
>
<h1>
'3' is a magic number.
</h1>
</div>
<div
className="text-muted"
>
<span>
hotspot.category
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
hotspot.status
</span>
<span
className="badge little-spacer-left"
>
issue.status.RESOLVED
</span>
<span
className="huge-spacer-left"
>
hotspot.assigned_to
</span>
<strong
className="little-spacer-left"
>
John Doe
</strong>
</div>
<HotspotViewerTabs
hotspot={
Object {
"assignee": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"resolution": "FALSE-POSITIVE",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "RESOLVED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: anonymous user 1`] = `
<DeferredSpinner
loading={false}
timeout={100}
>
<div
className="big-padded"
>
<div
className="big-spacer-bottom"
>
<div
className="display-flex-space-between"
>
<h1>
'3' is a magic number.
</h1>
</div>
<div
className="text-muted"
>
@@ -146,9 +289,13 @@ exports[`should render correctly: deleted assignee 1`] = `
<div
className="big-spacer-bottom"
>
<h1>
'3' is a magic number.
</h1>
<div
className="display-flex-space-between"
>
<h1>
'3' is a magic number.
</h1>
</div>
<div
className="text-muted"
>
@@ -276,3 +423,145 @@ exports[`should render correctly: no hotspot 1`] = `
timeout={100}
/>
`;

exports[`should render correctly: user logged in 1`] = `
<DeferredSpinner
loading={false}
timeout={100}
>
<div
className="big-padded"
>
<div
className="big-spacer-bottom"
>
<div
className="display-flex-space-between"
>
<h1>
'3' is a magic number.
</h1>
<HotspotActions
hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
/>
</div>
<div
className="text-muted"
>
<span>
hotspot.category
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
hotspot.status
</span>
<span
className="badge little-spacer-left"
>
issue.status.RESOLVED
</span>
<span
className="huge-spacer-left"
>
hotspot.assigned_to
</span>
<strong
className="little-spacer-left"
>
John Doe
</strong>
</div>
<HotspotViewerTabs
hotspot={
Object {
"assignee": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"resolution": "FALSE-POSITIVE",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "RESOLVED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

+ 7
- 0
server/sonar-web/src/main/js/apps/securityHotspots/styles.css View File

@@ -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;
}

+ 22
- 0
server/sonar-web/src/main/js/types/security-hotspots.ts View File

@@ -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;
}

+ 15
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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.

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save