3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.issue.workflow;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.util.Calendar;
26 import java.util.Collection;
27 import java.util.Date;
28 import java.util.List;
29 import java.util.stream.Stream;
30 import javax.annotation.Nullable;
31 import org.apache.commons.lang.time.DateUtils;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.sonar.api.issue.Issue;
35 import org.sonar.api.rule.RuleKey;
36 import org.sonar.core.issue.DefaultIssue;
37 import org.sonar.core.issue.FieldDiffs;
38 import org.sonar.core.issue.IssueChangeContext;
39 import org.sonar.server.issue.IssueFieldsSetter;
41 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
42 import static org.assertj.core.api.Assertions.assertThat;
43 import static org.sonar.api.issue.DefaultTransitions.RESET_AS_TO_REVIEW;
44 import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_ACKNOWLEDGED;
45 import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_REVIEWED;
46 import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_SAFE;
47 import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
48 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
49 import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
50 import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
51 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
52 import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
53 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
54 import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
55 import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByScanBuilder;
56 import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
57 import static org.sonar.db.rule.RuleTesting.XOO_X1;
58 import static org.sonar.server.issue.workflow.IssueWorkflowTest.emptyIfNull;
60 @RunWith(DataProviderRunner.class)
61 public class IssueWorkflowForSecurityHotspotsTest {
62 private static final IssueChangeContext SOME_CHANGE_CONTEXT = issueChangeContextByUserBuilder(new Date(), "USER1").build();
63 private static final List<String> RESOLUTION_TYPES = List.of(RESOLUTION_FIXED, RESOLUTION_SAFE, RESOLUTION_ACKNOWLEDGED);
65 private final IssueFieldsSetter updater = new IssueFieldsSetter();
66 private final IssueWorkflow underTest = new IssueWorkflow(new FunctionExecutor(updater), updater);
69 @UseDataProvider("anyResolutionIncludingNone")
70 public void to_review_hotspot_with_any_resolution_can_be_resolved_as_safe_or_fixed(@Nullable String resolution) {
72 DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, resolution);
74 List<Transition> transitions = underTest.outTransitions(hotspot);
76 assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESOLVE_AS_SAFE, RESOLVE_AS_ACKNOWLEDGED);
80 public static Object[][] anyResolutionIncludingNone() {
82 Issue.RESOLUTIONS.stream(),
83 Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
84 Stream.of(randomAlphabetic(12), null))
86 .map(t -> new Object[] {t})
87 .toArray(Object[][]::new);
91 public void reviewed_as_fixed_hotspot_can_be_resolved_as_safe_or_put_back_to_review() {
93 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED);
95 List<Transition> transitions = underTest.outTransitions(hotspot);
97 assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_SAFE, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
101 public void reviewed_as_safe_hotspot_can_be_resolved_as_fixed_or_put_back_to_review() {
103 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_SAFE);
105 List<Transition> transitions = underTest.outTransitions(hotspot);
107 assertThat(keys(transitions)).containsExactlyInAnyOrder(RESOLVE_AS_REVIEWED, RESET_AS_TO_REVIEW, RESOLVE_AS_ACKNOWLEDGED);
111 @UseDataProvider("anyResolutionButSafeOrFixed")
112 public void reviewed_with_any_resolution_but_safe_or_fixed_can_not_be_changed(String resolution) {
114 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, resolution);
116 List<Transition> transitions = underTest.outTransitions(hotspot);
118 assertThat(transitions).isEmpty();
122 public static Object[][] anyResolutionButSafeOrFixed() {
124 Issue.RESOLUTIONS.stream(),
125 Issue.SECURITY_HOTSPOT_RESOLUTIONS.stream(),
126 Stream.of(randomAlphabetic(12)))
128 .filter(t -> !RESOLUTION_TYPES.contains(t))
129 .map(t -> new Object[] {t})
130 .toArray(Object[][]::new);
134 public void doManualTransition_to_review_hostpot_is_resolved_as_fixed() {
136 DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, null);
138 boolean result = underTest.doManualTransition(hotspot, RESOLVE_AS_REVIEWED, SOME_CHANGE_CONTEXT);
140 assertThat(result).isTrue();
141 assertThat(hotspot.getStatus()).isEqualTo(STATUS_REVIEWED);
142 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_FIXED);
146 public void doManualTransition_reviewed_as_safe_hostpot_is_resolved_as_fixed() {
148 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_SAFE);
150 boolean result = underTest.doManualTransition(hotspot, RESOLVE_AS_REVIEWED, SOME_CHANGE_CONTEXT);
152 assertThat(result).isTrue();
153 assertThat(hotspot.getStatus()).isEqualTo(STATUS_REVIEWED);
154 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_FIXED);
158 public void doManualTransition_to_review_hostpot_is_resolved_as_safe() {
160 DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, null);
162 boolean result = underTest.doManualTransition(hotspot, RESOLVE_AS_SAFE, SOME_CHANGE_CONTEXT);
164 assertThat(result).isTrue();
165 assertThat(hotspot.getStatus()).isEqualTo(STATUS_REVIEWED);
166 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_SAFE);
170 public void doManualTransition_reviewed_as_fixed_hostpot_is_resolved_as_safe() {
172 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED);
174 boolean result = underTest.doManualTransition(hotspot, RESOLVE_AS_SAFE, SOME_CHANGE_CONTEXT);
176 assertThat(result).isTrue();
177 assertThat(hotspot.getStatus()).isEqualTo(STATUS_REVIEWED);
178 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_SAFE);
182 public void doManualTransition_reviewed_as_fixed_hostpot_is_put_back_to_review() {
184 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED);
186 boolean result = underTest.doManualTransition(hotspot, RESET_AS_TO_REVIEW, SOME_CHANGE_CONTEXT);
188 assertThat(result).isTrue();
189 assertThat(hotspot.getStatus()).isEqualTo(STATUS_TO_REVIEW);
190 assertThat(hotspot.resolution()).isNull();
194 public void doManualTransition_reviewed_as_safe_hostpot_is_put_back_to_review() {
196 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_SAFE);
198 boolean result = underTest.doManualTransition(hotspot, RESET_AS_TO_REVIEW, SOME_CHANGE_CONTEXT);
200 assertThat(result).isTrue();
201 assertThat(hotspot.getStatus()).isEqualTo(STATUS_TO_REVIEW);
202 assertThat(hotspot.resolution()).isNull();
206 public void reset_as_to_review_from_reviewed() {
208 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, RESOLUTION_FIXED);
210 boolean result = underTest.doManualTransition(hotspot, RESET_AS_TO_REVIEW, SOME_CHANGE_CONTEXT);
211 assertThat(result).isTrue();
212 assertThat(hotspot.type()).isEqualTo(SECURITY_HOTSPOT);
213 assertThat(hotspot.getStatus()).isEqualTo(STATUS_TO_REVIEW);
214 assertThat(hotspot.resolution()).isNull();
218 public void automatically_close_resolved_security_hotspots_in_status_to_review() {
220 DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, null)
222 .setBeingClosed(true);
223 Date now = new Date();
225 underTest.doAutomaticTransition(hotspot, issueChangeContextByScanBuilder(now).build());
227 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_FIXED);
228 assertThat(hotspot.status()).isEqualTo(STATUS_CLOSED);
229 assertThat(hotspot.closeDate()).isNotNull();
230 assertThat(hotspot.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND));
234 @UseDataProvider("safeOrFixedResolutions")
235 public void automatically_close_hotspot_resolved_as_fixed_or_safe(String resolution) {
237 DefaultIssue hotspot = newHotspot(STATUS_REVIEWED, resolution)
239 .setBeingClosed(true);
240 Date now = new Date();
242 underTest.doAutomaticTransition(hotspot, issueChangeContextByScanBuilder(now).build());
244 assertThat(hotspot.resolution()).isEqualTo(RESOLUTION_FIXED);
245 assertThat(hotspot.status()).isEqualTo(STATUS_CLOSED);
246 assertThat(hotspot.closeDate()).isNotNull();
247 assertThat(hotspot.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND));
251 public static Object[][] safeOrFixedResolutions() {
252 return new Object[][] {
259 public void automatically_reopen_closed_security_hotspots() {
260 DefaultIssue hotspot1 = newClosedHotspot(RESOLUTION_REMOVED);
261 setStatusPreviousToClosed(hotspot1, STATUS_REVIEWED, RESOLUTION_SAFE, RESOLUTION_REMOVED);
263 DefaultIssue hotspot2 = newClosedHotspot(RESOLUTION_FIXED);
264 setStatusPreviousToClosed(hotspot2, STATUS_TO_REVIEW, null, RESOLUTION_FIXED);
266 Date now = new Date();
269 underTest.doAutomaticTransition(hotspot1, issueChangeContextByScanBuilder(now).build());
270 underTest.doAutomaticTransition(hotspot2, issueChangeContextByScanBuilder(now).build());
272 assertThat(hotspot1.updateDate()).isNotNull();
273 assertThat(hotspot1.status()).isEqualTo(STATUS_REVIEWED);
274 assertThat(hotspot1.resolution()).isEqualTo(RESOLUTION_SAFE);
276 assertThat(hotspot2.updateDate()).isNotNull();
277 assertThat(hotspot2.status()).isEqualTo(STATUS_TO_REVIEW);
278 assertThat(hotspot2.resolution()).isNull();
282 public void doAutomaticTransition_does_nothing_on_security_hotspots_in_to_review_status() {
283 DefaultIssue hotspot = newHotspot(STATUS_TO_REVIEW, null)
288 underTest.doAutomaticTransition(hotspot, issueChangeContextByScanBuilder(new Date()).build());
290 assertThat(hotspot.status()).isEqualTo(STATUS_TO_REVIEW);
291 assertThat(hotspot.resolution()).isNull();
294 private Collection<String> keys(List<Transition> transitions) {
295 return transitions.stream().map(Transition::key).toList();
298 private static void setStatusPreviousToClosed(DefaultIssue hotspot, String previousStatus, @Nullable String previousResolution, @Nullable String newResolution) {
299 addStatusChange(hotspot, new Date(), previousStatus, STATUS_CLOSED, previousResolution, newResolution);
302 private static void addStatusChange(DefaultIssue issue, Date date, String previousStatus, String newStatus, @Nullable String previousResolution, @Nullable String newResolution) {
303 issue.addChange(new FieldDiffs()
304 .setCreationDate(date)
305 .setDiff("status", previousStatus, newStatus)
306 .setDiff("resolution", emptyIfNull(previousResolution), emptyIfNull(newResolution)));
309 private static DefaultIssue newClosedHotspot(String resolution) {
310 return newHotspot(STATUS_CLOSED, resolution)
312 .setRuleKey(RuleKey.of("js", "S001"))
314 .setCloseDate(new Date(5_999_999L));
317 private static DefaultIssue newHotspot(String status, @Nullable String resolution) {
318 return new DefaultIssue()
319 .setType(SECURITY_HOTSPOT)
321 .setResolution(resolution);