aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-09-29 16:50:57 +0200
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-10-17 15:13:58 +0200
commitdf058fb1983f9ef60fa3c467c209db5c0f08050b (patch)
tree5716b3719e866116a7c7247b006ce356a58d84f1 /server
parentca2ea93cc15c20595896b5308aac2e2b320c4fd4 (diff)
downloadsonarqube-df058fb1983f9ef60fa3c467c209db5c0f08050b.tar.gz
sonarqube-df058fb1983f9ef60fa3c467c209db5c0f08050b.zip
SONAR-9871 call webhook on single issue change on short lived branch
Diffstat (limited to 'server')
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java63
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java257
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java23
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java9
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java4
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java9
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooks.java8
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java477
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java53
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java4
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java4
12 files changed, 905 insertions, 8 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java
new file mode 100644
index 00000000000..edb30e69dd7
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.webhook;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.server.issue.ws.SearchResponseData;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public interface IssueChangeWebhook {
+ /**
+ * Will call webhooks once for any short living branch which has at least one issue in {@link SearchResponseData} and
+ * if change described in {@link IssueChange} can alter the status of the short living branch.
+ */
+ void onChange(SearchResponseData searchResponseData, IssueChange issueChange, IssueChangeContext context);
+
+ final class IssueChange {
+ private final RuleType ruleType;
+ private final String transitionKey;
+
+ public IssueChange(RuleType ruleType) {
+ this(ruleType, null);
+ }
+
+ public IssueChange(String transitionKey) {
+ this(null, transitionKey);
+ }
+
+ IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) {
+ checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null");
+ this.ruleType = ruleType;
+ this.transitionKey = transitionKey;
+ }
+
+ public Optional<RuleType> getRuleType() {
+ return Optional.ofNullable(ruleType);
+ }
+
+ public Optional<String> getTransitionKey() {
+ return Optional.ofNullable(transitionKey);
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java
new file mode 100644
index 00000000000..a27c1ae030f
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java
@@ -0,0 +1,257 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.webhook;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.elasticsearch.action.search.SearchResponse;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.issue.DefaultTransitions;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+import org.sonar.server.es.Facets;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.ws.SearchResponseData;
+import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.webhook.Analysis;
+import org.sonar.server.webhook.Branch;
+import org.sonar.server.webhook.Project;
+import org.sonar.server.webhook.ProjectAnalysis;
+import org.sonar.server.webhook.QualityGate;
+import org.sonar.server.webhook.WebHooks;
+import org.sonar.server.webhook.WebhookPayload;
+import org.sonar.server.webhook.WebhookPayloadFactory;
+
+import static java.lang.String.format;
+import static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class IssueChangeWebhookImpl implements IssueChangeWebhook {
+ private static final Set<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of(
+ DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+ private final DbClient dbClient;
+ private final WebHooks webhooks;
+ private final Configuration configuration;
+ private final WebhookPayloadFactory webhookPayloadFactory;
+ private final IssueIndex issueIndex;
+
+ public IssueChangeWebhookImpl(DbClient dbClient, WebHooks webhooks, Configuration configuration,
+ WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex) {
+ this.dbClient = dbClient;
+ this.webhooks = webhooks;
+ this.configuration = configuration;
+ this.webhookPayloadFactory = webhookPayloadFactory;
+ this.issueIndex = issueIndex;
+ }
+
+ @Override
+ public void onChange(SearchResponseData searchResponseData, IssueChange issueChange, IssueChangeContext context) {
+ if (isEmpty(searchResponseData) || !isUserChangeContext(context) || !isRelevant(issueChange)) {
+ return;
+ }
+
+ callWebHook(searchResponseData);
+ }
+
+ private static boolean isRelevant(IssueChange issueChange) {
+ return issueChange.getTransitionKey().map(IssueChangeWebhookImpl::isMeaningfulTransition).orElse(true);
+ }
+
+ private static boolean isEmpty(SearchResponseData searchResponseData) {
+ return searchResponseData.getIssues().isEmpty();
+ }
+
+ private static boolean isUserChangeContext(IssueChangeContext context) {
+ return context.login() != null;
+ }
+
+ private static boolean isMeaningfulTransition(String transitionKey) {
+ return MEANINGFUL_TRANSITIONS.contains(transitionKey);
+ }
+
+ private void callWebHook(SearchResponseData searchResponseData) {
+ if (!webhooks.isEnabled(configuration)) {
+ return;
+ }
+
+ Set<String> componentUuids = searchResponseData.getIssues().stream()
+ .map(IssueDto::getComponentUuid)
+ .collect(toSet());
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, componentUuids, searchResponseData);
+ if (branchesByUuid.isEmpty()) {
+ return;
+ }
+
+ Set<String> branchProjectUuids = branchesByUuid.values().stream()
+ .map(ComponentDto::uuid)
+ .collect(toSet(branchesByUuid.size()));
+ Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids)
+ .stream()
+ .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT)
+ .collect(toSet(branchesByUuid.size()));
+ if (shortBranches.isEmpty()) {
+ return;
+ }
+
+ Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(
+ dbSession,
+ shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size())))
+ .stream()
+ .collect(uniqueIndex(SnapshotDto::getComponentUuid));
+ shortBranches
+ .forEach(shortBranch -> {
+ ComponentDto branch = branchesByUuid.get(shortBranch.getUuid());
+ SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid());
+ if (branch != null && analysis != null) {
+ webhooks.sendProjectAnalysisUpdate(
+ configuration,
+ new WebHooks.Analysis(shortBranch.getUuid(), analysis.getUuid(), null),
+ () -> buildWebHookPayload(branch, shortBranch, analysis));
+ }
+ });
+ }
+ }
+
+ private WebhookPayload buildWebHookPayload(ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) {
+ ProjectAnalysis projectAnalysis = new ProjectAnalysis(
+ new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()),
+ null,
+ new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
+ new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
+ createQualityGate(branch, issueIndex),
+ null,
+ Collections.emptyMap());
+ return webhookPayloadFactory.create(projectAnalysis);
+ }
+
+ private static QualityGate createQualityGate(ComponentDto branch, IssueIndex issueIndex) {
+ SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
+ .projectUuids(singletonList(branch.getMainBranchProjectUuid()))
+ .branchUuid(branch.uuid())
+ .mainBranch(false)
+ .resolved(false)
+ .checkAuthorization(false)
+ .build(),
+ new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
+ LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse)
+ .get(RuleIndex.FACET_TYPES);
+
+ Set<QualityGate.Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
+ .map(c -> toCondition(typeFacet, c))
+ .collect(MoreCollectors.toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
+
+ return new QualityGate(valueOf(ShortLivingBranchQualityGate.ID), ShortLivingBranchQualityGate.NAME, qgStatusFrom(conditions), conditions);
+ }
+
+ private static QualityGate.Condition toCondition(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
+ long measure = getMeasure(typeFacet, c);
+ QualityGate.EvaluationStatus status = measure > 0 ? QualityGate.EvaluationStatus.ERROR : QualityGate.EvaluationStatus.OK;
+ return new QualityGate.Condition(status, c.getMetricKey(),
+ toOperator(c),
+ c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak(),
+ valueOf(measure));
+ }
+
+ private static QualityGate.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
+ String operator = c.getOperator();
+ switch (operator) {
+ case QualityGateConditionDto.OPERATOR_GREATER_THAN:
+ return QualityGate.Operator.GREATER_THAN;
+ case QualityGateConditionDto.OPERATOR_LESS_THAN:
+ return QualityGate.Operator.LESS_THAN;
+ case QualityGateConditionDto.OPERATOR_EQUALS:
+ return QualityGate.Operator.EQUALS;
+ case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
+ return QualityGate.Operator.NOT_EQUALS;
+ default:
+ throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
+ }
+ }
+
+ private static QualityGate.Status qgStatusFrom(Set<QualityGate.Condition> conditions) {
+ if (conditions.stream().anyMatch(c -> c.getStatus() == QualityGate.EvaluationStatus.ERROR)) {
+ return QualityGate.Status.ERROR;
+ }
+ return QualityGate.Status.OK;
+ }
+
+ private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
+ String metricKey = c.getMetricKey();
+ switch (metricKey) {
+ case CoreMetrics.BUGS_KEY:
+ return getValueForRuleType(typeFacet, RuleType.BUG);
+ case CoreMetrics.VULNERABILITIES_KEY:
+ return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
+ case CoreMetrics.CODE_SMELLS_KEY:
+ return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
+ default:
+ throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
+ }
+ }
+
+ private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
+ Long res = facet.get(ruleType.name());
+ if (res == null) {
+ return 0L;
+ }
+ return res;
+ }
+
+ private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, Set<String> componentUuids, SearchResponseData searchResponseData) {
+ Set<String> missingComponentUuids = ImmutableSet.copyOf(Sets.difference(
+ componentUuids,
+ searchResponseData.getComponents()
+ .stream()
+ .map(ComponentDto::uuid)
+ .collect(Collectors.toSet())));
+ if (missingComponentUuids.isEmpty()) {
+ return searchResponseData.getComponents()
+ .stream()
+ .collect(uniqueIndex(ComponentDto::uuid));
+ }
+ return Stream.concat(
+ searchResponseData.getComponents().stream(),
+ dbClient.componentDao().selectByUuids(dbSession, missingComponentUuids).stream())
+ .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
+ .collect(uniqueIndex(ComponentDto::uuid));
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java
new file mode 100644
index 00000000000..9f828ad5e5e
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.issue.webhook;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
index acdb637bfeb..6da81b9afd0 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
@@ -35,6 +35,7 @@ import org.sonar.db.issue.IssueDto;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.IssueUpdater;
import org.sonar.server.issue.TransitionService;
+import org.sonar.server.issue.webhook.IssueChangeWebhook;
import org.sonar.server.user.UserSession;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
@@ -49,15 +50,17 @@ public class DoTransitionAction implements IssuesWsAction {
private final IssueUpdater issueUpdater;
private final TransitionService transitionService;
private final OperationResponseWriter responseWriter;
+ private final IssueChangeWebhook issueChangeWebhook;
public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
- OperationResponseWriter responseWriter) {
+ OperationResponseWriter responseWriter, IssueChangeWebhook issueChangeWebhook) {
this.dbClient = dbClient;
this.userSession = userSession;
this.issueFinder = issueFinder;
this.issueUpdater = issueUpdater;
this.transitionService = transitionService;
this.responseWriter = responseWriter;
+ this.issueChangeWebhook = issueChangeWebhook;
}
@Override
@@ -99,7 +102,9 @@ public class DoTransitionAction implements IssuesWsAction {
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin());
transitionService.checkTransitionPermission(transitionKey, defaultIssue);
if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
- return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null);
+ SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null);
+ issueChangeWebhook.onChange(searchResponseData, new IssueChangeWebhook.IssueChange(transitionKey), context);
+ return searchResponseData;
}
return new SearchResponseData(issueDto);
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
index f6c4d23e86e..e0ac1a58988 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
@@ -27,6 +27,7 @@ import org.sonar.server.issue.IssueQueryFactory;
import org.sonar.server.issue.IssueUpdater;
import org.sonar.server.issue.ServerIssueStorage;
import org.sonar.server.issue.TransitionService;
+import org.sonar.server.issue.webhook.IssueChangeWebhookImpl;
import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.ws.WsResponseCommonFormat;
@@ -63,6 +64,7 @@ public class IssueWsModule extends Module {
ComponentTagsAction.class,
AuthorsAction.class,
ChangelogAction.class,
- BulkChangeAction.class);
+ BulkChangeAction.class,
+ IssueChangeWebhookImpl.class);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
index 0c68784f034..c9512b35bab 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
@@ -35,6 +35,7 @@ import org.sonar.db.issue.IssueDto;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.IssueUpdater;
+import org.sonar.server.issue.webhook.IssueChangeWebhook;
import org.sonar.server.user.UserSession;
import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
@@ -50,15 +51,17 @@ public class SetTypeAction implements IssuesWsAction {
private final IssueFieldsSetter issueFieldsSetter;
private final IssueUpdater issueUpdater;
private final OperationResponseWriter responseWriter;
+ private final IssueChangeWebhook issueChangeWebhook;
public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
- OperationResponseWriter responseWriter) {
+ OperationResponseWriter responseWriter, IssueChangeWebhook issueChangeWebhook) {
this.userSession = userSession;
this.dbClient = dbClient;
this.issueFinder = issueFinder;
this.issueFieldsSetter = issueFieldsSetter;
this.issueUpdater = issueUpdater;
this.responseWriter = responseWriter;
+ this.issueChangeWebhook = issueChangeWebhook;
}
@Override
@@ -107,7 +110,9 @@ public class SetTypeAction implements IssuesWsAction {
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin());
if (issueFieldsSetter.setType(issue, ruleType, context)) {
- return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
+ SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
+ issueChangeWebhook.onChange(searchResponseData, new IssueChangeWebhook.IssueChange(ruleType), context);
+ return searchResponseData;
}
return new SearchResponseData(issueDto);
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooks.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooks.java
index 66bac2b535c..cf46e59008b 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooks.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooks.java
@@ -86,5 +86,13 @@ public interface WebHooks {
public int hashCode() {
return Objects.hash(projectUuid, ceTaskUuid, analysisUuid);
}
+
+ @Override
+ public String toString() {
+ return "Analysis{" +
+ "projectUuid='" + projectUuid + '\'' +
+ ", ceTaskUuid='" + ceTaskUuid + '\'' +
+ '}';
+ }
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java
new file mode 100644
index 00000000000..afd311201b7
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java
@@ -0,0 +1,477 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.webhook;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.issue.DefaultTransitions;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.issue.webhook.IssueChangeWebhook.IssueChange;
+import org.sonar.server.issue.ws.SearchResponseData;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.webhook.Analysis;
+import org.sonar.server.webhook.Branch;
+import org.sonar.server.webhook.Project;
+import org.sonar.server.webhook.ProjectAnalysis;
+import org.sonar.server.webhook.QualityGate;
+import org.sonar.server.webhook.QualityGate.EvaluationStatus;
+import org.sonar.server.webhook.WebHooks;
+import org.sonar.server.webhook.WebhookPayloadFactory;
+
+import static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
+import static org.sonar.server.webhook.QualityGate.Operator.GREATER_THAN;
+
+@RunWith(DataProviderRunner.class)
+public class IssueChangeWebhookImplTest {
+ private static final List<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
+ private static final List<String> NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList());
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ @Rule
+ public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+ private DbClient dbClient = dbTester.getDbClient();
+
+ private Random random = new Random();
+ private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
+ private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size()));
+ private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size()));
+ private RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)];
+
+ private IssueChangeContext scanChangeContext = IssueChangeContext.createScan(new Date());
+ private IssueChangeContext userChangeContext = IssueChangeContext.createUser(new Date(), "userLogin");
+ private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
+ private WebHooks webHooks = mock(WebHooks.class);
+ private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
+ private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
+ private Configuration mockedConfiguration = mock(Configuration.class);
+ private IssueChangeWebhookImpl underTest = new IssueChangeWebhookImpl(dbClient, webHooks, mockedConfiguration, webhookPayloadFactory, issueIndex);
+ private DbClient mockedDbClient = mock(DbClient.class);
+ private IssueIndex spiedOnIssueIndex = spy(issueIndex);
+ private IssueChangeWebhookImpl mockedUnderTest = new IssueChangeWebhookImpl(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+
+ @Before
+ public void setUp() throws Exception {
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(true);
+ }
+
+ @Test
+ public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+ mockedUnderTest.onChange(new SearchResponseData(Collections.emptyList()), new IssueChange(randomRuleType), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_change_has_no_effect_if_scan_IssueChangeContext() {
+ mockedUnderTest.onChange(new SearchResponseData(Collections.emptyList()), new IssueChange(randomRuleType), scanChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_change_has_no_effect_if_webhooks_are_disabled() {
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(false);
+
+ underTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(randomRuleType), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+ mockedUnderTest.onChange(new SearchResponseData(Collections.emptyList()), new IssueChange(randomAlphanumeric(12)), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void onTransition_has_no_effect_if_transition_key_is_empty() {
+ on_transition_changeHasNoEffectForTransitionKey("");
+ }
+
+ @Test
+ public void onTransition_has_no_effect_if_transition_key_is_random() {
+ on_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(99));
+ }
+
+ @Test
+ public void on_transition_change_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
+ Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
+ DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+ DefaultTransitions.ALL.stream()
+ .filter(s -> !supportedDefaultTransitionKeys.contains(s))
+ .forEach(this::on_transition_changeHasNoEffectForTransitionKey);
+ }
+
+ private void on_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
+ reset(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory);
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(true);
+
+ mockedUnderTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(transitionKey), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() {
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(true);
+
+ mockedUnderTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(randomAlphanumeric(12)), scanChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_transition_change_has_no_effect_if_webhooks_are_disabled() {
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(false);
+
+ mockedUnderTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(randomAlphanumeric(12)), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+ mockedUnderTest.onChange(new SearchResponseData(Collections.emptyList()), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() {
+ mockedUnderTest.onChange(new SearchResponseData(Collections.emptyList()), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_and_transition_change_has_no_effect_if_webhooks_are_disabled() {
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(false);
+
+ underTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ public void on_type_and_transition_has_no_effect_if_transition_key_is_empty() {
+ on_type_and_transition_changeHasNoEffectForTransitionKey("");
+ }
+
+ @Test
+ public void on_type_and_transition_has_no_effect_if_transition_key_is_random() {
+ on_type_and_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(66));
+ }
+
+ @Test
+ public void on_type_and_transition_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
+ Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
+ DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+ DefaultTransitions.ALL.stream()
+ .filter(s -> !supportedDefaultTransitionKeys.contains(s))
+ .forEach(this::on_type_and_transition_changeHasNoEffectForTransitionKey);
+ }
+
+ private void on_type_and_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
+ reset(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory);
+ when(webHooks.isEnabled(mockedConfiguration)).thenReturn(true);
+
+ mockedUnderTest.onChange(new SearchResponseData(singletonList(new IssueDto())), new IssueChange(randomRuleType, transitionKey), userChangeContext);
+
+ verifyZeroInteractions(mockedDbClient, webHooks, mockedConfiguration, webhookPayloadFactory, spiedOnIssueIndex);
+ }
+
+ @Test
+ @UseDataProvider("validIssueChanges")
+ public void call_webhook_for_short_living_branch_of_issue(IssueChange issueChange) {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto project = dbTester.components().insertPublicProject(organization);
+ ComponentDto branch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
+ .setBranchType(BranchType.SHORT)
+ .setKey("foo"));
+ SnapshotDto analysis = insertAnalysisTask(branch);
+
+ underTest.onChange(new SearchResponseData(new IssueDto().setComponent(branch)), issueChange, userChangeContext);
+
+ ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFromSupplier(branch, analysis);
+ assertThat(projectAnalysis).isEqualTo(
+ new ProjectAnalysis(
+ new Project(project.uuid(), project.getKey(), project.name()),
+ null,
+ new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
+ new Branch(false, "foo", Branch.Type.SHORT),
+ new QualityGate(
+ valueOf(ShortLivingBranchQualityGate.ID),
+ ShortLivingBranchQualityGate.NAME,
+ QualityGate.Status.OK,
+ ImmutableSet.of(
+ new QualityGate.Condition(EvaluationStatus.OK, BUGS_KEY, GREATER_THAN, "0", null, false, "0"),
+ new QualityGate.Condition(EvaluationStatus.OK, CoreMetrics.VULNERABILITIES_KEY, GREATER_THAN, "0", null, false, "0"),
+ new QualityGate.Condition(EvaluationStatus.OK, CODE_SMELLS_KEY, GREATER_THAN, "0", null, false, "0"))),
+ null,
+ Collections.emptyMap()));
+ }
+
+ @Test
+ public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto branch = insertPrivateBranch(organization);
+ SnapshotDto analysis = insertAnalysisTask(branch);
+
+ underTest.onChange(new SearchResponseData(new IssueDto().setComponent(branch)), new IssueChange(randomRuleType), userChangeContext);
+
+ ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFromSupplier(branch, analysis);
+ QualityGate qualityGate = projectAnalysis.getQualityGate().get();
+ assertThat(qualityGate.getStatus()).isEqualTo(QualityGate.Status.OK);
+ assertThat(qualityGate.getConditions())
+ .extracting(QualityGate.Condition::getStatus, QualityGate.Condition::getValue)
+ .containsOnly(Tuple.tuple(EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.BUG,
+ Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+ Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
+ Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.VULNERABILITY,
+ Tuple.tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
+ Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+ Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.CODE_SMELL,
+ Tuple.tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
+ Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
+ Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
+ }
+
+ private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto branch = insertPrivateBranch(organization);
+ SnapshotDto analysis = insertAnalysisTask(branch);
+ IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(branch, ruleType, randomOpenStatus, null));
+ IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(branch, ruleType, randomNonOpenStatus, randomResolution));
+ indexIssues(branch);
+
+ underTest.onChange(new SearchResponseData(new IssueDto().setComponent(branch)), new IssueChange(randomRuleType), userChangeContext);
+
+ ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFromSupplier(branch, analysis);
+ QualityGate qualityGate = projectAnalysis.getQualityGate().get();
+ assertThat(qualityGate.getStatus()).isEqualTo(QualityGate.Status.ERROR);
+ assertThat(qualityGate.getConditions())
+ .extracting(QualityGate.Condition::getMetricKey, QualityGate.Condition::getStatus, QualityGate.Condition::getValue)
+ .containsOnly(expectedQGConditions);
+ }
+
+ @Test
+ public void computes_QG_error_with_all_failing_conditions() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto branch = insertPrivateBranch(organization);
+ SnapshotDto analysis = insertAnalysisTask(branch);
+ int unresolvedBugs = 1 + random.nextInt(10);
+ int unresolvedVulnerabilities = 1 + random.nextInt(10);
+ int unresolvedCodeSmells = 1 + random.nextInt(10);
+ IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(branch, RuleType.BUG, randomOpenStatus, null));
+ IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(branch, RuleType.VULNERABILITY, randomOpenStatus, null));
+ IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(branch, RuleType.CODE_SMELL, randomOpenStatus, null));
+ indexIssues(branch);
+
+ underTest.onChange(new SearchResponseData(new IssueDto().setComponent(branch)), new IssueChange(randomRuleType), userChangeContext);
+
+ ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFromSupplier(branch, analysis);
+ QualityGate qualityGate = projectAnalysis.getQualityGate().get();
+ assertThat(qualityGate.getStatus()).isEqualTo(QualityGate.Status.ERROR);
+ assertThat(qualityGate.getConditions())
+ .extracting(QualityGate.Condition::getMetricKey, QualityGate.Condition::getStatus, QualityGate.Condition::getValue)
+ .containsOnly(
+ Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
+ Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
+ Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
+ }
+
+ @Test
+ public void call_webhook_only_once_per_branch_with_at_least_one_issue_in_SearchResponseData() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto branch1 = insertPrivateBranch(organization);
+ ComponentDto branch2 = insertPrivateBranch(organization);
+ ComponentDto branch3 = insertPrivateBranch(organization);
+ SnapshotDto analysis1 = insertAnalysisTask(branch1);
+ SnapshotDto analysis2 = insertAnalysisTask(branch2);
+ SnapshotDto analysis3 = insertAnalysisTask(branch3);
+ int issuesBranch1 = 2 + random.nextInt(10);
+ int issuesBranch2 = 2 + random.nextInt(10);
+ int issuesBranch3 = 2 + random.nextInt(10);
+ List<IssueDto> issueDtos = Stream.of(
+ IntStream.range(0, issuesBranch1).mapToObj(i -> new IssueDto().setComponent(branch1)),
+ IntStream.range(0, issuesBranch2).mapToObj(i -> new IssueDto().setComponent(branch2)),
+ IntStream.range(0, issuesBranch3).mapToObj(i -> new IssueDto().setComponent(branch3)))
+ .flatMap(s -> s)
+ .collect(MoreCollectors.toList());
+
+ underTest.onChange(new SearchResponseData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
+
+ verifyWebhookCalledAndExtractPayloadFromSupplier(branch1, analysis1);
+ verifyWebhookCalledAndExtractPayloadFromSupplier(branch2, analysis2);
+ verifyWebhookCalledAndExtractPayloadFromSupplier(branch3, analysis3);
+ }
+
+ private void insertIssue(ComponentDto branch, RuleType ruleType, String status, @Nullable String resolution) {
+ RuleDefinitionDto rule = RuleTesting.newRule();
+ dbTester.rules().insert(rule);
+ dbTester.commit();
+ dbTester.issues().insert(rule, branch, branch, i -> i.setType(ruleType).setStatus(status).setResolution(resolution));
+ dbTester.commit();
+ }
+
+ private ComponentDto insertPrivateBranch(OrganizationDto organization) {
+ ComponentDto project = dbTester.components().insertPrivateProject(organization);
+ return dbTester.components().insertProjectBranch(project, branchDto -> branchDto
+ .setBranchType(BranchType.SHORT)
+ .setKey("foo"));
+ }
+
+ private SnapshotDto insertAnalysisTask(ComponentDto branch) {
+ return dbTester.components().insertSnapshot(branch);
+ }
+
+ private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFromSupplier(ComponentDto branch, SnapshotDto analysis) {
+ verify(webHooks).isEnabled(mockedConfiguration);
+ ArgumentCaptor<Supplier> supplierCaptor = ArgumentCaptor.forClass(Supplier.class);
+ verify(webHooks).sendProjectAnalysisUpdate(
+ same(mockedConfiguration),
+ eq(new WebHooks.Analysis(branch.uuid(), analysis.getUuid(), null)),
+ supplierCaptor.capture());
+
+ reset(webhookPayloadFactory);
+ supplierCaptor.getValue().get();
+ ArgumentCaptor<ProjectAnalysis> projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class);
+ verify(webhookPayloadFactory).create(projectAnalysisCaptor.capture());
+ return projectAnalysisCaptor.getValue();
+ }
+
+ private void indexIssues(ComponentDto branch) {
+ issueIndexer.indexOnAnalysis(branch.uuid());
+ }
+
+ @DataProvider
+ public static Object[][] validIssueChanges() {
+ return new Object[][] {
+ {new IssueChange(RuleType.BUG)},
+ {new IssueChange(RuleType.VULNERABILITY)},
+ {new IssueChange(RuleType.CODE_SMELL)},
+ {new IssueChange(DefaultTransitions.RESOLVE)},
+ {new IssueChange(RuleType.BUG, DefaultTransitions.RESOLVE)},
+ {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.RESOLVE)},
+ {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.RESOLVE)},
+ {new IssueChange(DefaultTransitions.FALSE_POSITIVE)},
+ {new IssueChange(RuleType.BUG, DefaultTransitions.FALSE_POSITIVE)},
+ {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.FALSE_POSITIVE)},
+ {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.FALSE_POSITIVE)},
+ {new IssueChange(DefaultTransitions.WONT_FIX)},
+ {new IssueChange(RuleType.BUG, DefaultTransitions.WONT_FIX)},
+ {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.WONT_FIX)},
+ {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.WONT_FIX)},
+ {new IssueChange(DefaultTransitions.REOPEN)},
+ {new IssueChange(RuleType.BUG, DefaultTransitions.REOPEN)},
+ {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.REOPEN)},
+ {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.REOPEN)}
+ };
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java
new file mode 100644
index 00000000000..dde3ac524ea
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.webhook;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.rules.RuleType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueChangeWebhookTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void IssueChange_constructor_throws_IAE_if_both_args_are_null() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("At least one of ruleType and transitionKey must be non null");
+
+ new IssueChangeWebhook.IssueChange(null, null);
+ }
+
+ @Test
+ public void verify_IssueChange_getters() {
+ IssueChangeWebhook.IssueChange transitionKeyOnly = new IssueChangeWebhookImpl.IssueChange("foo");
+ assertThat(transitionKeyOnly.getTransitionKey()).contains("foo");
+ assertThat(transitionKeyOnly.getRuleType()).isEmpty();
+ IssueChangeWebhook.IssueChange ruleTypeOnly = new IssueChangeWebhookImpl.IssueChange(RuleType.BUG);
+ assertThat(ruleTypeOnly.getTransitionKey()).isEmpty();
+ assertThat(ruleTypeOnly.getRuleType()).contains(RuleType.BUG);
+ IssueChangeWebhook.IssueChange transitionKeyAndRuleType = new IssueChangeWebhookImpl.IssueChange(RuleType.VULNERABILITY, "bar");
+ assertThat(transitionKeyAndRuleType.getTransitionKey()).contains("bar");
+ assertThat(transitionKeyAndRuleType.getRuleType()).contains(RuleType.VULNERABILITY);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
index 5433a6f5979..309b67b017a 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
@@ -50,6 +50,7 @@ import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.index.IssueIndexDefinition;
import org.sonar.server.issue.index.IssueIndexer;
import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.issue.webhook.IssueChangeWebhook;
import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.notification.NotificationManager;
@@ -108,8 +109,9 @@ public class DoTransitionActionTest {
private ComponentDto project;
private ComponentDto file;
private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
+ private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class);
- private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter);
+ private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, issueChangeWebhook);
private WsActionTester tester = new WsActionTester(underTest);
@Before
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
index a93c1f9e6f0..61b3b66e368 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
@@ -29,6 +29,6 @@ public class IssueWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new IssueWsModule().configure(container);
- assertThat(container.size()).isEqualTo(2 + 29);
+ assertThat(container.size()).isEqualTo(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
index 3f1e260e46e..4f786af8ae3 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
@@ -48,6 +48,7 @@ import org.sonar.server.issue.ServerIssueStorage;
import org.sonar.server.issue.index.IssueIndexDefinition;
import org.sonar.server.issue.index.IssueIndexer;
import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.issue.webhook.IssueChangeWebhook;
import org.sonar.server.notification.NotificationManager;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
@@ -92,10 +93,11 @@ public class SetTypeActionTest {
private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
+ private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class);
private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
new IssueUpdater(dbClient,
new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)),
- responseWriter));
+ responseWriter, issueChangeWebhook));
@Test
public void set_type() throws Exception {