ソースを参照

SONAR-12720 Review tab displays the changelog of the hotspot

tags/8.2.0.32929
Philippe Perrin 4年前
コミット
6c3ab41a66
29個のファイルの変更1208行の追加145行の削除
  1. 2
    2
      server/sonar-web/src/main/js/api/security-hotspots.ts
  2. 11
    0
      server/sonar-web/src/main/js/app/styles/components/badges.css
  3. 4
    0
      server/sonar-web/src/main/js/app/theme.js
  4. 51
    3
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts
  5. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx
  6. 1
    1
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx
  7. 0
    7
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css
  8. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainer.tsx
  9. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx
  10. 6
    9
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx
  11. 5
    5
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx
  12. 80
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerReviewHistoryTab.tsx
  13. 47
    24
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx
  14. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx
  15. 5
    5
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx
  16. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx
  17. 13
    3
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  18. 52
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerReviewHistoryTab-test.tsx
  19. 10
    9
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx
  20. 3
    3
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
  21. 1
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap
  22. 1
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap
  23. 453
    12
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  24. 119
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerReviewHistoryTab-test.tsx.snap
  25. 250
    36
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
  26. 41
    2
      server/sonar-web/src/main/js/apps/securityHotspots/utils.ts
  27. 19
    7
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  28. 17
    4
      server/sonar-web/src/main/js/types/security-hotspots.ts
  29. 7
    3
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 2
server/sonar-web/src/main/js/api/security-hotspots.ts ファイルの表示

@@ -21,7 +21,7 @@ import { getJSON, post } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import { BranchParameters } from '../types/branch-like';
import {
DetailedHotspot,
Hotspot,
HotspotAssignRequest,
HotspotResolution,
HotspotSearchResponse,
@@ -58,6 +58,6 @@ export function getSecurityHotspots(
return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
}

export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<DetailedHotspot> {
export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<Hotspot> {
return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey }).catch(throwGlobalError);
}

+ 11
- 0
server/sonar-web/src/main/js/app/styles/components/badges.css ファイルの表示

@@ -69,3 +69,14 @@ a.badge {
background-color: var(--alertBackgroundError);
color: var(--alertTextError);
}

.counter-badge {
color: var(--badgeBlueColor);
background-color: var(--badgeBlueBackground);
padding: calc(var(--gridSize) / 2) var(--gridSize);
border-radius: 1em;
}

.counter-badge:empty {
display: none;
}

+ 4
- 0
server/sonar-web/src/main/js/app/theme.js ファイルの表示

@@ -103,6 +103,10 @@ module.exports = {
alertTextInfo: '#0e516f',
alertIconInfo: '#0271b9',

// badge
badgeBlueBackground: '#2E7CB5',
badgeBlueColor: '#FFFFFF',

// alm
azure: '#0078d7',
bitbucket: '#0052CC',

+ 51
- 3
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts ファイルの表示

@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { RiskExposure } from '../../../types/security-hotspots';
import { groupByCategory, mapRules, sortHotspots } from '../utils';
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots';
import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils';

const hotspots = [
mockRawHotspot({
@@ -142,3 +142,51 @@ describe('mapRules', () => {
});
});
});

describe('getHotspotReviewHistory', () => {
it('should properly create the review history', () => {
const changelogElement: T.IssueChangelog = {
creationDate: '2018-10-01',
isUserActive: true,
user: 'me',
userName: 'me-name',
diffs: [
{
key: 'assign',
newValue: 'me',
oldValue: 'him'
}
]
};
const hotspot = mockHotspot({
creationDate: '2018-09-01',
changelog: [changelogElement]
});
const history = getHotspotReviewHistory(hotspot);

expect(history.length).toBe(2);
expect(history[0]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Creation,
date: hotspot.creationDate,
user: {
avatar: hotspot.author.avatar,
name: hotspot.author.name,
active: hotspot.author.active
}
})
);
expect(history[1]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Diff,
date: changelogElement.creationDate,
user: {
avatar: changelogElement.avatar,
name: changelogElement.userName,
active: changelogElement.isUserActive
},
diffs: changelogElement.diffs
})
);
});
});

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActions.tsx ファイルの表示

@@ -24,11 +24,11 @@ import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClic
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 { DetailedHotspot, HotspotUpdateFields } from '../../../types/security-hotspots';
import { Hotspot, HotspotUpdateFields } from '../../../types/security-hotspots';
import HotspotActionsForm from './HotspotActionsForm';

export interface HotspotActionsProps {
hotspot: DetailedHotspot;
hotspot: Hotspot;
onSubmit: (hotspot: HotspotUpdateFields) => void;
}


+ 1
- 1
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx ファイルの表示

@@ -53,7 +53,7 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
onClick={() => setExpanded(!expanded)}>
<strong className="flex-1">{category.title}</strong>
<span>
<span className="hotspot-counter">{hotspots.length}</span>
<span className="counter-badge">{hotspots.length}</span>
{expanded ? (
<ChevronUpIcon className="big-spacer-left" />
) : (

+ 0
- 7
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css ファイルの表示

@@ -75,13 +75,6 @@
cursor: unset;
}

.hotspot-counter {
color: var(--baseFontColor);
background-color: var(--gray94);
border-radius: 50%;
padding: calc(var(--gridSize) / 2) var(--gridSize);
}

.hotspot-risk-badge {
color: white;
text-transform: uppercase;

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainer.tsx ファイルの表示

@@ -22,13 +22,13 @@ import { getSources } from '../../../api/components';
import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { BranchLike } from '../../../types/branch-like';
import { DetailedHotspot } from '../../../types/security-hotspots';
import { Hotspot } from '../../../types/security-hotspots';
import { constructSourceViewerFile } from '../utils';
import HotspotSnippetContainerRenderer from './HotspotSnippetContainerRenderer';

interface Props {
branchLike?: BranchLike;
hotspot: DetailedHotspot;
hotspot: Hotspot;
}

interface State {

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotSnippetContainerRenderer.tsx ファイルの表示

@@ -22,13 +22,13 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
import { BranchLike } from '../../../types/branch-like';
import { DetailedHotspot } from '../../../types/security-hotspots';
import { Hotspot } from '../../../types/security-hotspots';
import SnippetViewer from '../../issues/crossComponentSourceViewer/SnippetViewer';

export interface HotspotSnippetContainerRendererProps {
branchLike?: BranchLike;
highlightedSymbols: string[];
hotspot: DetailedHotspot;
hotspot: Hotspot;
lastLine?: number;
loading: boolean;
locations: { [line: number]: T.LinearIssueLocation[] };

+ 6
- 9
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx ファイルの表示

@@ -21,11 +21,7 @@
import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { BranchLike } from '../../../types/branch-like';
import {
DetailedHotspot,
HotspotUpdate,
HotspotUpdateFields
} from '../../../types/security-hotspots';
import { Hotspot, HotspotUpdate, HotspotUpdateFields } from '../../../types/security-hotspots';
import HotspotViewerRenderer from './HotspotViewerRenderer';

interface Props {
@@ -36,14 +32,15 @@ interface Props {
}

interface State {
hotspot?: DetailedHotspot;
hotspot?: Hotspot;
loading: boolean;
}

export default class HotspotViewer extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };

componentWillMount() {
componentDidMount() {
this.mounted = true;
this.fetchHotspot();
}
@@ -61,8 +58,8 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
fetchHotspot() {
this.setState({ loading: true });
return getSecurityHotspotDetails(this.props.hotspotKey)
.then(hotspot => this.mounted && this.setState({ hotspot }))
.finally(() => this.mounted && this.setState({ loading: false }));
.then(hotspot => this.mounted && this.setState({ hotspot, loading: false }))
.catch(() => this.mounted && this.setState({ loading: false }));
}

handleHotspotUpdate = (data: HotspotUpdateFields) => {

+ 5
- 5
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx ファイルの表示

@@ -23,7 +23,7 @@ import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like';
import { DetailedHotspot, HotspotUpdateFields } from '../../../types/security-hotspots';
import { Hotspot, HotspotUpdateFields } from '../../../types/security-hotspots';
import HotspotActions from './HotspotActions';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import HotspotViewerTabs from './HotspotViewerTabs';
@@ -31,7 +31,7 @@ import HotspotViewerTabs from './HotspotViewerTabs';
export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
currentUser: T.CurrentUser;
hotspot?: DetailedHotspot;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: (hotspot: HotspotUpdateFields) => void;
securityCategories: T.StandardSecurityCategories;
@@ -52,20 +52,20 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
)}
</div>
<div className="text-muted">
<span>{translate('hotspot.category')}</span>
<span>{translate('category')}:</span>
<span className="little-spacer-left">
{securityCategories[hotspot.rule.securityCategory].title}
</span>
</div>
</div>
<div className="huge-spacer-bottom">
<span>{translate('hotspot.status')}</span>
<span>{translate('status')}:</span>
<span className="badge little-spacer-left">
{translate('hotspot.status', hotspot.resolution || hotspot.status)}
</span>
{hotspot.assignee && hotspot.assignee.name && (
<>
<span className="huge-spacer-left">{translate('hotspot.assigned_to')}</span>
<span className="huge-spacer-left">{translate('assigned_to')}:</span>
<strong className="little-spacer-left">
{hotspot.assignee.active
? hotspot.assignee.name

+ 80
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerReviewHistoryTab.tsx ファイルの表示

@@ -0,0 +1,80 @@
/*
* 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 { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff';
import Avatar from '../../../components/ui/Avatar';
import { ReviewHistoryElement, ReviewHistoryType } from '../../../types/security-hotspots';

export interface HotspotViewerReviewHistoryTabProps {
history: ReviewHistoryElement[];
}

export default function HotspotViewerReviewHistoryTab(props: HotspotViewerReviewHistoryTabProps) {
const { history } = props;

return (
<>
{history.map((elt, i) => (
<React.Fragment key={`${elt.user.name}-${elt.date}`}>
{i > 0 && <hr />}
<div>
<div className="display-flex-center">
{elt.user.name && (
<>
<Avatar
className="little-spacer-right"
hash={elt.user.avatar}
name={elt.user.name}
size={20}
/>
<strong>
{elt.user.active
? elt.user.name
: translateWithParameters('user.x_deleted', elt.user.name)}
</strong>
{elt.type === ReviewHistoryType.Creation && (
<span className="little-spacer-left">
{translate('hotspots.tabs.review_history.created')}
</span>
)}
<span className="little-spacer-left little-spacer-right">-</span>
</>
)}
<DateTimeFormatter date={elt.date} />
</div>

{elt.type === ReviewHistoryType.Diff && elt.diffs && (
<div className="spacer-top">
{elt.diffs.map(diff => (
<IssueChangelogDiff
diff={diff}
key={`${diff.key}-${diff.oldValue}-${diff.newValue}`}
/>
))}
</div>
)}
</div>
</React.Fragment>
))}
</>
);
}

+ 47
- 24
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx ファイルの表示

@@ -21,56 +21,79 @@ import { sanitize } from 'dompurify';
import * as React from 'react';
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { DetailedHotspot } from '../../../types/security-hotspots';
import { Hotspot } from '../../../types/security-hotspots';
import { getHotspotReviewHistory } from '../utils';
import HotspotViewerReviewHistoryTab from './HotspotViewerReviewHistoryTab';

export interface HotspotViewerTabsProps {
hotspot: DetailedHotspot;
hotspot: Hotspot;
}

export enum Tabs {
RiskDescription = 'risk',
VulnerabilityDescription = 'vulnerability',
FixRecommendation = 'fix'
FixRecommendation = 'fix',
ReviewHistory = 'review'
}

export default function HotspotViewerTabs(props: HotspotViewerTabsProps) {
const { hotspot } = props;
const [currentTab, setCurrentTab] = React.useState(Tabs.RiskDescription);
const [currentTabKey, setCurrentTabKey] = React.useState(Tabs.RiskDescription);
const hotspotReviewHistory = React.useMemo(() => getHotspotReviewHistory(hotspot), [hotspot]);

const tabs = {
[Tabs.RiskDescription]: {
title: translate('hotspot.tabs.risk_description'),
const tabs = [
{
key: Tabs.RiskDescription,
label: translate('hotspots.tabs.risk_description'),
content: hotspot.rule.riskDescription || ''
},
[Tabs.VulnerabilityDescription]: {
title: translate('hotspot.tabs.vulnerability_description'),
{
key: Tabs.VulnerabilityDescription,
label: translate('hotspots.tabs.vulnerability_description'),
content: hotspot.rule.vulnerabilityDescription || ''
},
[Tabs.FixRecommendation]: {
title: translate('hotspot.tabs.fix_recommendations'),
{
key: Tabs.FixRecommendation,
label: translate('hotspots.tabs.fix_recommendations'),
content: hotspot.rule.fixRecommendations || ''
},
{
key: Tabs.ReviewHistory,
label: (
<>
<span>{translate('hotspots.tabs.review_history')}</span>
<span className="counter-badge spacer-left">{hotspotReviewHistory.length}</span>
</>
),
content: hotspotReviewHistory.length > 0 && (
<HotspotViewerReviewHistoryTab history={hotspotReviewHistory} />
)
}
};

const tabsToDisplay = Object.values(Tabs)
.filter(tab => Boolean(tabs[tab].content))
.map(tab => ({ key: tab, label: tabs[tab].title }));
].filter(tab => Boolean(tab.content));

if (tabsToDisplay.length === 0) {
if (tabs.length === 0) {
return null;
}

if (!tabsToDisplay.find(tab => tab.key === currentTab)) {
setCurrentTab(tabsToDisplay[0].key);
}
const currentTab = tabs.find(tab => tab.key === currentTabKey) || tabs[0];

return (
<>
<BoxedTabs onSelect={tab => setCurrentTab(tab)} selected={currentTab} tabs={tabsToDisplay} />
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={{ __html: sanitize(tabs[currentTab].content) }}
<BoxedTabs
onSelect={tabKey => setCurrentTabKey(tabKey)}
selected={currentTabKey}
tabs={tabs}
/>
<div className="boxed-group big-padded">
{typeof currentTab.content === 'string' ? (
<div
className="markdown"
dangerouslySetInnerHTML={{ __html: sanitize(currentTab.content) }}
/>
) : (
<>{currentTab.content}</>
)}
</div>
</>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActions-test.tsx ファイルの表示

@@ -21,7 +21,7 @@ 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 { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { HotspotStatus } from '../../../../types/security-hotspots';
import HotspotActions, { HotspotActionsProps } from '../HotspotActions';

@@ -70,7 +70,7 @@ it('should register an eventlistener', () => {
function shallowRender(props: Partial<HotspotActionsProps> = {}) {
return shallow(
<HotspotActions
hotspot={mockDetailledHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })}
hotspot={mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })}
onSubmit={jest.fn()}
{...props}
/>

+ 5
- 5
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainer-test.tsx ファイルの表示

@@ -23,7 +23,7 @@ import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getSources } from '../../../../api/components';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockSourceLine } from '../../../../helpers/testMocks';
import HotspotSnippetContainer from '../HotspotSnippetContainer';
import HotspotSnippetContainerRenderer from '../HotspotSnippetContainerRenderer';
@@ -43,7 +43,7 @@ it('should load sources on mount', async () => {
range(5, 18).map(line => mockSourceLine({ line }))
);

const hotspot = mockDetailledHotspot({
const hotspot = mockHotspot({
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 }
});

@@ -65,7 +65,7 @@ it('should handle end-of-file on mount', async () => {
range(5, 15).map(line => mockSourceLine({ line }))
);

const hotspot = mockDetailledHotspot({
const hotspot = mockHotspot({
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 }
});

@@ -85,7 +85,7 @@ describe('Expansion', () => {
);
});

const hotspot = mockDetailledHotspot({
const hotspot = mockHotspot({
textRange: { startLine: 10, endLine: 11, startOffset: 0, endOffset: 12 }
});

@@ -193,6 +193,6 @@ it('should handle symbol click', () => {

function shallowRender(props?: Partial<HotspotSnippetContainer['props']>) {
return shallow<HotspotSnippetContainer>(
<HotspotSnippetContainer branchLike={branch} hotspot={mockDetailledHotspot()} {...props} />
<HotspotSnippetContainer branchLike={branch} hotspot={mockHotspot()} {...props} />
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotSnippetContainerRenderer-test.tsx ファイルの表示

@@ -20,7 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/testMocks';
import HotspotSnippetContainerRenderer, {
HotspotSnippetContainerRendererProps
@@ -36,7 +36,7 @@ function shallowRender(props?: Partial<HotspotSnippetContainerRendererProps>) {
<HotspotSnippetContainerRenderer
branchLike={mockMainBranch()}
highlightedSymbols={[]}
hotspot={mockDetailledHotspot()}
hotspot={mockHotspot()}
lastLine={undefined}
linePopup={undefined}
loading={false}

+ 13
- 3
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx ファイルの表示

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks';
import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';

@@ -27,9 +27,19 @@ it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot');
expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot(
'unassigned'
);
expect(
shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) })
shallowRender({ hotspot: mockHotspot({ assignee: mockUser({ active: false }) }) })
).toMatchSnapshot('deleted assignee');
expect(
shallowRender({
hotspot: mockHotspot({
assignee: mockUser({ name: undefined, login: 'assignee_login' })
})
})
).toMatchSnapshot('assignee without name');
expect(shallowRender()).toMatchSnapshot('anonymous user');
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in');
});
@@ -38,7 +48,7 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
currentUser={mockCurrentUser()}
hotspot={mockDetailledHotspot()}
hotspot={mockHotspot()}
loading={false}
onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}

+ 52
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerReviewHistoryTab-test.tsx ファイルの表示

@@ -0,0 +1,52 @@
/*
* 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 { mockHotspotReviewHistoryElement } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
import { ReviewHistoryType } from '../../../../types/security-hotspots';
import HotspotViewerReviewHistoryTab, {
HotspotViewerReviewHistoryTabProps
} from '../HotspotViewerReviewHistoryTab';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

function shallowRender(props?: Partial<HotspotViewerReviewHistoryTabProps>) {
return shallow(
<HotspotViewerReviewHistoryTab
history={[
mockHotspotReviewHistoryElement({ user: mockUser({ avatar: 'with-avatar' }) }),
mockHotspotReviewHistoryElement({ user: mockUser({ active: false }) }),
mockHotspotReviewHistoryElement({ user: mockUser({ login: undefined, name: undefined }) }),
mockHotspotReviewHistoryElement({
type: ReviewHistoryType.Diff,
diffs: [
{ key: 'test', oldValue: 'old', newValue: 'new' },
{ key: 'test-1', oldValue: 'old-1', newValue: 'new-1' }
]
})
]}
{...props}
/>
);
}

+ 10
- 9
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx ファイルの表示

@@ -20,10 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
import {
mockDetailledHotspot,
mockDetailledHotspotRule
} from '../../../../helpers/mocks/security-hotspots';
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots';
import HotspotViewerTabs, { HotspotViewerTabsProps, Tabs } from '../HotspotViewerTabs';

it('should render correctly', () => {
@@ -40,20 +37,24 @@ it('should render correctly', () => {

onSelect(Tabs.FixRecommendation);
expect(wrapper).toMatchSnapshot('fix');

onSelect(Tabs.ReviewHistory);
expect(wrapper).toMatchSnapshot('review');
}

expect(
shallowRender({
hotspot: mockDetailledHotspot({
rule: mockDetailledHotspotRule({ riskDescription: undefined })
hotspot: mockHotspot({
rule: mockHotspotRule({ riskDescription: undefined })
})
})
).toMatchSnapshot('empty tab');

expect(
shallowRender({
hotspot: mockDetailledHotspot({
rule: mockDetailledHotspotRule({
hotspot: mockHotspot({
creationDate: undefined,
rule: mockHotspotRule({
riskDescription: undefined,
fixRecommendations: undefined,
vulnerabilityDescription: undefined
@@ -64,5 +65,5 @@ it('should render correctly', () => {
});

function shallowRender(props?: Partial<HotspotViewerTabsProps>) {
return shallow(<HotspotViewerTabs hotspot={mockDetailledHotspot()} {...props} />);
return shallow(<HotspotViewerTabs hotspot={mockHotspot()} {...props} />);
}

+ 3
- 3
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap ファイルの表示

@@ -16,7 +16,7 @@ exports[`should handle collapse and expand 1`] = `
</strong>
<span>
<span
className="hotspot-counter"
className="counter-badge"
>
1
</span>
@@ -44,7 +44,7 @@ exports[`should handle collapse and expand 2`] = `
</strong>
<span>
<span
className="hotspot-counter"
className="counter-badge"
>
1
</span>
@@ -101,7 +101,7 @@ exports[`should render correctly with hotspots 1`] = `
</strong>
<span>
<span
className="hotspot-counter"
className="counter-badge"
>
2
</span>

+ 1
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainer-test.tsx.snap ファイルの表示

@@ -25,6 +25,7 @@ exports[`should render correctly 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",

+ 1
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap ファイルの表示

@@ -149,6 +149,7 @@ exports[`should render correctly: with sourcelines 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",

+ 453
- 12
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap ファイルの表示

@@ -22,7 +22,8 @@ exports[`should render correctly 1`] = `
className="text-muted"
>
<span>
hotspot.category
category
:
</span>
<span
className="little-spacer-left"
@@ -35,7 +36,8 @@ exports[`should render correctly 1`] = `
className="huge-spacer-bottom"
>
<span>
hotspot.status
status
:
</span>
<span
className="badge little-spacer-left"
@@ -45,7 +47,8 @@ exports[`should render correctly 1`] = `
<span
className="huge-spacer-left"
>
hotspot.assigned_to
assigned_to
:
</span>
<strong
className="little-spacer-left"
@@ -68,6 +71,7 @@ exports[`should render correctly 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -150,6 +154,7 @@ exports[`should render correctly 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -243,7 +248,8 @@ exports[`should render correctly: anonymous user 1`] = `
className="text-muted"
>
<span>
hotspot.category
category
:
</span>
<span
className="little-spacer-left"
@@ -256,7 +262,8 @@ exports[`should render correctly: anonymous user 1`] = `
className="huge-spacer-bottom"
>
<span>
hotspot.status
status
:
</span>
<span
className="badge little-spacer-left"
@@ -266,7 +273,8 @@ exports[`should render correctly: anonymous user 1`] = `
<span
className="huge-spacer-left"
>
hotspot.assigned_to
assigned_to
:
</span>
<strong
className="little-spacer-left"
@@ -289,6 +297,7 @@ exports[`should render correctly: anonymous user 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -371,6 +380,222 @@ exports[`should render correctly: anonymous user 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"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": "FIXED",
"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": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: assignee without name 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"
>
<span>
category
:
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
status
:
</span>
<span
className="badge little-spacer-left"
>
hotspot.status.FIXED
</span>
</div>
<HotspotSnippetContainer
hotspot={
Object {
"assignee": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"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": "FIXED",
"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": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
<HotspotViewerTabs
hotspot={
Object {
"assignee": Object {
"active": true,
"local": true,
"login": "assignee_login",
"name": undefined,
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -464,7 +689,8 @@ exports[`should render correctly: deleted assignee 1`] = `
className="text-muted"
>
<span>
hotspot.category
category
:
</span>
<span
className="little-spacer-left"
@@ -477,7 +703,8 @@ exports[`should render correctly: deleted assignee 1`] = `
className="huge-spacer-bottom"
>
<span>
hotspot.status
status
:
</span>
<span
className="badge little-spacer-left"
@@ -487,7 +714,8 @@ exports[`should render correctly: deleted assignee 1`] = `
<span
className="huge-spacer-left"
>
hotspot.assigned_to
assigned_to
:
</span>
<strong
className="little-spacer-left"
@@ -510,6 +738,7 @@ exports[`should render correctly: deleted assignee 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -592,6 +821,7 @@ exports[`should render correctly: deleted assignee 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -670,6 +900,211 @@ exports[`should render correctly: no hotspot 1`] = `
/>
`;

exports[`should render correctly: unassigned 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"
>
<span>
category
:
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
status
:
</span>
<span
className="badge little-spacer-left"
>
hotspot.status.FIXED
</span>
</div>
<HotspotSnippetContainer
hotspot={
Object {
"assignee": undefined,
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"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": "FIXED",
"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": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
<HotspotViewerTabs
hotspot={
Object {
"assignee": undefined,
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"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": "FIXED",
"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": "REVIEWED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: user logged in 1`] = `
<DeferredSpinner
loading={false}
@@ -702,6 +1137,7 @@ exports[`should render correctly: user logged in 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -775,7 +1211,8 @@ exports[`should render correctly: user logged in 1`] = `
className="text-muted"
>
<span>
hotspot.category
category
:
</span>
<span
className="little-spacer-left"
@@ -788,7 +1225,8 @@ exports[`should render correctly: user logged in 1`] = `
className="huge-spacer-bottom"
>
<span>
hotspot.status
status
:
</span>
<span
className="badge little-spacer-left"
@@ -798,7 +1236,8 @@ exports[`should render correctly: user logged in 1`] = `
<span
className="huge-spacer-left"
>
hotspot.assigned_to
assigned_to
:
</span>
<strong
className="little-spacer-left"
@@ -821,6 +1260,7 @@ exports[`should render correctly: user logged in 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
@@ -903,6 +1343,7 @@ exports[`should render correctly: user logged in 1`] = `
"login": "john.doe",
"name": "John Doe",
},
"changelog": Array [],
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",

+ 119
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerReviewHistoryTab-test.tsx.snap ファイルの表示

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

exports[`should render correctly 1`] = `
<Fragment>
<div>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
hash="with-avatar"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.tabs.review_history.created
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2019-09-13T17:55:42+0200"
/>
</div>
</div>
<hr />
<div>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
user.x_deleted.John Doe
</strong>
<span
className="little-spacer-left"
>
hotspots.tabs.review_history.created
</span>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2019-09-13T17:55:42+0200"
/>
</div>
</div>
<hr />
<div>
<div
className="display-flex-center"
>
<DateTimeFormatter
date="2019-09-13T17:55:42+0200"
/>
</div>
</div>
<hr />
<div>
<div
className="display-flex-center"
>
<Connect(Avatar)
className="little-spacer-right"
name="John Doe"
size={20}
/>
<strong>
John Doe
</strong>
<span
className="little-spacer-left little-spacer-right"
>
-
</span>
<DateTimeFormatter
date="2019-09-13T17:55:42+0200"
/>
</div>
<div
className="spacer-top"
>
<IssueChangelogDiff
diff={
Object {
"key": "test",
"newValue": "new",
"oldValue": "old",
}
}
key="test-old-new"
/>
<IssueChangelogDiff
diff={
Object {
"key": "test-1",
"newValue": "new-1",
"oldValue": "old-1",
}
}
key="test-1-old-1-new-1"
/>
</div>
</div>
</Fragment>
`;

+ 250
- 36
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap ファイルの表示

@@ -4,28 +4,62 @@ exports[`should render correctly: empty tab 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="vulnerability"
selected="risk"
tabs={
Array [
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
"label": "hotspots.tabs.fix_recommendations",
},
Object {
"content": <HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>,
"key": "review",
"label": <React.Fragment>
<span>
hotspots.tabs.review_history
</span>
<span
className="counter-badge spacer-left"
>
1
</span>
</React.Fragment>,
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
className="boxed-group big-padded"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
}
/>
/>
</div>
</Fragment>
`;

@@ -37,63 +71,208 @@ exports[`should render correctly: fix 1`] = `
tabs={
Array [
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"key": "risk",
"label": "hotspot.tabs.risk_description",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
"label": "hotspots.tabs.fix_recommendations",
},
Object {
"content": <HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>,
"key": "review",
"label": <React.Fragment>
<span>
hotspots.tabs.review_history
</span>
<span
className="counter-badge spacer-left"
>
1
</span>
</React.Fragment>,
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
className="boxed-group big-padded"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
}
}
}
/>
/>
</div>
</Fragment>
`;

exports[`should render correctly: no tabs 1`] = `""`;

exports[`should render correctly: risk 1`] = `
exports[`should render correctly: review 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="risk"
selected="review"
tabs={
Array [
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"key": "risk",
"label": "hotspot.tabs.risk_description",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
"label": "hotspots.tabs.fix_recommendations",
},
Object {
"content": <HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>,
"key": "review",
"label": <React.Fragment>
<span>
hotspots.tabs.review_history
</span>
<span
className="counter-badge spacer-left"
>
1
</span>
</React.Fragment>,
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
className="boxed-group big-padded"
>
<HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>
</div>
</Fragment>
`;

exports[`should render correctly: risk 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="risk"
tabs={
Array [
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"key": "risk",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"key": "vulnerability",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "fix",
"label": "hotspots.tabs.fix_recommendations",
},
Object {
"content": <HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>,
"key": "review",
"label": <React.Fragment>
<span>
hotspots.tabs.review_history
</span>
<span
className="counter-badge spacer-left"
>
1
</span>
</React.Fragment>,
},
]
}
/>
<div
className="boxed-group big-padded"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
/>
</div>
</Fragment>
`;

@@ -105,27 +284,62 @@ exports[`should render correctly: vulnerability 1`] = `
tabs={
Array [
Object {
"content": "<p>This a <strong>strong</strong> message about risk !</p>",
"key": "risk",
"label": "hotspot.tabs.risk_description",
"label": "hotspots.tabs.risk_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
"label": "hotspots.tabs.vulnerability_description",
},
Object {
"content": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
"label": "hotspots.tabs.fix_recommendations",
},
Object {
"content": <HotspotViewerReviewHistoryTab
history={
Array [
Object {
"date": "2013-05-13T17:55:41+0200",
"type": 0,
"user": Object {
"active": true,
"avatar": undefined,
"name": "John Doe",
},
},
]
}
/>,
"key": "review",
"label": <React.Fragment>
<span>
hotspots.tabs.review_history
</span>
<span
className="counter-badge spacer-left"
>
1
</span>
</React.Fragment>,
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
className="boxed-group big-padded"
>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
}
/>
/>
</div>
</Fragment>
`;

+ 41
- 2
server/sonar-web/src/main/js/apps/securityHotspots/utils.ts ファイルの表示

@@ -18,7 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { groupBy, sortBy } from 'lodash';
import { DetailedHotspot, RawHotspot, RiskExposure } from '../../types/security-hotspots';
import {
Hotspot,
RawHotspot,
ReviewHistoryElement,
ReviewHistoryType,
RiskExposure
} from '../../types/security-hotspots';

export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];

@@ -61,7 +67,7 @@ function getCategoryTitle(key: string, securityCategories: T.StandardSecurityCat
}

export function constructSourceViewerFile(
{ component, project }: DetailedHotspot,
{ component, project }: Hotspot,
lines?: number
): T.SourceViewerFile {
return {
@@ -74,3 +80,36 @@ export function constructSourceViewerFile(
uuid: ''
};
}

export function getHotspotReviewHistory(hotspot: Hotspot): ReviewHistoryElement[] {
const history: ReviewHistoryElement[] = [];

if (hotspot.creationDate) {
history.push({
type: ReviewHistoryType.Creation,
date: hotspot.creationDate,
user: {
avatar: hotspot.author.avatar,
name: hotspot.author.name || hotspot.author.login,
active: hotspot.author.active
}
});
}

if (hotspot.changelog) {
history.push(
...hotspot.changelog.map(log => ({
type: ReviewHistoryType.Diff,
date: log.creationDate,
user: {
avatar: log.avatar,
name: log.userName || log.user,
active: log.isUserActive
},
diffs: log.diffs
}))
);
}

return sortBy(history, elt => elt.date);
}

+ 19
- 7
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts ファイルの表示

@@ -19,11 +19,13 @@
*/
import { ComponentQualifier } from '../../types/component';
import {
DetailedHotspot,
DetailedHotspotRule,
Hotspot,
HotspotResolution,
HotspotRule,
HotspotStatus,
RawHotspot,
ReviewHistoryElement,
ReviewHistoryType,
RiskExposure
} from '../../types/security-hotspots';
import { mockComponent, mockUser } from '../testMocks';
@@ -47,10 +49,11 @@ export function mockRawHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot
};
}

export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): DetailedHotspot {
export function mockHotspot(overrides?: Partial<Hotspot>): Hotspot {
return {
assignee: mockUser(),
author: mockUser(),
changelog: [],
component: mockComponent({ qualifier: ComponentQualifier.File }),
creationDate: '2013-05-13T17:55:41+0200',
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
@@ -58,7 +61,7 @@ export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): Deta
message: "'3' is a magic number.",
project: mockComponent({ qualifier: ComponentQualifier.Project }),
resolution: HotspotResolution.FIXED,
rule: mockDetailledHotspotRule(),
rule: mockHotspotRule(),
status: HotspotStatus.REVIEWED,
textRange: {
startLine: 142,
@@ -71,9 +74,7 @@ export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): Deta
};
}

export function mockDetailledHotspotRule(
overrides?: Partial<DetailedHotspotRule>
): DetailedHotspotRule {
export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule {
return {
key: 'squid:S2077',
name: 'That rule',
@@ -85,3 +86,14 @@ export function mockDetailledHotspotRule(
...overrides
};
}

export function mockHotspotReviewHistoryElement(
overrides?: Partial<ReviewHistoryElement>
): ReviewHistoryElement {
return {
date: '2019-09-13T17:55:42+0200',
type: ReviewHistoryType.Creation,
user: mockUser(),
...overrides
};
}

+ 17
- 4
server/sonar-web/src/main/js/types/security-hotspots.ts ファイルの表示

@@ -68,9 +68,10 @@ export interface RawHotspot {
vulnerabilityProbability: RiskExposure;
}

export interface DetailedHotspot {
export interface Hotspot {
assignee?: Pick<T.UserBase, 'active' | 'login' | 'name'>;
author?: Pick<T.UserBase, 'login'>;
author: Pick<T.UserBase, 'active' | 'avatar' | 'login' | 'name'>;
changelog?: T.IssueChangelog[];
component: T.Component;
creationDate: string;
key: string;
@@ -78,7 +79,7 @@ export interface DetailedHotspot {
message: string;
project: T.Component;
resolution?: string;
rule: DetailedHotspotRule;
rule: HotspotRule;
status: string;
textRange: T.TextRange;
updateDate: string;
@@ -93,7 +94,7 @@ export interface HotspotUpdate extends HotspotUpdateFields {
key: string;
}

export interface DetailedHotspotRule {
export interface HotspotRule {
fixRecommendations?: string;
key: string;
name: string;
@@ -103,6 +104,18 @@ export interface DetailedHotspotRule {
vulnerabilityProbability: RiskExposure;
}

export interface ReviewHistoryElement {
type: ReviewHistoryType;
date: string;
user: Pick<T.UserBase, 'active' | 'avatar' | 'name'>;
diffs?: T.IssueChangelogDiff[];
}

export enum ReviewHistoryType {
Creation,
Diff
}

export interface HotspotSearchResponse {
components?: { key: string; qualifier: string; name: string }[];
hotspots: RawHotspot[];

+ 7
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties ファイルの表示

@@ -656,9 +656,12 @@ hotspots.risk_exposure=Review priority:
hotspot.category=Category:
hotspot.status=Status:
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.tabs.risk_description=What's the risk?
hotspots.tabs.vulnerability_description=Are you vulnerable?
hotspots.tabs.fix_recommendations=How can you fix it?
hotspots.tabs.review_history=Review history
hotspots.tabs.review_history.created=created Security Hotspot

hotspot.change_status.REVIEWED=Change status
hotspot.change_status.TO_REVIEW=Review Hotspot

@@ -672,6 +675,7 @@ hotspot.filters.assignee.all=All
hotspot.filters.status.to_review=To review
hotspot.filters.status.fixed=Reviewed as fixed
hotspot.filters.status.safe=Reviewed as safe
hotspots.review_hotspot=Review Hotspot

hotspots.form.title=Mark Security Hotspot as:


読み込み中…
キャンセル
保存