From 6fd40a8af54818efb8c29c8a01102121a529099e Mon Sep 17 00:00:00 2001 From: Philippe Perrin Date: Mon, 24 Aug 2020 15:08:25 +0200 Subject: [PATCH] SONAR-13790 Security hotspot page fails to load hotspot that doesn't have associated lines of code --- .../components/HotspotSnippetContainer.tsx | 34 +- .../HotspotSnippetContainerRenderer.tsx | 88 ++-- .../HotspotSnippetContainer-test.tsx | 30 ++ ...spotSnippetContainerRenderer-test.tsx.snap | 485 +++++++++--------- .../src/main/js/types/security-hotspots.ts | 2 +- .../resources/org/sonar/l10n/core.properties | 1 + 6 files changed, 348 insertions(+), 292 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx index 1ee4f92c777..2cfa8b7e855 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainer.tsx @@ -77,28 +77,38 @@ export default class HotspotSnippetContainer extends React.Component { - if (this.mounted) { - const lastLine = this.checkLastLine(sourceLines, to); - - // remove extra sourceline if we didn't reach the end: - sourceLines = lastLine ? sourceLines : sourceLines.slice(0, -1); - this.setState({ lastLine, loading: false, sourceLines }); - } - }) - .catch(() => this.mounted && this.setState({ loading: false })); + + let sourceLines = await getSources({ + key: component.key, + from, + to, + ...getBranchLikeQuery(branchLike) + }).catch(() => [] as T.SourceLine[]); + + if (this.mounted) { + const lastLine = this.checkLastLine(sourceLines, to); + + // remove extra sourceline if we didn't reach the end: + sourceLines = lastLine ? sourceLines : sourceLines.slice(0, -1); + this.setState({ lastLine, loading: false, sourceLines }); + } } handleExpansion = (direction: T.ExpandDirection) => { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx index ff58972f6f0..ad674541435 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; +import { translate } from 'sonar-ui-common/helpers/l10n'; import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext'; import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim'; import { BranchLike } from '../../../types/branch-like'; @@ -55,46 +56,51 @@ export default function HotspotSnippetContainerRenderer( } = props; return ( -
- - - {sourceLines.length > 0 && ( - - props.onExpandBlock(direction)} - handleCloseIssues={noop} - handleOpenIssues={noop} - handleSymbolClick={props.onSymbolClick} - highlightedLocationMessage={undefined} - highlightedSymbols={highlightedSymbols} - index={0} - issue={hotspot} - issuesByLine={{}} - lastSnippetOfLastGroup={false} - locations={[]} - locationsByLine={locations} - onIssueChange={noop} - onIssuePopupToggle={noop} - onLocationSelect={noop} - openIssuesByLine={{}} - renderDuplicationPopup={noop} - snippet={sourceLines} - /> - - )} - -
+ <> + {!loading && sourceLines.length === 0 && ( +

{translate('hotspots.no_associated_lines')}

+ )} +
+ + + {sourceLines.length > 0 && ( + + props.onExpandBlock(direction)} + handleCloseIssues={noop} + handleOpenIssues={noop} + handleSymbolClick={props.onSymbolClick} + highlightedLocationMessage={undefined} + highlightedSymbols={highlightedSymbols} + index={0} + issue={hotspot} + issuesByLine={{}} + lastSnippetOfLastGroup={false} + locations={[]} + locationsByLine={locations} + onIssueChange={noop} + onIssuePopupToggle={noop} + onLocationSelect={noop} + openIssuesByLine={{}} + renderDuplicationPopup={noop} + snippet={sourceLines} + /> + + )} + +
+ ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx index 69445d2298b..29ca15899e7 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSnippetContainer-test.tsx @@ -32,6 +32,8 @@ jest.mock('../../../../api/components', () => ({ getSources: jest.fn().mockResolvedValue([]) })); +beforeEach(() => jest.clearAllMocks()); + const branch = mockBranch(); it('should render correctly', () => { @@ -60,6 +62,34 @@ it('should load sources on mount', async () => { expect(wrapper.state().sourceLines).toHaveLength(12); }); +it('should handle load sources failure', async () => { + (getSources as jest.Mock).mockRejectedValueOnce(null); + + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(getSources).toHaveBeenCalled(); + expect(wrapper.state().loading).toBe(false); + expect(wrapper.state().lastLine).toBeUndefined(); + expect(wrapper.state().sourceLines).toHaveLength(0); +}); + +it('should not load sources on mount when the hotspot is not associated to any loc', async () => { + const hotspot = mockHotspot({ + line: undefined, + textRange: undefined + }); + + const wrapper = shallowRender({ hotspot }); + + await waitAndUpdate(wrapper); + + expect(getSources).not.toBeCalled(); + expect(wrapper.state().lastLine).toBeUndefined(); + expect(wrapper.state().sourceLines).toHaveLength(0); +}); + it('should handle end-of-file on mount', async () => { (getSources as jest.Mock).mockResolvedValueOnce( range(5, 15).map(line => mockSourceLine({ line })) diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap index f1808647b9b..5bc70f9917c 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSnippetContainerRenderer-test.tsx.snap @@ -1,269 +1,278 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
- +

+ hotspots.no_associated_lines +

+
+ - -
+ /> + +
+ `; exports[`should render correctly: with sourcelines 1`] = ` -
- - +
- + - + This a strong message about fixing !

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

This a strong message about risk !

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

This a strong message about vulnerability !

", - "vulnerabilityProbability": "HIGH", - }, - "status": "REVIEWED", - "textRange": Object { - "endLine": 142, - "endOffset": 83, - "startLine": 142, - "startOffset": 26, - }, - "updateDate": "2013-05-13T17:55:42+0200", - "users": Array [ - Object { + "path": "foo/bar.ts", + "project": "my-project", + "projectName": "MyProject", + "q": "FIL", + "uuid": "foo-bar", + } + } + displaySCM={false} + expandBlock={[Function]} + handleCloseIssues={[Function]} + handleOpenIssues={[Function]} + handleSymbolClick={[MockFunction]} + highlightedSymbols={Array []} + index={0} + issue={ + Object { + "assignee": "assignee", + "assigneeUser": Object { "active": true, "local": true, "login": "assignee", "name": "John Doe", }, - Object { + "author": "author", + "authorUser": Object { "active": true, "local": true, "login": "author", "name": "John Doe", }, - ], + "canChangeStatus": true, + "changelog": Array [], + "comment": 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": "

This a strong message about fixing !

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

This a strong message about risk !

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

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } } - } - issuesByLine={Object {}} - lastSnippetOfLastGroup={false} - locations={Array []} - locationsByLine={Object {}} - onIssueChange={[Function]} - onIssuePopupToggle={[Function]} - onLocationSelect={[Function]} - openIssuesByLine={Object {}} - renderDuplicationPopup={[Function]} - snippet={ - Array [ - Object { - "code": "import java.util.ArrayList;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 16, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - }, - ] - } - /> -
- -
+ issuesByLine={Object {}} + lastSnippetOfLastGroup={false} + locations={Array []} + locationsByLine={Object {}} + onIssueChange={[Function]} + onIssuePopupToggle={[Function]} + onLocationSelect={[Function]} + openIssuesByLine={Object {}} + renderDuplicationPopup={[Function]} + snippet={ + Array [ + Object { + "code": "import java.util.ArrayList;", + "coverageStatus": "covered", + "coveredConditions": 2, + "duplicated": false, + "isNew": true, + "line": 16, + "scmAuthor": "simon.brandhof@sonarsource.com", + "scmDate": "2018-12-11T10:48:39+0100", + "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", + }, + ] + } + /> + +
+
+ `; diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index 79e354a19ea..36e9fab714b 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -86,7 +86,7 @@ export interface Hotspot { resolution?: HotspotResolution; rule: HotspotRule; status: HotspotStatus; - textRange: T.TextRange; + textRange?: T.TextRange; updateDate: string; users: T.UserBase[]; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 2a9fe575fa3..70115c53fc0 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -696,6 +696,7 @@ hotspots.status_option.FIXED.description=The code has been modified to follow re hotspots.status_option.SAFE=Safe hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. hotspots.get_permalink=Get Permalink +hotspots.no_associated_lines=Security Hotspot raised on the following file: hotspot.filters.title=Filters hotspot.filters.assignee.assigned_to_me=Assigned to me -- 2.39.5