aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-server-common
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2018-08-14 18:07:10 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-21 20:21:05 +0200
commit3065f6824c72d504c8e317c7d2d6a2c682081c4f (patch)
treeaa9a369907d4c31f3225e81f6073de77ba887d9e /server/sonar-server-common
parent61c813392f60ec29e4b628e7244593c627058b5b (diff)
downloadsonarqube-3065f6824c72d504c8e317c7d2d6a2c682081c4f.tar.gz
sonarqube-3065f6824c72d504c8e317c7d2d6a2c682081c4f.zip
SONAR-8368 reopen closed issues (restore status)
but those from Hotspots rules and manual vulnerabilities
Diffstat (limited to 'server/sonar-server-common')
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java39
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java50
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java155
3 files changed, 242 insertions, 2 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
index 5eaa9d556e6..fb27f0d5853 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java
@@ -38,7 +38,7 @@ import static com.google.common.base.Preconditions.checkState;
@ComputeEngineSide
public class IssueWorkflow implements Startable {
- public static final String AUTOMATIC_CLOSE_TRANSITION = "automaticclose";
+ private static final String AUTOMATIC_CLOSE_TRANSITION = "automaticclose";
private final FunctionExecutor functionExecutor;
private final IssueFieldsSetter updater;
private StateMachine machine;
@@ -242,7 +242,42 @@ public class IssueWorkflow implements Startable {
.conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED), IsNotHotspotNorManualVulnerability.INSTANCE)
.functions(new SetResolution(null), UnsetCloseDate.INSTANCE)
.automatic()
- .build());
+ .build())
+
+ .transition(Transition.builder("automaticuncloseopen")
+ .from(Issue.STATUS_CLOSED).to(Issue.STATUS_OPEN)
+ .conditions(
+ new PreviousStatusWas(Issue.STATUS_OPEN),
+ new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED),
+ IsNotHotspotNorManualVulnerability.INSTANCE)
+ .automatic()
+ .build())
+ .transition(Transition.builder("automaticunclosereopen")
+ .from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED)
+ .conditions(
+ new PreviousStatusWas(Issue.STATUS_REOPENED),
+ new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED),
+ IsNotHotspotNorManualVulnerability.INSTANCE)
+ .automatic()
+ .build())
+ .transition(Transition.builder("automaticuncloseconfirmed")
+ .from(Issue.STATUS_CLOSED).to(Issue.STATUS_CONFIRMED)
+ .conditions(
+ new PreviousStatusWas(Issue.STATUS_CONFIRMED),
+ new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED),
+ IsNotHotspotNorManualVulnerability.INSTANCE)
+ .automatic()
+ .build())
+ .transition(Transition.builder("automaticuncloseresolved")
+ .from(Issue.STATUS_CLOSED).to(Issue.STATUS_RESOLVED)
+ .conditions(
+ new PreviousStatusWas(Issue.STATUS_RESOLVED),
+ new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED),
+ IsNotHotspotNorManualVulnerability.INSTANCE)
+ .automatic()
+ .build())
+
+ ;
}
@Override
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java
new file mode 100644
index 00000000000..e3fec3623ae
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+package org.sonar.server.issue.workflow;
+
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import org.sonar.api.issue.Issue;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.FieldDiffs;
+
+class PreviousStatusWas implements Condition {
+ private final String expectedPreviousStatus;
+
+ PreviousStatusWas(String expectedPreviousStatus) {
+ this.expectedPreviousStatus = expectedPreviousStatus;
+ }
+
+ @Override
+ public boolean matches(Issue issue) {
+ DefaultIssue defaultIssue = (DefaultIssue) issue;
+ Optional<String> lastPreviousStatus = defaultIssue.changes().stream()
+ // exclude current change (if any)
+ .filter(change -> change != defaultIssue.currentChange())
+ .sorted(Comparator.comparing(FieldDiffs::creationDate).reversed())
+ .map(change -> change.get("status"))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .map(t -> (String) t.oldValue());
+
+ return lastPreviousStatus.filter(this.expectedPreviousStatus::equals).isPresent();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java
index bc508bd450d..c515234fd08 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java
@@ -21,6 +21,10 @@ package org.sonar.server.issue.workflow;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
@@ -28,16 +32,21 @@ import java.util.List;
import javax.annotation.Nullable;
import org.apache.commons.lang.time.DateUtils;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.server.issue.IssueFieldsSetter;
+import static org.apache.commons.lang.time.DateUtils.addDays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED;
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
import static org.sonar.api.issue.Issue.STATUS_CLOSED;
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
@@ -45,6 +54,7 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
+@RunWith(DataProviderRunner.class)
public class IssueWorkflowTest {
IssueFieldsSetter updater = new IssueFieldsSetter();
@@ -148,6 +158,151 @@ public class IssueWorkflowTest {
}
@Test
+ @UseDataProvider("allStatusesLeadingToClosed")
+ public void automatically_reopen_closed_issue_to_its_previous_status_from_changelog(String previousStatus) {
+ DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(resolution -> {
+ DefaultIssue issue = newClosedIssue(resolution);
+ setStatusPreviousToClosed(issue, previousStatus);
+ return issue;
+ })
+ .toArray(DefaultIssue[]::new);
+ Date now = new Date();
+ workflow.start();
+
+ Arrays.stream(issues).forEach(issue -> {
+ workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
+
+ assertThat(issue.status()).isEqualTo(previousStatus);
+ assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND));
+ assertThat(issue.closeDate()).isNull();
+ assertThat(issue.isChanged()).isTrue();
+ });
+ }
+
+ @Test
+ @UseDataProvider("allStatusesLeadingToClosed")
+ public void automatically_reopen_closed_issue_to_most_recent_previous_status_from_changelog(String previousStatus) {
+ DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(resolution -> {
+ DefaultIssue issue = newClosedIssue(resolution);
+ Date now = new Date();
+ addStatusChange(issue, addDays(now, -60), STATUS_OPEN, STATUS_CONFIRMED);
+ addStatusChange(issue, addDays(now, -10), STATUS_CONFIRMED, previousStatus);
+ addStatusChange(issue, now, previousStatus, STATUS_CLOSED);
+ return issue;
+ })
+ .toArray(DefaultIssue[]::new);
+ Date now = new Date();
+ workflow.start();
+
+ Arrays.stream(issues).forEach(issue -> {
+ workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
+
+ assertThat(issue.status()).isEqualTo(previousStatus);
+ assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND));
+ });
+ }
+
+ @DataProvider
+ public static Object[][] allStatusesLeadingToClosed() {
+ return new Object[][] {
+ {STATUS_OPEN},
+ {STATUS_REOPENED},
+ {STATUS_CONFIRMED},
+ {STATUS_RESOLVED}
+ };
+ }
+
+ private static final String[] SUPPORTED_RESOLUTIONS_FOR_UNCLOSING = new String[] {RESOLUTION_FIXED, RESOLUTION_REMOVED};
+
+ @DataProvider
+ public static Object[][] supportedResolutionsForUnClosing() {
+ return Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(t -> new Object[] {t})
+ .toArray(Object[][]::new);
+ }
+
+ @Test
+ public void do_not_automatically_reopen_closed_issue_which_have_no_previous_status_in_changelog() {
+ DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(IssueWorkflowTest::newClosedIssue)
+ .toArray(DefaultIssue[]::new);
+ Date now = new Date();
+ workflow.start();
+
+ Arrays.stream(issues).forEach(issue -> {
+ workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
+
+ assertThat(issue.status()).isEqualTo(STATUS_CLOSED);
+ assertThat(issue.updateDate()).isNull();
+ });
+ }
+
+ @Test
+ @UseDataProvider("allStatusesLeadingToClosed")
+ public void do_not_automatically_reopen_closed_issues_of_security_hotspots(String previousStatus) {
+ DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(resolution -> {
+ DefaultIssue issue = newClosedIssue(resolution);
+ setStatusPreviousToClosed(issue, previousStatus);
+ issue.setType(RuleType.SECURITY_HOTSPOT);
+ return issue;
+ })
+ .toArray(DefaultIssue[]::new);
+ Date now = new Date();
+ workflow.start();
+
+ Arrays.stream(issues).forEach(issue -> {
+ workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
+
+ assertThat(issue.status()).isEqualTo(STATUS_CLOSED);
+ assertThat(issue.updateDate()).isNull();
+ });
+ }
+
+ @Test
+ @UseDataProvider("allStatusesLeadingToClosed")
+ public void do_not_automatically_reopen_closed_issues_of_manual_vulnerability(String previousStatus) {
+ DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING)
+ .map(resolution -> {
+ DefaultIssue issue = newClosedIssue(resolution);
+ setStatusPreviousToClosed(issue, previousStatus);
+ issue.setIsFromHotspot(true);
+ return issue;
+ })
+ .toArray(DefaultIssue[]::new);
+ Date now = new Date();
+ workflow.start();
+
+ Arrays.stream(issues).forEach(issue -> {
+ workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
+
+ assertThat(issue.status()).isEqualTo(STATUS_CLOSED);
+ assertThat(issue.updateDate()).isNull();
+ });
+ }
+
+ private static DefaultIssue newClosedIssue(String resolution) {
+ DefaultIssue res = new DefaultIssue()
+ .setKey("ABCDE")
+ .setRuleKey(RuleKey.of("js", "S001"))
+ .setResolution(resolution)
+ .setStatus(STATUS_CLOSED)
+ .setNew(false)
+ .setCloseDate(new Date(5_999_999L));
+ return res;
+ }
+
+ private static void setStatusPreviousToClosed(DefaultIssue issue, String previousStatus) {
+ addStatusChange(issue, new Date(), previousStatus, STATUS_CLOSED);
+ }
+
+ private static void addStatusChange(DefaultIssue issue, Date date, String previousStatus, String newStatus) {
+ issue.addChange(new FieldDiffs().setCreationDate(date).setDiff("status", previousStatus, newStatus));
+ }
+
+ @Test
public void close_open_dead_issue() {
workflow.start();