From 9e025bf15700eff81e11cb00bcf5e9650f765ee9 Mon Sep 17 00:00:00 2001
From: Philippe Perrin
Date: Tue, 11 Feb 2020 18:49:50 +0100
Subject: [PATCH] SONAR-12719 Move the status edition popup in hotspot main
screen
---
.../src/main/js/app/styles/init/icons.css | 2 +-
.../src/main/js/app/styles/init/misc.css | 5 +
.../security-hotspots/__tests__/utils-test.ts | 51 +-
.../components/HotspotActions.tsx | 77 --
.../components/HotspotActionsForm.tsx | 163 ----
.../components/HotspotActionsFormRenderer.tsx | 166 ----
.../components/HotspotListItem.tsx | 6 +-
.../components/HotspotViewerRenderer.tsx | 21 +-
.../__tests__/HotspotActions-test.tsx | 78 --
.../__tests__/HotspotActionsForm-test.tsx | 155 ---
.../HotspotActionsFormRenderer-test.tsx | 109 ---
.../__tests__/HotspotViewerRenderer-test.tsx | 6 +-
.../HotspotActions-test.tsx.snap | 406 --------
.../HotspotActionsForm-test.tsx.snap | 112 ---
.../HotspotActionsFormRenderer-test.tsx.snap | 544 -----------
.../HotspotListItem-test.tsx.snap | 4 +-
.../__snapshots__/HotspotViewer-test.tsx.snap | 4 +-
.../HotspotViewerRenderer-test.tsx.snap | 891 +++++++++---------
.../assignee/AssigneeSelectionRenderer.tsx | 2 +-
.../AssigneeSelectionRenderer-test.tsx.snap | 8 +-
.../components/status/Status.css | 44 +
.../components/status/Status.tsx | 107 +++
.../components/status/StatusDescription.tsx | 41 +
.../components/status/StatusSelection.tsx | 109 +++
.../status/StatusSelectionRenderer.tsx | 91 ++
.../status/__tests__/Status-test.tsx | 74 ++
.../__tests__/StatusDescription-test.tsx | 35 +
.../status/__tests__/StatusSelection-test.tsx | 78 ++
.../StatusSelectionRenderer-test.tsx | 72 ++
.../__snapshots__/Status-test.tsx.snap | 436 +++++++++
.../StatusDescription-test.tsx.snap | 24 +
.../StatusSelection-test.tsx.snap | 12 +
.../StatusSelectionRenderer-test.tsx.snap | 140 +++
.../main/js/apps/security-hotspots/styles.css | 7 -
.../main/js/apps/security-hotspots/utils.ts | 35 +
.../src/main/js/types/security-hotspots.ts | 6 +-
.../resources/org/sonar/l10n/core.properties | 38 +-
37 files changed, 1838 insertions(+), 2321 deletions(-)
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css
index 753fb321eb3..7a04513fabd 100644
--- a/server/sonar-web/src/main/js/app/styles/init/icons.css
+++ b/server/sonar-web/src/main/js/app/styles/init/icons.css
@@ -77,7 +77,7 @@ a[class*=' icon-'] {
transition: opacity 0.3s ease;
}
-a:hover > .icon-radio {
+a:not(.disabled):hover > .icon-radio {
border-color: var(--blue);
}
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 9dd45c38a90..9a8ffd44fd9 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -384,6 +384,11 @@ th.huge-spacer-right {
justify-content: center;
}
+.display-flex-justify-end {
+ display: flex !important;
+ justify-content: flex-end;
+}
+
.display-flex-space-around {
display: flex !important;
justify-content: space-around;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
index a837324eba8..0f134e3b118 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
@@ -19,8 +19,21 @@
*/
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../helpers/testMocks';
-import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots';
-import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils';
+import {
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusOption,
+ ReviewHistoryType,
+ RiskExposure
+} from '../../../types/security-hotspots';
+import {
+ getHotspotReviewHistory,
+ getStatusAndResolutionFromStatusOption,
+ getStatusOptionFromStatusAndResolution,
+ groupByCategory,
+ mapRules,
+ sortHotspots
+} from '../utils';
const hotspots = [
mockRawHotspot({
@@ -223,3 +236,37 @@ describe('getHotspotReviewHistory', () => {
);
});
});
+
+describe('getStatusOptionFromStatusAndResolution', () => {
+ it('should return the correct values', () => {
+ expect(
+ getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.FIXED)
+ ).toBe(HotspotStatusOption.FIXED);
+ expect(
+ getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.SAFE)
+ ).toBe(HotspotStatusOption.SAFE);
+ expect(getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED)).toBe(
+ HotspotStatusOption.FIXED
+ );
+ expect(getStatusOptionFromStatusAndResolution(HotspotStatus.TO_REVIEW)).toBe(
+ HotspotStatusOption.TO_REVIEW
+ );
+ });
+});
+
+describe('getStatusAndResolutionFromStatusOption', () => {
+ it('should return the correct values', () => {
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual({
+ status: HotspotStatus.TO_REVIEW,
+ resolution: undefined
+ });
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.FIXED)).toEqual({
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.FIXED
+ });
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.SAFE)).toEqual({
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ });
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
deleted file mode 100644
index 2018a18feb3..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { Button } from 'sonar-ui-common/components/controls/buttons';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
-import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler';
-import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { Hotspot } from '../../../types/security-hotspots';
-import HotspotActionsForm from './HotspotActionsForm';
-
-export interface HotspotActionsProps {
- hotspot: Hotspot;
- onSubmit: () => void;
-}
-
-const ESCAPE_KEY = 'Escape';
-
-export default function HotspotActions(props: HotspotActionsProps) {
- const { hotspot } = props;
- const [open, setOpen] = React.useState(false);
-
- React.useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === ESCAPE_KEY) {
- setOpen(false);
- }
- };
-
- document.addEventListener('keydown', handleKeyDown, false);
-
- return () => {
- document.removeEventListener('keydown', handleKeyDown, false);
- };
- });
-
- return (
-
- setOpen(!open)}>
- {translate('hotspot.change_status', hotspot.status)}
-
-
-
- {open && (
- setOpen(false)}>
-
- {
- setOpen(false);
- props.onSubmit();
- }}
- />
-
-
- )}
-
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
deleted file mode 100644
index c5564548006..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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 {
- assignSecurityHotspot,
- commentSecurityHotspot,
- setSecurityHotspotStatus
-} from '../../../api/security-hotspots';
-import {
- Hotspot,
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../types/security-hotspots';
-import HotspotActionsFormRenderer from './HotspotActionsFormRenderer';
-
-interface Props {
- hotspot: Hotspot;
- onSubmit: () => void;
-}
-
-interface State {
- comment: string;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
- submitting: boolean;
-}
-
-export default class HotspotActionsForm extends React.Component {
- constructor(props: Props) {
- super(props);
-
- let selectedOption = HotspotStatusOption.FIXED;
- if (props.hotspot.status === HotspotStatus.TO_REVIEW) {
- selectedOption = HotspotStatusOption.ADDITIONAL_REVIEW;
- } else if (props.hotspot.resolution) {
- selectedOption = HotspotStatusOption[props.hotspot.resolution];
- }
-
- this.state = {
- comment: '',
- selectedOption,
- submitting: false
- };
- }
-
- handleSelectOption = (selectedOption: HotspotStatusOption) => {
- this.setState({ selectedOption });
- };
-
- handleAssign = (selectedUser: T.UserActive) => {
- this.setState({ selectedUser });
- };
-
- handleCommentChange = (comment: string) => {
- this.setState({ comment });
- };
-
- handleSubmit = (event: React.SyntheticEvent) => {
- event.preventDefault();
-
- const { hotspot } = this.props;
- const { comment, selectedOption, selectedUser } = this.state;
-
- const status =
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotStatus.TO_REVIEW
- : HotspotStatus.REVIEWED;
-
- const resolution =
- selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotResolution[selectedOption]
- : undefined;
-
- this.setState({ submitting: true });
- /*
- * updateAssignee depends on updateStatus, hence these are chained rather than
- * run in parallel. The comment should also appear last in the changelog.
- */
- return Promise.resolve()
- .then(() => this.updateStatus(hotspot, status, resolution))
- .then(() => this.updateAssignee(hotspot, selectedOption, selectedUser))
- .then(() => this.addComment(hotspot, comment))
- .then(() => {
- this.props.onSubmit();
- // No need to set "submitting", we are closing the window
- })
- .catch(() => {
- this.setState({ submitting: false });
- });
- };
-
- updateStatus = (hotspot: Hotspot, status: HotspotStatus, resolution?: HotspotResolution) => {
- if (
- hotspot.canChangeStatus &&
- (status !== hotspot.status || resolution !== hotspot.resolution)
- ) {
- return setSecurityHotspotStatus(hotspot.key, { status, resolution });
- }
-
- return Promise.resolve();
- };
-
- updateAssignee = (
- hotspot: Hotspot,
- selectedOption: HotspotStatusOption,
- selectedUser?: T.UserActive
- ) => {
- if (
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW &&
- selectedUser &&
- selectedUser.login !== hotspot.assignee
- ) {
- return assignSecurityHotspot(hotspot.key, {
- assignee: selectedUser.login
- });
- }
- return Promise.resolve();
- };
-
- addComment = (hotspot: Hotspot, comment: string) => {
- if (comment.length > 0) {
- return commentSecurityHotspot(hotspot.key, comment);
- }
- return Promise.resolve();
- };
-
- render() {
- const { hotspot } = this.props;
- const { comment, selectedOption, selectedUser, submitting } = this.state;
-
- return (
-
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
deleted file mode 100644
index d8d0ff6ceb0..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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 classnames from 'classnames';
-import * as React from 'react';
-import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import Radio from 'sonar-ui-common/components/controls/Radio';
-import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import MarkdownTips from '../../../components/common/MarkdownTips';
-import {
- Hotspot,
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../types/security-hotspots';
-
-export interface HotspotActionsFormRendererProps {
- comment: string;
- hotspot: Hotspot;
- onAssign: (user: T.UserActive) => void;
- onChangeComment: (comment: string) => void;
- onSelectOption: (option: HotspotStatusOption) => void;
- onSubmit: (event: React.SyntheticEvent) => void;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
- submitting: boolean;
-}
-
-export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) {
- const { comment, hotspot, selectedOption, submitting } = props;
-
- const disableStatusChange = !hotspot.canChangeStatus;
-
- return (
-
- );
-}
-
-const noop = () => {};
-
-function changes(params: {
- comment: string;
- hotspot: Hotspot;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
-}) {
- const { comment, hotspot, selectedOption, selectedUser } = params;
-
- const status =
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotStatus.TO_REVIEW
- : HotspotStatus.REVIEWED;
-
- const resolution =
- selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotResolution[selectedOption]
- : undefined;
-
- return (
- comment.length > 0 ||
- selectedUser ||
- status !== hotspot.status ||
- resolution !== hotspot.resolution
- );
-}
-
-function renderOption(params: {
- disabled: boolean;
- option: HotspotStatusOption;
- onClick: (option: HotspotStatusOption) => void;
- selectedOption: HotspotStatusOption;
-}) {
- const { disabled, onClick, option, selectedOption } = params;
-
- const optionRender = (
-
-
-
- {translate('hotspots.status_option', option)}
-
-
-
- {translate('hotspots.status_option', option, 'description')}
-
-
- );
-
- return disabled ? (
-
- {optionRender}
-
- ) : (
- optionRender
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index 95a9826cc7d..59540a73c4e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -21,6 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { RawHotspot } from '../../../types/security-hotspots';
+import { getStatusOptionFromStatusAndResolution } from '../utils';
export interface HotspotListItemProps {
hotspot: RawHotspot;
@@ -37,7 +38,10 @@ export default function HotspotListItem(props: HotspotListItemProps) {
onClick={() => !selected && props.onClick(hotspot.key)}>
{hotspot.message}
- {translate('hotspot.status', hotspot.resolution || hotspot.status)}
+ {translate(
+ 'hotspots.status_option',
+ getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution)
+ )}
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
index 179f461cda7..3a841add34a 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
@@ -20,26 +20,23 @@
import * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
-import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee';
-import HotspotActions from './HotspotActions';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import HotspotViewerTabs from './HotspotViewerTabs';
+import Status from './status/Status';
export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
- currentUser: T.CurrentUser;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: () => void;
securityCategories: T.StandardSecurityCategories;
}
-export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
- const { branchLike, currentUser, hotspot, loading, securityCategories } = props;
+export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
+ const { branchLike, hotspot, loading, securityCategories } = props;
return (
@@ -48,9 +45,6 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
{hotspot.message}
- {isLoggedIn(currentUser) && (
-
- )}
{translate('category')}:
@@ -59,12 +53,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
-
-
{translate('status')}:
-
- {translate('hotspot.status', hotspot.resolution || hotspot.status)}
-
+
@@ -73,5 +64,3 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
);
}
-
-export default withCurrentUser(HotspotViewerRenderer);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
deleted file mode 100644
index 1f67a35002f..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { Button } from 'sonar-ui-common/components/controls/buttons';
-import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { HotspotStatus } from '../../../../types/security-hotspots';
-import HotspotActions, { HotspotActionsProps } from '../HotspotActions';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should open when clicked', async () => {
- const wrapper = shallowRender();
-
- wrapper.find(Button).simulate('click');
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should register an eventlistener', () => {
- let useEffectCleanup: void | (() => void | undefined) = () =>
- fail('useEffect should clean after itself');
- jest.spyOn(React, 'useEffect').mockImplementationOnce(f => {
- useEffectCleanup = f() || useEffectCleanup;
- });
- let listenerCallback = (_event: { key: string }) =>
- fail('Effect should have registered callback');
- const addEventListener = jest.fn((_event, callback) => {
- listenerCallback = callback;
- });
- jest.spyOn(document, 'addEventListener').mockImplementation(addEventListener);
- const removeEventListener = jest.spyOn(document, 'removeEventListener');
- const wrapper = shallowRender();
-
- wrapper.find(Button).simulate('click');
- expect(wrapper).toMatchSnapshot('Dropdown open');
-
- listenerCallback({ key: 'whatever' });
- expect(wrapper).toMatchSnapshot('Dropdown still open');
-
- listenerCallback({ key: 'Escape' });
- expect(wrapper).toMatchSnapshot('Dropdown closed');
-
- useEffectCleanup();
- expect(removeEventListener).toBeCalledWith('keydown', listenerCallback, false);
-});
-
-function shallowRender(props: Partial
= {}) {
- return shallow(
-
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
deleted file mode 100644
index 11fb8aa26ef..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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 { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import {
- assignSecurityHotspot,
- commentSecurityHotspot,
- setSecurityHotspotStatus
-} from '../../../../api/security-hotspots';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import {
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../../types/security-hotspots';
-import HotspotActionsForm from '../HotspotActionsForm';
-
-jest.mock('../../../../api/security-hotspots', () => ({
- assignSecurityHotspot: jest.fn().mockResolvedValue(undefined),
- commentSecurityHotspot: jest.fn().mockResolvedValue(undefined),
- setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined)
-}));
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should handle option selection', () => {
- const wrapper = shallowRender();
- expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.FIXED);
- wrapper.instance().handleSelectOption(HotspotStatusOption.SAFE);
- expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.SAFE);
-});
-
-it('should handle comment change', () => {
- const wrapper = shallowRender();
- wrapper.instance().handleCommentChange('new comment');
- expect(wrapper.state().comment).toBe('new comment');
-});
-
-describe('submit', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should be handled for additional review', async () => {
- const onSubmit = jest.fn();
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });
-
- const promise = wrapper.instance().handleSubmit(mockEvent());
-
- expect(wrapper.state().submitting).toBe(true);
- await promise;
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.TO_REVIEW
- });
- expect(onSubmit).toBeCalled();
- });
-
- it('should be handled for SAFE', async () => {
- const wrapper = shallowRender();
- wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'commentsafe');
- });
-
- it('should be handled for FIXED', async () => {
- const wrapper = shallowRender({
- hotspot: mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })
- });
- wrapper.setState({ comment: 'commentfixed', selectedOption: HotspotStatusOption.FIXED });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.FIXED
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'commentfixed');
- });
-
- it('should ignore no change', async () => {
- const wrapper = shallowRender();
- wrapper.setState({ selectedOption: HotspotStatusOption.FIXED });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).not.toBeCalled();
- });
-});
-
-it('should handle assignment', async () => {
- const onSubmit = jest.fn();
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({
- comment: 'assignment comment',
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW
- });
-
- wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' }));
- await waitAndUpdate(wrapper);
-
- const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
-
- expect(wrapper.state().submitting).toBe(true);
- await promise;
-
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.TO_REVIEW
- });
- expect(assignSecurityHotspot).toBeCalledWith('key', {
- assignee: 'userLogin'
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'assignment comment');
- expect(onSubmit).toBeCalled();
-});
-
-it('should handle submit failure', async () => {
- const onSubmit = jest.fn();
- (setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure');
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });
- const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
- expect(wrapper.state().submitting).toBe(true);
- await promise;
- await waitAndUpdate(wrapper);
- expect(wrapper.state().submitting).toBe(false);
- expect(onSubmit).not.toBeCalled();
-});
-
-function shallowRender(props: Partial = {}) {
- return shallow(
-
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
deleted file mode 100644
index 77dc7596bd0..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import {
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../../types/security-hotspots';
-import HotspotActionsForm from '../HotspotActionsForm';
-import HotspotActionsFormRenderer, {
- HotspotActionsFormRendererProps
-} from '../HotspotActionsFormRenderer';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
- expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting');
- expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot(
- 'safe option selected'
- );
- expect(
- shallowRender({
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
- selectedUser: mockLoggedInUser()
- })
- ).toMatchSnapshot('user selected');
- expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
- 'restricted access'
- );
-});
-
-it('should enable the submit button if anything has changed', () => {
- const hotspot = mockHotspot({
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- const selectedOption = HotspotStatusOption.SAFE;
- expect(
- shallowRender({ comment: '', hotspot, selectedOption, selectedUser: undefined })
- .find(SubmitButton)
- .props().disabled
- ).toBe(true);
- expect(
- shallowRender({ comment: 'some comment', hotspot, selectedOption, selectedUser: undefined })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({ comment: '', hotspot, selectedOption, selectedUser: mockLoggedInUser() })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({
- comment: '',
- hotspot,
- selectedOption: HotspotStatusOption.FIXED,
- selectedUser: undefined
- })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({
- comment: '',
- hotspot,
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
- selectedUser: undefined
- })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
-});
-
-function shallowRender(props: Partial = {}) {
- return shallow(
-
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
index 68820dd230e..de1a77e5c77 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
@@ -20,8 +20,8 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks';
-import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
+import { mockUser } from '../../../../helpers/testMocks';
+import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';
it('should render correctly', () => {
const wrapper = shallowRender();
@@ -41,13 +41,11 @@ it('should render correctly', () => {
})
).toMatchSnapshot('assignee without name');
expect(shallowRender()).toMatchSnapshot('anonymous user');
- expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in');
});
function shallowRender(props?: Partial) {
return shallow(
-
- hotspot.change_status.TO_REVIEW
-
-
-
-
- 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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
-
-
-
-`;
-
-exports[`should register an eventlistener: Dropdown closed 1`] = `
-
-
- hotspot.change_status.TO_REVIEW
-
-
-
-`;
-
-exports[`should register an eventlistener: Dropdown open 1`] = `
-
-
- hotspot.change_status.TO_REVIEW
-
-
-
-
- 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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
-
-
-
-`;
-
-exports[`should register an eventlistener: Dropdown still open 1`] = `
-
-
- hotspot.change_status.TO_REVIEW
-
-
-
-
- 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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
-
-
-
-`;
-
-exports[`should render correctly 1`] = `
-
-
- hotspot.change_status.TO_REVIEW
-
-
-
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
deleted file mode 100644
index 9ad46c82552..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
+++ /dev/null
@@ -1,112 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 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 {
- "active": true,
- "local": true,
- "login": "assignee",
- "name": "John Doe",
- },
- Object {
- "active": true,
- "local": true,
- "login": "author",
- "name": "John Doe",
- },
- ],
- }
- }
- onAssign={[Function]}
- onChangeComment={[Function]}
- onSelectOption={[Function]}
- onSubmit={[Function]}
- selectedOption="FIXED"
- submitting={false}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
deleted file mode 100644
index 1f5255ea037..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
+++ /dev/null
@@ -1,544 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-
-
- hotspots.form.title
-
-
-
-
-
- hotspots.status_option.FIXED
-
-
-
- hotspots.status_option.FIXED.description
-
-
-
-
-
- hotspots.status_option.SAFE
-
-
-
- hotspots.status_option.SAFE.description
-
-
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW.description
-
-
-
-
-
- hotspots.form.comment
-
-
-
-
-
-
- hotspots.form.submit.REVIEWED
-
-
-
-`;
-
-exports[`should render correctly: Submitting 1`] = `
-
-
- hotspots.form.title
-
-
-
-
-
- hotspots.status_option.FIXED
-
-
-
- hotspots.status_option.FIXED.description
-
-
-
-
-
- hotspots.status_option.SAFE
-
-
-
- hotspots.status_option.SAFE.description
-
-
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW.description
-
-
-
-
-
- hotspots.form.comment
-
-
-
-
-
-
-
- hotspots.form.submit.REVIEWED
-
-
-
-`;
-
-exports[`should render correctly: restricted access 1`] = `
-
-
- hotspots.form.title.disabled
-
-
-
-
-
-
- hotspots.status_option.FIXED
-
-
-
- hotspots.status_option.FIXED.description
-
-
-
-
-
-
-
- hotspots.status_option.SAFE
-
-
-
- hotspots.status_option.SAFE.description
-
-
-
-
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW.description
-
-
-
-
-
-
- hotspots.form.comment
-
-
-
-
-
-
- hotspots.form.submit.REVIEWED
-
-
-
-`;
-
-exports[`should render correctly: safe option selected 1`] = `
-
-
- hotspots.form.title
-
-
-
-
-
- hotspots.status_option.FIXED
-
-
-
- hotspots.status_option.FIXED.description
-
-
-
-
-
- hotspots.status_option.SAFE
-
-
-
- hotspots.status_option.SAFE.description
-
-
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW.description
-
-
-
-
-
- hotspots.form.comment
-
-
-
-
-
-
- hotspots.form.submit.REVIEWED
-
-
-
-`;
-
-exports[`should render correctly: user selected 1`] = `
-
-
- hotspots.form.title
-
-
-
-
-
- hotspots.status_option.FIXED
-
-
-
- hotspots.status_option.FIXED.description
-
-
-
-
-
- hotspots.status_option.SAFE
-
-
-
- hotspots.status_option.SAFE.description
-
-
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW
-
-
-
- hotspots.status_option.ADDITIONAL_REVIEW.description
-
-
-
-
-
- hotspots.form.comment
-
-
-
-
-
-
- hotspots.form.submit.REVIEWED
-
-
-
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index 2bf9b48a976..b959dae2d97 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = `
- hotspot.status.TO_REVIEW
+ hotspots.status_option.TO_REVIEW
`;
@@ -33,7 +33,7 @@ exports[`should render correctly 2`] = `
- hotspot.status.TO_REVIEW
+ hotspots.status_option.TO_REVIEW
`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
index c9bd0adb0ab..c069dd4ca30 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-
-
- status
- :
-
-
- hotspot.status.FIXED
-
+
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",
+ },
+ ],
+ }
+ }
+ onStatusChange={[MockFunction]}
+ />
-
- status
- :
-
-
- hotspot.status.FIXED
-
+
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",
+ },
+ ],
+ }
+ }
+ onStatusChange={[MockFunction]}
+ />
-
- status
- :
-
-
- hotspot.status.FIXED
-
-
- 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",
- },
- ],
- }
- }
- />
- 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",
- },
- ],
- }
- }
- onUpdateHotspot={[MockFunction]}
- />
-
-
-`;
-
-exports[`should render correctly: deleted assignee 1`] = `
-
-
-
-
-
- '3' is a magic number.
-
-
-
-
- category
- :
-
-
- SQL injection
-
-
-
-
-
- status
- :
-
-
- hotspot.status.FIXED
-
-
`;
-exports[`should render correctly: no hotspot 1`] = `
-
-`;
-
-exports[`should render correctly: unassigned 1`] = `
+exports[`should render correctly: deleted assignee 1`] = `
-
- status
- :
-
-
- hotspot.status.FIXED
-
+
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",
+ },
+ ],
+ }
+ }
+ onStatusChange={[MockFunction]}
+ />
`;
-exports[`should render correctly: user logged in 1`] = `
+exports[`should render correctly: no hotspot 1`] = `
+
+`;
+
+exports[`should render correctly: unassigned 1`] = `
'3' is a magic number.
- 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",
- },
- ],
- }
- }
- onSubmit={[MockFunction]}
- />
-
- status
- :
-
-
- hotspot.status.FIXED
-
+
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",
+ },
+ ],
+ }
+ }
+ onStatusChange={[MockFunction]}
+ />
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
index 93774eb9f81..004aedfae6c 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
@@ -9,7 +9,7 @@ exports[`should render correctly 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
@@ -24,7 +24,7 @@ exports[`should render correctly: loading 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
void;
+}
+
+export function Status(props: StatusProps) {
+ const { currentUser, hotspot } = props;
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ const statusOption = getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution);
+ const readonly = !hotspot.canChangeStatus || !isLoggedIn(currentUser);
+
+ const trigger = (
+
!readonly && setIsOpen(true)}
+ role="button"
+ tabIndex={0}>
+
+ {isOpen ? (
+ {translate('hotspots.status.select_status')}
+ ) : (
+
+ )}
+ {!readonly && }
+
+
+ );
+
+ const actionableTrigger = (
+
setIsOpen(false)}
+ open={isOpen}
+ overlay={
+
+ {
+ setIsOpen(false);
+ props.onStatusChange();
+ }}
+ />
+
+ }>
+ {trigger}
+
+ );
+
+ return (
+
+ {readonly ? (
+
+ {actionableTrigger}
+
+ ) : (
+ actionableTrigger
+ )}
+
+ );
+}
+
+export default withCurrentUser(Status);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
new file mode 100644
index 00000000000..ae853a786c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 } from 'sonar-ui-common/helpers/l10n';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+
+export interface StatusDescriptionProps {
+ statusOption: HotspotStatusOption;
+ showTitle?: boolean;
+}
+
+export default function StatusDescription(props: StatusDescriptionProps) {
+ const { statusOption, showTitle } = props;
+
+ return (
+
+
+ {showTitle && `${translate('status')}: `}
+ {translate('hotspots.status_option', statusOption)}
+
+ {translate('hotspots.status_option', statusOption, 'description')}
+
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
new file mode 100644
index 00000000000..943fc94f904
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
+import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
+import {
+ getStatusAndResolutionFromStatusOption,
+ getStatusOptionFromStatusAndResolution
+} from '../../utils';
+import StatusSelectionRenderer from './StatusSelectionRenderer';
+
+interface Props {
+ hotspot: Hotspot;
+ onStatusOptionChange: (statusOption: HotspotStatusOption) => void;
+}
+
+interface State {
+ comment?: string;
+ loading: boolean;
+ initialStatus: HotspotStatusOption;
+ selectedStatus: HotspotStatusOption;
+}
+
+export default class StatusSelection extends React.PureComponent
{
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ const initialStatus = getStatusOptionFromStatusAndResolution(
+ props.hotspot.status,
+ props.hotspot.resolution
+ );
+
+ this.state = {
+ loading: false,
+ initialStatus,
+ selectedStatus: initialStatus
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleStatusChange = (selectedStatus: HotspotStatusOption) => {
+ this.setState({ selectedStatus });
+ };
+
+ handleCommentChange = (comment: string) => {
+ this.setState({ comment });
+ };
+
+ handleSubmit = () => {
+ const { hotspot } = this.props;
+ const { comment, initialStatus, selectedStatus } = this.state;
+
+ if (selectedStatus && selectedStatus !== initialStatus) {
+ this.setState({ loading: true });
+ setSecurityHotspotStatus(hotspot.key, {
+ ...getStatusAndResolutionFromStatusOption(selectedStatus),
+ comment: comment || undefined
+ })
+ .then(() => {
+ this.setState({ loading: false });
+ this.props.onStatusOptionChange(selectedStatus);
+ })
+ .catch(() => this.setState({ loading: false }));
+ }
+ };
+
+ render() {
+ const { comment, initialStatus, loading, selectedStatus } = this.state;
+ const submitDisabled = selectedStatus === initialStatus;
+
+ return (
+
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
new file mode 100644
index 00000000000..16487044701
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import MarkdownTips from '../../../../components/common/MarkdownTips';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+import StatusDescription from './StatusDescription';
+
+export interface StatusSelectionRendererProps {
+ selectedStatus: HotspotStatusOption;
+ onStatusChange: (statusOption: HotspotStatusOption) => void;
+
+ comment?: string;
+ onCommentChange: (comment: string) => void;
+
+ onSubmit: () => void;
+
+ loading: boolean;
+ submitDisabled: boolean;
+}
+
+export default function StatusSelectionRenderer(props: StatusSelectionRendererProps) {
+ const { comment, loading, selectedStatus, submitDisabled } = props;
+
+ const renderOption = (status: HotspotStatusOption) => {
+ return (
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+ {renderOption(HotspotStatusOption.TO_REVIEW)}
+ {renderOption(HotspotStatusOption.FIXED)}
+ {renderOption(HotspotStatusOption.SAFE)}
+
+
+
+
+
+ {translate('hotspots.status.add_comment')}
+
+
+ >
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
new file mode 100644
index 00000000000..309dba42348
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { mockCurrentUser } from '../../../../../helpers/testMocks';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import { Status, StatusProps } from '../Status';
+import StatusSelection from '../StatusSelection';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot('closed');
+
+ click(wrapper.find('#status-trigger'));
+ expect(wrapper).toMatchSnapshot('open');
+
+ wrapper
+ .find(Toggler)
+ .props()
+ .onRequestClose();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+
+ expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
+ 'readonly'
+ );
+});
+
+it('should properly deal with status changes', () => {
+ const onStatusChange = jest.fn();
+ const wrapper = shallowRender({ onStatusChange });
+
+ click(wrapper.find('#status-trigger'));
+ wrapper
+ .find(Toggler)
+ .dive()
+ .find(StatusSelection)
+ .props()
+ .onStatusOptionChange(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalled();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+});
+
+function shallowRender(props?: Partial) {
+ return shallow(
+
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
new file mode 100644
index 00000000000..048f5c6aec5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusDescription, { StatusDescriptionProps } from '../StatusDescription';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ showTitle: true })).toMatchSnapshot('with title');
+});
+
+function shallowRender(props?: Partial) {
+ return shallow(
+
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
new file mode 100644
index 00000000000..9b1d44c0fa8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { setSecurityHotspotStatus } from '../../../../../api/security-hotspots';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { HotspotStatus, HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelection from '../StatusSelection';
+import StatusSelectionRenderer from '../StatusSelectionRenderer';
+
+jest.mock('../../../../../api/security-hotspots', () => ({
+ setSecurityHotspotStatus: jest.fn()
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should properly deal with comment/status/submit events', async () => {
+ const hotspot = mockHotspot();
+ const onStatusOptionChange = jest.fn();
+ const wrapper = shallowRender({ hotspot, onStatusOptionChange });
+
+ const newStatusOption = HotspotStatusOption.SAFE;
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onStatusChange(newStatusOption);
+ expect(wrapper.state().selectedStatus).toBe(newStatusOption);
+ expect(wrapper.find(StatusSelectionRenderer).props().submitDisabled).toBe(false);
+
+ const newComment = 'TEST-COMMENT';
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onCommentChange(newComment);
+ expect(wrapper.state().comment).toBe(newComment);
+
+ (setSecurityHotspotStatus as jest.Mock).mockResolvedValueOnce({});
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onSubmit();
+ expect(setSecurityHotspotStatus).toHaveBeenCalledWith(hotspot.key, {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotStatusOption.SAFE,
+ comment: newComment
+ });
+
+ await waitAndUpdate(wrapper);
+
+ expect(onStatusOptionChange).toHaveBeenCalledWith(newStatusOption);
+});
+
+function shallowRender(props?: Partial) {
+ return shallow(
+
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
new file mode 100644
index 00000000000..f63e47f45c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { change, click } from 'sonar-ui-common/helpers/testUtils';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelectionRenderer, { StatusSelectionRendererProps } from '../StatusSelectionRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(
+ shallowRender({ submitDisabled: true })
+ .find(SubmitButton)
+ .props().disabled
+ ).toBe(true);
+});
+
+it('should call proper callbacks on actions', () => {
+ const onCommentChange = jest.fn();
+ const onStatusChange = jest.fn();
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onCommentChange, onStatusChange, onSubmit });
+
+ change(wrapper.find('textarea'), 'TATA');
+ expect(onCommentChange).toHaveBeenCalledWith('TATA');
+
+ wrapper
+ .find(Radio)
+ .first()
+ .props()
+ .onCheck(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalledWith(HotspotStatusOption.SAFE);
+
+ click(wrapper.find(SubmitButton));
+ expect(onSubmit).toHaveBeenCalled();
+});
+
+function shallowRender(props?: Partial) {
+ return shallow(
+
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
new file mode 100644
index 00000000000..36c5ed7da90
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
@@ -0,0 +1,436 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: closed 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 {
+ "active": true,
+ "local": true,
+ "login": "assignee",
+ "name": "John Doe",
+ },
+ Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+
+ }
+ >
+
+
+
+`;
+
+exports[`should render correctly: open 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 {
+ "active": true,
+ "local": true,
+ "login": "assignee",
+ "name": "John Doe",
+ },
+ Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+
+ }
+ >
+
+
+
+ hotspots.status.select_status
+
+
+
+
+
+
+`;
+
+exports[`should render correctly: readonly 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 {
+ "active": true,
+ "local": true,
+ "login": "assignee",
+ "name": "John Doe",
+ },
+ Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+
+ }
+ >
+
+
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
new file mode 100644
index 00000000000..b77051910de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+
+
+ hotspots.status_option.TO_REVIEW
+
+
+ hotspots.status_option.TO_REVIEW.description
+
+
+`;
+
+exports[`should render correctly: with title 1`] = `
+
+
+ status:
+ hotspots.status_option.TO_REVIEW
+
+
+ hotspots.status_option.TO_REVIEW.description
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
new file mode 100644
index 00000000000..61d22f512e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
new file mode 100644
index 00000000000..482dd86bb54
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hotspots.status.add_comment
+
+
+
+
+
+ hotspots.status.change_status
+
+
+
+
+`;
+
+exports[`should render correctly: loading 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hotspots.status.add_comment
+
+
+
+
+
+ hotspots.status.change_status
+
+
+
+
+
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
index 9a327c158d7..f95c95b6331 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
@@ -51,10 +51,3 @@
overflow-y: auto;
background-color: white;
}
-
-/*
-* Align description with label by offsetting by width of radio + margin
-*/
-#security_hotspots .radio-button-description {
- margin-left: 23px;
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
index cfa1e81f5ed..0f3165da554 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
@@ -20,6 +20,9 @@
import { groupBy, sortBy } from 'lodash';
import {
Hotspot,
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusOption,
RawHotspot,
ReviewHistoryElement,
ReviewHistoryType,
@@ -137,3 +140,35 @@ export function getHotspotReviewHistory(
functionalCount
};
}
+
+const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = {
+ [HotspotStatus.TO_REVIEW]: HotspotStatusOption.TO_REVIEW,
+ [HotspotStatus.REVIEWED]: HotspotStatusOption.FIXED,
+ [HotspotResolution.FIXED]: HotspotStatusOption.FIXED,
+ [HotspotResolution.SAFE]: HotspotStatusOption.SAFE
+};
+
+export function getStatusOptionFromStatusAndResolution(
+ status: HotspotStatus,
+ resolution?: HotspotResolution
+) {
+ // Resolution is the most determinist info here, so we use it first to get the matching status option
+ // If not provided, we use the status (which will be TO_REVIEW)
+ return STATUS_AND_RESOLUTION_TO_STATUS_OPTION[resolution ?? status];
+}
+
+const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = {
+ [HotspotStatusOption.TO_REVIEW]: { status: HotspotStatus.TO_REVIEW, resolution: undefined },
+ [HotspotStatusOption.FIXED]: {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.FIXED
+ },
+ [HotspotStatusOption.SAFE]: {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ }
+};
+
+export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) {
+ return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption];
+}
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 00ac45c8c14..6ab8963e914 100644
--- a/server/sonar-web/src/main/js/types/security-hotspots.ts
+++ b/server/sonar-web/src/main/js/types/security-hotspots.ts
@@ -42,7 +42,7 @@ export enum HotspotStatusFilter {
export enum HotspotStatusOption {
FIXED = 'FIXED',
SAFE = 'SAFE',
- ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW'
+ TO_REVIEW = 'TO_REVIEW'
}
export interface HotspotFilters {
@@ -60,10 +60,10 @@ export interface RawHotspot {
line?: number;
message: string;
project: string;
- resolution?: string;
+ resolution?: HotspotResolution;
rule: string;
securityCategory: string;
- status: string;
+ status: HotspotStatus;
subProject?: string;
updateDate: string;
vulnerabilityProbability: RiskExposure;
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 f651a6a2837..d5fa52bc55b 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -664,9 +664,6 @@ hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed
hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe
hotspots.risk_exposure=Review priority:
-hotspot.category=Category:
-hotspot.status=Status:
-hotspot.assigned_to=Assigned to:
hotspots.tabs.risk_description=What's the risk?
hotspots.tabs.vulnerability_description=Are you at risk?
hotspots.tabs.fix_recommendations=How can you fix it?
@@ -677,12 +674,17 @@ hotspots.tabs.review_history.comment.add=Add a comment
hotspots.tabs.review_history.comment.field=Comment:
hotspots.tabs.review_history.comment.submit=Comment
-hotspot.change_status.REVIEWED=Change status
-hotspot.change_status.TO_REVIEW=Review Hotspot
-
-hotspot.status.TO_REVIEW=To review
-hotspot.status.FIXED=Fixed
-hotspot.status.SAFE=Safe
+hotspots.assignee.select_user=Select a user...
+hotspots.status.cannot_change_status=Changing a hotspot's status requires permission.
+hotspots.status.select_status=Select a status...
+hotspots.status.add_comment=Add a comment (Optional)
+hotspots.status.change_status=Change status
+hotspots.status_option.TO_REVIEW=To review
+hotspots.status_option.TO_REVIEW.description=This Security Hotspot needs to be reviewed to assess whether the code poses a risk.
+hotspots.status_option.FIXED=Fixed
+hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
+hotspots.status_option.SAFE=Safe
+hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
hotspot.filters.title=Filters
hotspot.filters.assignee.assigned_to_me=Assigned to me
@@ -697,24 +699,6 @@ hotspot.filters.show_all=Show all hotspots
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot
-hotspots.form.title=Mark Security Hotspot as:
-hotspots.form.title.disabled=Security Hotspot is marked as:
-
-hotspots.form.cannot_change_status=Changing a hotspot's status requires permission.
-hotspots.form.assign_to=Assign to:
-hotspots.form.select_user=Select a user...
-hotspots.form.comment=Comment:
-hotspots.form.comment.placeholder=For tracking purposes, we highly recommend explaining why the code is safe.
-hotspots.form.submit.TO_REVIEW=Submit Review
-hotspots.form.submit.REVIEWED=Apply changes
-
-hotspots.status_option.FIXED=Fixed
-hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
-hotspots.status_option.SAFE=Safe
-hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
-hotspots.status_option.ADDITIONAL_REVIEW=Needs additional review
-hotspots.status_option.ADDITIONAL_REVIEW.description=Someone else needs to review this Security Hotspot.
-
#------------------------------------------------------------------------------
#
# ISSUES
--
2.39.5