diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-29 16:50:57 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-10-17 15:13:58 +0200 |
commit | df058fb1983f9ef60fa3c467c209db5c0f08050b (patch) | |
tree | 5716b3719e866116a7c7247b006ce356a58d84f1 /server | |
parent | ca2ea93cc15c20595896b5308aac2e2b320c4fd4 (diff) | |
download | sonarqube-df058fb1983f9ef60fa3c467c209db5c0f08050b.tar.gz sonarqube-df058fb1983f9ef60fa3c467c209db5c0f08050b.zip |
SONAR-9871 call webhook on single issue change on short lived branch
Diffstat (limited to 'server')
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 { |