* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Location } from 'history';
+import { flatMap, range } from 'lodash';
import * as React from 'react';
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { getMeasures } from '../../api/measures';
HotspotResolution,
HotspotStatus,
HotspotStatusFilter,
- HotspotUpdate,
RawHotspot
} from '../../types/security-hotspots';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
loadingMeasure: boolean;
loadingMore: boolean;
securityCategories: T.StandardSecurityCategories;
- selectedHotspotKey: string | undefined;
+ selectedHotspot: RawHotspot | undefined;
filters: HotspotFilters;
}
hotspots: [],
hotspotsPageIndex: 1,
securityCategories: {},
- selectedHotspotKey: undefined,
+ selectedHotspot: undefined,
filters: {
...this.constructFiltersFromProps(props),
status: HotspotStatusFilter.TO_REVIEW
hotspotsTotal: paging.total,
loading: false,
securityCategories: sonarsourceSecurity,
- selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
+ selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined
});
})
.catch(this.handleCallFailure);
}
- fetchSecurityHotspotsReviewed() {
+ fetchSecurityHotspotsReviewed = () => {
const { branchLike, component } = this.props;
const { filters } = this.state;
this.setState({ loadingMeasure: false });
}
});
- }
+ };
fetchSecurityHotspots(page = 1) {
const { branchLike, component, location } = this.props;
hotspotsPageIndex: 1,
hotspotsTotal: paging.total,
loading: false,
- selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined
+ selectedHotspot: hotspots.length > 0 ? hotspots[0] : undefined
});
})
.catch(this.handleCallFailure);
);
};
- handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key });
+ handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot });
- handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => {
- this.setState(({ hotspots }) => {
- const index = hotspots.findIndex(h => h.key === key);
+ handleHotspotUpdate = (hotspotKey: string) => {
+ const { hotspots, hotspotsPageIndex } = this.state;
+ const index = hotspots.findIndex(h => h.key === hotspotKey);
- if (index > -1) {
- const hotspot = {
- ...hotspots[index],
- status,
- resolution
- };
+ return Promise.all(
+ range(hotspotsPageIndex).map(p => this.fetchSecurityHotspots(p + 1 /* pages are 1-indexed */))
+ )
+ .then(hotspotPages => {
+ const allHotspots = flatMap(hotspotPages, 'hotspots');
- return { hotspots: [...hotspots.slice(0, index), hotspot, ...hotspots.slice(index + 1)] };
- }
- return null;
- });
- return this.fetchSecurityHotspotsReviewed();
+ const { paging } = hotspotPages[hotspotPages.length - 1];
+
+ const nextHotspot = allHotspots[Math.min(index, allHotspots.length - 1)];
+
+ this.setState({
+ hotspots: allHotspots,
+ hotspotsPageIndex: paging.pageIndex,
+ hotspotsTotal: paging.total,
+ selectedHotspot: nextHotspot
+ });
+ })
+ .then(this.fetchSecurityHotspotsReviewed);
};
handleShowAllHotspots = () => {
loadingMeasure,
loadingMore,
securityCategories,
- selectedHotspotKey,
+ selectedHotspot,
filters
} = this.state;
onShowAllHotspots={this.handleShowAllHotspots}
onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
- selectedHotspotKey={selectedHotspotKey}
+ selectedHotspot={selectedHotspot}
/>
);
}
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
import { isBranch } from '../../helpers/branch-like';
import { BranchLike } from '../../types/branch-like';
-import {
- HotspotFilters,
- HotspotStatusFilter,
- HotspotUpdate,
- RawHotspot
-} from '../../types/security-hotspots';
+import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots';
import EmptyHotspotsPage from './components/EmptyHotspotsPage';
import FilterBar from './components/FilterBar';
import HotspotList from './components/HotspotList';
loadingMeasure: boolean;
loadingMore: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
- onHotspotClick: (key: string) => void;
+ onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
onShowAllHotspots: () => void;
- onUpdateHotspot: (hotspot: HotspotUpdate) => void;
- selectedHotspotKey?: string;
+ onUpdateHotspot: (hotspotKey: string) => Promise<void>;
+ selectedHotspot: RawHotspot | undefined;
securityCategories: T.StandardSecurityCategories;
}
loadingMeasure,
loadingMore,
securityCategories,
- selectedHotspotKey,
+ selectedHotspot,
filters
} = props;
<DeferredSpinner className="huge-spacer-left big-spacer-top" />
) : (
<>
- {hotspots.length === 0 ? (
+ {hotspots.length === 0 || !selectedHotspot ? (
<EmptyHotspotsPage
filtered={
filters.assignedToMe ||
onHotspotClick={props.onHotspotClick}
onLoadMore={props.onLoadMore}
securityCategories={securityCategories}
- selectedHotspotKey={selectedHotspotKey}
+ selectedHotspot={selectedHotspot}
statusFilter={filters.status}
/>
</div>
<div className="main">
- {selectedHotspotKey && (
- <HotspotViewer
- branchLike={branchLike}
- hotspotKey={selectedHotspotKey}
- onUpdateHotspot={props.onUpdateHotspot}
- securityCategories={securityCategories}
- />
- )}
+ <HotspotViewer
+ branchLike={branchLike}
+ hotspotKey={selectedHotspot.key}
+ onUpdateHotspot={props.onUpdateHotspot}
+ securityCategories={securityCategories}
+ />
</div>
</div>
)}
expect(wrapper.state().loading).toBe(false);
expect(wrapper.state().hotspots).toEqual(hotspots);
- expect(wrapper.state().selectedHotspotKey).toBe(hotspots[0].key);
+ expect(wrapper.state().selectedHotspot).toBe(hotspots[0]);
expect(wrapper.state().securityCategories).toEqual({
cat1: { title: 'cat 1' }
});
const hotspots = [mockRawHotspot(), mockRawHotspot({ key })];
(getSecurityHotspots as jest.Mock).mockResolvedValueOnce({
hotspots,
- paging: { total: 2 }
+ paging: { pageIndex: 1, total: 1252 }
});
const wrapper = shallowRender();
-
await waitAndUpdate(wrapper);
+ wrapper.setState({ hotspotsPageIndex: 2 });
- wrapper
+ jest.clearAllMocks();
+ (getSecurityHotspots as jest.Mock)
+ .mockResolvedValueOnce({
+ hotspots: [mockRawHotspot()],
+ paging: { pageIndex: 1, total: 1251 }
+ })
+ .mockResolvedValueOnce({
+ hotspots: [mockRawHotspot()],
+ paging: { pageIndex: 2, total: 1251 }
+ });
+
+ const selectedHotspotIndex = wrapper
+ .state()
+ .hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key);
+
+ await wrapper
.find(SecurityHotspotsAppRenderer)
.props()
- .onUpdateHotspot({ key, status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE });
+ .onUpdateHotspot(key);
- expect(wrapper.state().hotspots[0]).toEqual(hotspots[0]);
- expect(wrapper.state().hotspots[1]).toEqual({
- ...hotspots[1],
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- expect(getMeasures).toBeCalled();
+ expect(getSecurityHotspots).toHaveBeenCalledTimes(2);
- await waitAndUpdate(wrapper);
- const previousState = wrapper.state();
- wrapper.instance().handleHotspotUpdate({
- key: 'unknown',
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- await waitAndUpdate(wrapper);
- expect(wrapper.state()).toEqual(previousState);
+ expect(wrapper.state().hotspots).toHaveLength(2);
+ expect(wrapper.state().hotspotsPageIndex).toBe(2);
+ expect(wrapper.state().hotspotsTotal).toBe(1251);
+ expect(
+ wrapper.state().hotspots.findIndex(h => h.key === wrapper.state().selectedHotspot?.key)
+ ).toBe(selectedHotspotIndex);
+
+ expect(getMeasures).toBeCalled();
});
it('should handle status filter change', async () => {
.dive()
).toMatchSnapshot();
expect(
- shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspotKey: 'h2' })
+ shallowRender({ hotspots, hotspotsTotal: 3, selectedHotspot: mockRawHotspot({ key: 'h2' }) })
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot();
onShowAllHotspots={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{}}
+ selectedHotspot={undefined}
{...props}
/>
);
<A11ySkipTarget
anchor="security_hotspots_main"
/>
- <div
- className="layout-page"
- >
- <div
- className="sidebar"
- >
- <HotspotList
- hotspots={
- Array [
- Object {
- "author": "Developer 1",
- "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
- "creationDate": "2013-05-13T17:55:39+0200",
- "key": "h1",
- "line": 81,
- "message": "'3' is a magic number.",
- "project": "com.github.kevinsawicki:http-request",
- "resolution": undefined,
- "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "command-injection",
- "status": "TO_REVIEW",
- "updateDate": "2013-05-13T17:55:39+0200",
- "vulnerabilityProbability": "HIGH",
- },
- Object {
- "author": "Developer 1",
- "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
- "creationDate": "2013-05-13T17:55:39+0200",
- "key": "h2",
- "line": 81,
- "message": "'3' is a magic number.",
- "project": "com.github.kevinsawicki:http-request",
- "resolution": undefined,
- "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "command-injection",
- "status": "TO_REVIEW",
- "updateDate": "2013-05-13T17:55:39+0200",
- "vulnerabilityProbability": "HIGH",
- },
- ]
- }
- hotspotsTotal={2}
- isStaticListOfHotspots={true}
- loadingMore={false}
- onHotspotClick={[MockFunction]}
- onLoadMore={[MockFunction]}
- securityCategories={Object {}}
- statusFilter="TO_REVIEW"
- />
- </div>
- <div
- className="main"
- />
- </div>
+ <EmptyHotspotsPage
+ filtered={false}
+ isStaticListOfHotspots={true}
+ />
</div>
</div>
`;
onHotspotClick={[MockFunction]}
onLoadMore={[MockFunction]}
securityCategories={Object {}}
- selectedHotspotKey="h2"
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
statusFilter="TO_REVIEW"
/>
</div>
});
describe('groupByCategory', () => {
- it('should group and sort properly', () => {
+ it('should group properly', () => {
const result = groupByCategory(hotspots, categories);
expect(result).toHaveLength(7);
- expect(result.map(g => g.key)).toEqual([
- 'xss',
- 'dos',
- 'log-injection',
- 'object-injection',
- 'ssrf',
- 'xxe',
- 'xpath-injection'
- ]);
});
});
import HotspotListItem from './HotspotListItem';
export interface HotspotCategoryProps {
+ categoryKey: string;
+ expanded: boolean;
hotspots: RawHotspot[];
- onHotspotClick: (key: string) => void;
- selectedHotspotKey: string | undefined;
- startsExpanded: boolean;
+ onHotspotClick: (hotspot: RawHotspot) => void;
+ onToggleExpand: (categoryKey: string, value: boolean) => void;
+ selectedHotspot: RawHotspot;
title: string;
}
export default function HotspotCategory(props: HotspotCategoryProps) {
- const { hotspots, selectedHotspotKey, startsExpanded, title } = props;
-
- const [expanded, setExpanded] = React.useState(startsExpanded);
+ const { categoryKey, expanded, hotspots, selectedHotspot, title } = props;
if (hotspots.length < 1) {
return null;
return (
<div className={classNames('hotspot-category', risk)}>
<a
- className="hotspot-category-header display-flex-space-between display-flex-center"
+ className={classNames(
+ 'hotspot-category-header display-flex-space-between display-flex-center',
+ { 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey }
+ )}
href="#"
- onClick={() => setExpanded(!expanded)}>
+ onClick={() => props.onToggleExpand(categoryKey, !expanded)}>
<strong className="flex-1">{title}</strong>
<span>
<span className="counter-badge">{hotspots.length}</span>
<HotspotListItem
hotspot={h}
onClick={props.onHotspotClick}
- selected={h.key === selectedHotspotKey}
+ selected={h.key === selectedHotspot.key}
/>
</li>
))}
border-left: 4px solid;
}
-.hotspot-category .hotspot-category-header:hover {
+.hotspot-category .hotspot-category-header:hover,
+.hotspot-category .hotspot-category-header.contains-selected-hotspot {
color: var(--blue);
}
import HotspotCategory from './HotspotCategory';
import './HotspotList.css';
-export interface HotspotListProps {
+interface Props {
hotspots: RawHotspot[];
hotspotsTotal?: number;
isStaticListOfHotspots: boolean;
loadingMore: boolean;
- onHotspotClick: (key: string) => void;
+ onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
securityCategories: T.StandardSecurityCategories;
- selectedHotspotKey: string | undefined;
+ selectedHotspot: RawHotspot;
statusFilter: HotspotStatusFilter;
}
-export default function HotspotList(props: HotspotListProps) {
- const {
- hotspots,
- hotspotsTotal,
- isStaticListOfHotspots,
- loadingMore,
- securityCategories,
- selectedHotspotKey,
- statusFilter
- } = props;
-
- const groupedHotspots: Array<{
+interface State {
+ expandedCategories: T.Dict<boolean>;
+ groupedHotspots: Array<{
risk: RiskExposure;
categories: Array<{ key: string; hotspots: RawHotspot[]; title: string }>;
- }> = React.useMemo(() => {
+ }>;
+}
+
+export default class HotspotList extends React.Component<Props, State> {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ expandedCategories: { [props.selectedHotspot.securityCategory]: true },
+ groupedHotspots: this.groupHotspots(props.hotspots, props.securityCategories)
+ };
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ // Force open the category of selected hotspot
+ if (
+ this.props.selectedHotspot.securityCategory !== prevProps.selectedHotspot.securityCategory
+ ) {
+ this.handleToggleCategory(this.props.selectedHotspot.securityCategory, true);
+ }
+
+ // Compute the hotspot tree from the list
+ if (
+ this.props.hotspots !== prevProps.hotspots ||
+ this.props.securityCategories !== prevProps.securityCategories
+ ) {
+ const groupedHotspots = this.groupHotspots(
+ this.props.hotspots,
+ this.props.securityCategories
+ );
+ this.setState({ groupedHotspots });
+ }
+ }
+
+ groupHotspots = (hotspots: RawHotspot[], securityCategories: T.StandardSecurityCategories) => {
const risks = groupBy(hotspots, h => h.vulnerabilityProbability);
return RISK_EXPOSURE_LEVELS.map(risk => ({
risk,
categories: groupByCategory(risks[risk], securityCategories)
})).filter(risk => risk.categories.length > 0);
- }, [hotspots, securityCategories]);
+ };
+
+ handleToggleCategory = (categoryKey: string, value: boolean) => {
+ this.setState(({ expandedCategories }) => ({
+ expandedCategories: { ...expandedCategories, [categoryKey]: value }
+ }));
+ };
+
+ render() {
+ const {
+ hotspots,
+ hotspotsTotal,
+ isStaticListOfHotspots,
+ loadingMore,
+ selectedHotspot,
+ statusFilter
+ } = this.props;
+
+ const { expandedCategories, groupedHotspots } = this.state;
- return (
- <div className="huge-spacer-bottom">
- <h1 className="hotspot-list-header bordered-bottom">
- <SecurityHotspotIcon className="spacer-right" />
- {translateWithParameters(
- isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
- hotspots.length
- )}
- </h1>
- <ul className="big-spacer-bottom">
- {groupedHotspots.map((riskGroup, groupIndex) => (
- <li className="big-spacer-bottom" key={riskGroup.risk}>
- <div className="hotspot-risk-header little-spacer-left">
- <span>{translate('hotspots.risk_exposure')}</span>
- <div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}>
- {translate('risk_exposure', riskGroup.risk)}
+ return (
+ <div className="huge-spacer-bottom">
+ <h1 className="hotspot-list-header bordered-bottom">
+ <SecurityHotspotIcon className="spacer-right" />
+ {translateWithParameters(
+ isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`,
+ hotspots.length
+ )}
+ </h1>
+ <ul className="big-spacer-bottom">
+ {groupedHotspots.map(riskGroup => (
+ <li className="big-spacer-bottom" key={riskGroup.risk}>
+ <div className="hotspot-risk-header little-spacer-left">
+ <span>{translate('hotspots.risk_exposure')}</span>
+ <div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}>
+ {translate('risk_exposure', riskGroup.risk)}
+ </div>
</div>
- </div>
- <ul>
- {riskGroup.categories.map((cat, catIndex) => (
- <li className="spacer-bottom" key={cat.key}>
- <HotspotCategory
- hotspots={cat.hotspots}
- onHotspotClick={props.onHotspotClick}
- selectedHotspotKey={selectedHotspotKey}
- startsExpanded={groupIndex === 0 && catIndex === 0}
- title={cat.title}
- />
- </li>
- ))}
- </ul>
- </li>
- ))}
- </ul>
- <ListFooter
- count={hotspots.length}
- loadMore={!loadingMore ? props.onLoadMore : undefined}
- loading={loadingMore}
- total={hotspotsTotal}
- />
- </div>
- );
+ <ul>
+ {riskGroup.categories.map(cat => (
+ <li className="spacer-bottom" key={cat.key}>
+ <HotspotCategory
+ categoryKey={cat.key}
+ expanded={expandedCategories[cat.key]}
+ hotspots={cat.hotspots}
+ onHotspotClick={this.props.onHotspotClick}
+ onToggleExpand={this.handleToggleCategory}
+ selectedHotspot={selectedHotspot}
+ title={cat.title}
+ />
+ </li>
+ ))}
+ </ul>
+ </li>
+ ))}
+ </ul>
+ <ListFooter
+ count={hotspots.length}
+ loadMore={!loadingMore ? this.props.onLoadMore : undefined}
+ loading={loadingMore}
+ total={hotspotsTotal}
+ />
+ </div>
+ );
+ }
}
export interface HotspotListItemProps {
hotspot: RawHotspot;
- onClick: (key: string) => void;
+ onClick: (hotspot: RawHotspot) => void;
selected: boolean;
}
<a
className={classNames('hotspot-item', { highlight: selected })}
href="#"
- onClick={() => !selected && props.onClick(hotspot.key)}>
+ onClick={() => !selected && props.onClick(hotspot)}>
<div className="little-spacer-left">{hotspot.message}</div>
<div className="badge spacer-top">
{translate(
import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { BranchLike } from '../../../types/branch-like';
-import { Hotspot, HotspotUpdate } from '../../../types/security-hotspots';
+import { Hotspot } from '../../../types/security-hotspots';
import HotspotViewerRenderer from './HotspotViewerRenderer';
interface Props {
branchLike?: BranchLike;
hotspotKey: string;
- onUpdateHotspot: (hotspot: HotspotUpdate) => void;
+ onUpdateHotspot: (hotspotKey: string) => Promise<void>;
securityCategories: T.StandardSecurityCategories;
}
handleHotspotUpdate = () => {
return this.fetchHotspot().then((hotspot?: Hotspot) => {
if (hotspot) {
- this.props.onUpdateHotspot({
- key: hotspot.key,
- status: hotspot.status,
- resolution: hotspot.resolution
- });
+ return this.props.onUpdateHotspot(hotspot.key);
}
});
};
branchLike?: BranchLike;
hotspot?: Hotspot;
loading: boolean;
- onUpdateHotspot: () => void;
+ onUpdateHotspot: () => Promise<void>;
securityCategories: T.StandardSecurityCategories;
}
});
it('should render correctly with hotspots', () => {
- const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
+ const securityCategory = 'command-injection';
+ const hotspots = [
+ mockRawHotspot({ key: 'h1', securityCategory }),
+ mockRawHotspot({ key: 'h2', securityCategory })
+ ];
expect(shallowRender({ hotspots })).toMatchSnapshot();
- expect(shallowRender({ hotspots, startsExpanded: false })).toMatchSnapshot('collapsed');
+ expect(shallowRender({ hotspots, expanded: false })).toMatchSnapshot('collapsed');
+ expect(
+ shallowRender({ categoryKey: securityCategory, hotspots, selectedHotspot: hotspots[0] })
+ ).toMatchSnapshot('contains selected');
});
it('should handle collapse and expand', () => {
- const wrapper = shallowRender({ hotspots: [mockRawHotspot()] });
+ const onToggleExpand = jest.fn();
+
+ const categoryKey = 'xss-injection';
+
+ const wrapper = shallowRender({
+ categoryKey,
+ expanded: true,
+ hotspots: [mockRawHotspot()],
+ onToggleExpand
+ });
wrapper.find('.hotspot-category-header').simulate('click');
- expect(wrapper).toMatchSnapshot();
+ expect(onToggleExpand).toBeCalledWith(categoryKey, false);
+ wrapper.setProps({ expanded: false });
wrapper.find('.hotspot-category-header').simulate('click');
- expect(wrapper).toMatchSnapshot();
+ expect(onToggleExpand).toBeCalledWith(categoryKey, true);
});
function shallowRender(props: Partial<HotspotCategoryProps> = {}) {
return shallow(
<HotspotCategory
+ categoryKey="xss-injection"
+ expanded={true}
hotspots={[]}
onHotspotClick={jest.fn()}
- selectedHotspotKey=""
- startsExpanded={true}
+ onToggleExpand={jest.fn()}
+ selectedHotspot={mockRawHotspot()}
title="Class Injection"
{...props}
/>
import * as React from 'react';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
import { HotspotStatusFilter, RiskExposure } from '../../../../types/security-hotspots';
-import HotspotList, { HotspotListProps } from '../HotspotList';
+import HotspotList from '../HotspotList';
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot();
});
+const hotspots = [
+ mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
+ mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
+ mockRawHotspot({
+ key: 'h3',
+ securityCategory: 'cat1',
+ vulnerabilityProbability: RiskExposure.MEDIUM
+ }),
+ mockRawHotspot({
+ key: 'h4',
+ securityCategory: 'cat1',
+ vulnerabilityProbability: RiskExposure.MEDIUM
+ }),
+ mockRawHotspot({
+ key: 'h5',
+ securityCategory: 'cat2',
+ vulnerabilityProbability: RiskExposure.MEDIUM
+ })
+];
+
it('should render correctly with hotspots', () => {
- const hotspots = [
- mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
- mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
- mockRawHotspot({
- key: 'h3',
- securityCategory: 'cat1',
- vulnerabilityProbability: RiskExposure.MEDIUM
- }),
- mockRawHotspot({
- key: 'h4',
- securityCategory: 'cat1',
- vulnerabilityProbability: RiskExposure.MEDIUM
- }),
- mockRawHotspot({
- key: 'h5',
- securityCategory: 'cat2',
- vulnerabilityProbability: RiskExposure.MEDIUM
- })
- ];
expect(shallowRender({ hotspots })).toMatchSnapshot('no pagination');
expect(shallowRender({ hotspots, hotspotsTotal: 7 })).toMatchSnapshot('pagination');
});
-function shallowRender(props: Partial<HotspotListProps> = {}) {
- return shallow(
+it('should update expanded categories correctly', () => {
+ const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] });
+
+ expect(wrapper.state().expandedCategories).toEqual({ cat2: true });
+
+ wrapper.setProps({ selectedHotspot: hotspots[1] });
+
+ expect(wrapper.state().expandedCategories).toEqual({ cat1: true, cat2: true });
+});
+
+it('should update grouped hotspots when the list changes', () => {
+ const wrapper = shallowRender({ hotspots, selectedHotspot: hotspots[0] });
+
+ wrapper.setProps({ hotspots: [mockRawHotspot()] });
+
+ expect(wrapper.state().groupedHotspots).toHaveLength(1);
+ expect(wrapper.state().groupedHotspots[0].categories).toHaveLength(1);
+ expect(wrapper.state().groupedHotspots[0].categories[0].hotspots).toHaveLength(1);
+});
+
+function shallowRender(props: Partial<HotspotList['props']> = {}) {
+ return shallow<HotspotList>(
<HotspotList
hotspots={[]}
isStaticListOfHotspots={false}
onHotspotClick={jest.fn()}
onLoadMore={jest.fn()}
securityCategories={{}}
- selectedHotspotKey="h2"
+ selectedHotspot={mockRawHotspot({ key: 'h2' })}
statusFilter={HotspotStatusFilter.TO_REVIEW}
{...props}
/>
wrapper.simulate('click');
- expect(onClick).toBeCalledWith(hotspot.key);
+ expect(onClick).toBeCalledWith(hotspot);
});
function shallowRender(props: Partial<HotspotListItemProps> = {}) {
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should handle collapse and expand 1`] = `
-<div
- className="hotspot-category HIGH"
->
- <a
- className="hotspot-category-header display-flex-space-between display-flex-center"
- href="#"
- onClick={[Function]}
- >
- <strong
- className="flex-1"
- >
- Class Injection
- </strong>
- <span>
- <span
- className="counter-badge"
- >
- 1
- </span>
- <ChevronDownIcon
- className="big-spacer-left"
- />
- </span>
- </a>
-</div>
-`;
-
-exports[`should handle collapse and expand 2`] = `
+exports[`should render correctly with hotspots 1`] = `
<div
className="hotspot-category HIGH"
>
<span
className="counter-badge"
>
- 1
+ 2
</span>
<ChevronUpIcon
className="big-spacer-left"
</a>
<ul>
<li
- key="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ key="h1"
+ >
+ <HotspotListItem
+ hotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h1",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
+ onClick={[MockFunction]}
+ selected={false}
+ />
+ </li>
+ <li
+ key="h2"
>
<HotspotListItem
hotspot={
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
</div>
`;
-exports[`should render correctly with hotspots 1`] = `
+exports[`should render correctly with hotspots: collapsed 1`] = `
<div
className="hotspot-category HIGH"
>
className="hotspot-category-header display-flex-space-between display-flex-center"
href="#"
onClick={[Function]}
+ >
+ <strong
+ className="flex-1"
+ >
+ Class Injection
+ </strong>
+ <span>
+ <span
+ className="counter-badge"
+ >
+ 2
+ </span>
+ <ChevronDownIcon
+ className="big-spacer-left"
+ />
+ </span>
+ </a>
+</div>
+`;
+
+exports[`should render correctly with hotspots: contains selected 1`] = `
+<div
+ className="hotspot-category HIGH"
+>
+ <a
+ className="hotspot-category-header display-flex-space-between display-flex-center contains-selected-hotspot"
+ href="#"
+ onClick={[Function]}
>
<strong
className="flex-1"
}
}
onClick={[MockFunction]}
- selected={false}
+ selected={true}
/>
</li>
<li
</div>
`;
-exports[`should render correctly with hotspots: collapsed 1`] = `
-<div
- className="hotspot-category HIGH"
->
- <a
- className="hotspot-category-header display-flex-space-between display-flex-center"
- href="#"
- onClick={[Function]}
- >
- <strong
- className="flex-1"
- >
- Class Injection
- </strong>
- <span>
- <span
- className="counter-badge"
- >
- 2
- </span>
- <ChevronDownIcon
- className="big-spacer-left"
- />
- </span>
- </a>
-</div>
-`;
-
exports[`should render correctly: empty 1`] = `""`;
<ul>
<li
className="spacer-bottom"
- key="cat1"
+ key="cat2"
>
<HotspotCategory
+ categoryKey="cat2"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
- "key": "h2",
+ "key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "cat1",
+ "securityCategory": "cat2",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={true}
- title="cat1"
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
+ title="cat2"
/>
</li>
<li
className="spacer-bottom"
- key="cat2"
+ key="cat1"
>
<HotspotCategory
+ categoryKey="cat1"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
- "key": "h1",
+ "key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "cat2",
+ "securityCategory": "cat1",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
- title="cat2"
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
+ title="cat1"
/>
</li>
</ul>
key="cat1"
>
<HotspotCategory
+ categoryKey="cat1"
hotspots={
Array [
Object {
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
title="cat1"
/>
</li>
key="cat2"
>
<HotspotCategory
+ categoryKey="cat2"
hotspots={
Array [
Object {
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
title="cat2"
/>
</li>
<ul>
<li
className="spacer-bottom"
- key="cat1"
+ key="cat2"
>
<HotspotCategory
+ categoryKey="cat2"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
- "key": "h2",
+ "key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "cat1",
+ "securityCategory": "cat2",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={true}
- title="cat1"
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
+ title="cat2"
/>
</li>
<li
className="spacer-bottom"
- key="cat2"
+ key="cat1"
>
<HotspotCategory
+ categoryKey="cat1"
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
- "key": "h1",
+ "key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
- "securityCategory": "cat2",
+ "securityCategory": "cat1",
"status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
- title="cat2"
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
+ title="cat1"
/>
</li>
</ul>
key="cat1"
>
<HotspotCategory
+ categoryKey="cat1"
hotspots={
Array [
Object {
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
title="cat1"
/>
</li>
key="cat2"
>
<HotspotCategory
+ categoryKey="cat2"
hotspots={
Array [
Object {
]
}
onHotspotClick={[MockFunction]}
- selectedHotspotKey="h2"
- startsExpanded={false}
+ onToggleExpand={[Function]}
+ selectedHotspot={
+ Object {
+ "author": "Developer 1",
+ "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+ "creationDate": "2013-05-13T17:55:39+0200",
+ "key": "h2",
+ "line": 81,
+ "message": "'3' is a magic number.",
+ "project": "com.github.kevinsawicki:http-request",
+ "resolution": undefined,
+ "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+ "securityCategory": "command-injection",
+ "status": "TO_REVIEW",
+ "updateDate": "2013-05-13T17:55:39+0200",
+ "vulnerabilityProbability": "HIGH",
+ }
+ }
title="cat2"
/>
</li>
*/
import * as React from 'react';
+import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { assignSecurityHotspot } from '../../../../api/security-hotspots';
+import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
import { withCurrentUser } from '../../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../../helpers/users';
import { Hotspot, HotspotStatus } from '../../../../types/security-hotspots';
this.props.onAssigneeChange();
}
})
+ .then(() =>
+ addGlobalSuccessMessage(
+ translateWithParameters('hotspots.assign.success', newAssignee.name)
+ )
+ )
.catch(() => this.setState({ loading: false }));
}
};
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { assignSecurityHotspot } from '../../../../../api/security-hotspots';
+import addGlobalSuccessMessage from '../../../../../app/utils/addGlobalSuccessMessage';
import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockUser } from '../../../../../helpers/testMocks';
import { HotspotStatus } from '../../../../../types/security-hotspots';
assignSecurityHotspot: jest.fn()
}));
+jest.mock('../../../../../app/utils/addGlobalSuccessMessage', () => ({
+ default: jest.fn()
+}));
+
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(
loading: false
});
expect(onAssigneeChange).toHaveBeenCalled();
+ expect(addGlobalSuccessMessage).toHaveBeenCalled();
});
function shallowRender(props?: Partial<Assignee['props']>) {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
+import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage';
import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
import {
getStatusAndResolutionFromStatusOption,
this.setState({ loading: false });
this.props.onStatusOptionChange(selectedStatus);
})
+ .then(() =>
+ addGlobalSuccessMessage(
+ translateWithParameters(
+ 'hotspots.update.success',
+ translate('hotspots.status_option', selectedStatus)
+ )
+ )
+ )
.catch(() => this.setState({ loading: false }));
}
};
) {
const groups = groupBy(hotspots, h => h.securityCategory);
- return sortBy(
- Object.keys(groups).map(key => ({
- key,
- title: getCategoryTitle(key, securityCategories),
- hotspots: groups[key]
- })),
- cat => cat.title
- );
+ return Object.keys(groups).map(key => ({
+ key,
+ title: getCategoryTitle(key, securityCategories),
+ hotspots: groups[key]
+ }));
}
export function sortHotspots(
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot
+hotspots.assign.success=Security Hotspot was successfully assigned to {0}
+hotspots.update.success=Security Hotspot status was successfully changed to {0}
+
#------------------------------------------------------------------------------
#
# ISSUES