diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2018-08-14 18:07:10 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-21 20:21:05 +0200 |
commit | 3065f6824c72d504c8e317c7d2d6a2c682081c4f (patch) | |
tree | aa9a369907d4c31f3225e81f6073de77ba887d9e /server/sonar-server-common | |
parent | 61c813392f60ec29e4b628e7244593c627058b5b (diff) | |
download | sonarqube-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')
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(); |