From 013c10ada9d0e4328872914bb21749439cd7ae7f Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Thu, 16 Nov 2017 10:36:17 +0100 Subject: [PATCH] SONAR-10085 add Quality Gate Change event API and use it for webhook on issue changes --- .../sonar/db/component/ComponentDbTester.java | 11 + .../server/issue/ws/BulkChangeAction.java | 16 +- .../server/issue/ws/DoTransitionAction.java | 14 +- .../sonar/server/issue/ws/IssueWsModule.java | 8 +- .../sonar/server/issue/ws/SetTypeAction.java | 14 +- .../changeevent/IssueChangeTrigger.java} | 4 +- .../changeevent/IssueChangeTriggerImpl.java | 152 ++++ .../changeevent/QGChangeEvent.java | 91 ++ .../changeevent/QGChangeEventListener.java | 26 + .../changeevent/QGChangeEventListeners.java | 28 + .../QGChangeEventListenersImpl.java | 74 ++ .../qualitygate/changeevent/Trigger.java | 24 + .../changeevent}/package-info.java | 2 +- .../WebhookQGChangeEventListener.java} | 132 +-- .../webhook/IssueChangeWebhookImplTest.java | 835 ------------------ .../server/issue/ws/BulkChangeActionTest.java | 20 +- .../issue/ws/DoTransitionActionTest.java | 14 +- .../server/issue/ws/IssueWsModuleTest.java | 2 +- .../server/issue/ws/SetTypeActionTest.java | 14 +- .../IssueChangeTriggerImplTest.java | 606 +++++++++++++ .../changeevent/IssueChangeTriggerTest.java} | 56 +- .../WebhookQGChangeEventListenerTest.java | 482 ++++++++++ 22 files changed, 1605 insertions(+), 1020 deletions(-) rename server/sonar-server/src/main/java/org/sonar/server/{issue/webhook/IssueChangeWebhook.java => qualitygate/changeevent/IssueChangeTrigger.java} (97%) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java rename server/sonar-server/src/main/java/org/sonar/server/{issue/webhook => qualitygate/changeevent}/package-info.java (94%) rename server/sonar-server/src/main/java/org/sonar/server/{issue/webhook/IssueChangeWebhookImpl.java => webhook/WebhookQGChangeEventListener.java} (58%) delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java rename server/sonar-server/src/test/java/org/sonar/server/{issue/webhook/IssueChangeWebhookTest.java => qualitygate/changeevent/IssueChangeTriggerTest.java} (60%) create mode 100644 server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDbTester.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDbTester.java index ddd63f27043..cb0349e3868 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDbTester.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDbTester.java @@ -27,6 +27,7 @@ import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.organization.OrganizationDto; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static java.util.Arrays.asList; import static org.sonar.db.component.BranchType.LONG; @@ -280,6 +281,16 @@ public class ComponentDbTester { return branch; } + public final ComponentDto insertProjectBranch(ComponentDto project, BranchDto branchDto) { + // MainBranchProjectUuid will be null if it's a main branch + checkArgument(branchDto.getProjectUuid().equals(firstNonNull(project.getMainBranchProjectUuid(), project.projectUuid()))); + ComponentDto branch = newProjectBranch(project, branchDto); + insertComponent(branch); + dbClient.branchDao().insert(dbSession, branchDto); + db.commit(); + return branch; + } + private static T firstNonNull(@Nullable T first, T second) { return (first != null) ? first : second; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java index 60ad453ba28..032cc02cd50 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java @@ -59,8 +59,8 @@ import org.sonar.server.issue.RemoveTagsAction; import org.sonar.server.issue.SetTypeAction; import org.sonar.server.issue.TransitionAction; import org.sonar.server.issue.notification.IssueChangeNotification; -import org.sonar.server.issue.webhook.IssueChangeWebhook; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Issues; @@ -109,17 +109,17 @@ public class BulkChangeAction implements IssuesWsAction { private final IssueStorage issueStorage; private final NotificationManager notificationService; private final List actions; - private final IssueChangeWebhook issueChangeWebhook; + private final IssueChangeTrigger issueChangeTrigger; public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService, List actions, - IssueChangeWebhook issueChangeWebhook) { + IssueChangeTrigger issueChangeTrigger) { this.system2 = system2; this.userSession = userSession; this.dbClient = dbClient; this.issueStorage = issueStorage; this.notificationService = notificationService; this.actions = actions; - this.issueChangeWebhook = issueChangeWebhook; + this.issueChangeTrigger = issueChangeTrigger; } @Override @@ -206,8 +206,8 @@ public class BulkChangeAction implements IssuesWsAction { issueStorage.save(items); items.forEach(sendNotification(issueChangeContext, bulkChangeData)); buildWebhookIssueChange(bulkChangeData.propertiesByActions) - .ifPresent(issueChange -> issueChangeWebhook.onChange( - new IssueChangeWebhook.IssueChangeData( + .ifPresent(issueChange -> issueChangeTrigger.onChange( + new IssueChangeTrigger.IssueChangeData( bulkChangeData.issues.stream().filter(i -> result.success.contains(i.key())).collect(MoreCollectors.toList()), copyOf(bulkChangeData.componentsByUuid.values())), issueChange, @@ -216,7 +216,7 @@ public class BulkChangeAction implements IssuesWsAction { }; } - private static Optional buildWebhookIssueChange(Map> propertiesByActions) { + private static Optional buildWebhookIssueChange(Map> propertiesByActions) { RuleType ruleType = Optional.ofNullable(propertiesByActions.get(SetTypeAction.SET_TYPE_KEY)) .map(t -> (String) t.get(SetTypeAction.TYPE_PARAMETER)) .map(RuleType::valueOf) @@ -227,7 +227,7 @@ public class BulkChangeAction implements IssuesWsAction { if (ruleType == null && transitionKey == null) { return Optional.empty(); } - return Optional.of(new IssueChangeWebhook.IssueChange(ruleType, transitionKey)); + return Optional.of(new IssueChangeTrigger.IssueChange(ruleType, transitionKey)); } private static Predicate bulkChange(IssueChangeContext issueChangeContext, BulkChangeData bulkChangeData, BulkChangeResult result) { 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 201ca3d98e5..43a0d0056a7 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 @@ -37,7 +37,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.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.user.UserSession; import static com.google.common.collect.ImmutableList.copyOf; @@ -54,10 +54,10 @@ public class DoTransitionAction implements IssuesWsAction { private final TransitionService transitionService; private final OperationResponseWriter responseWriter; private final System2 system2; - private final IssueChangeWebhook issueChangeWebhook; + private final IssueChangeTrigger issueChangeTrigger; public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService, - OperationResponseWriter responseWriter, System2 system2, IssueChangeWebhook issueChangeWebhook) { + OperationResponseWriter responseWriter, System2 system2, IssueChangeTrigger issueChangeTrigger) { this.dbClient = dbClient; this.userSession = userSession; this.issueFinder = issueFinder; @@ -65,7 +65,7 @@ public class DoTransitionAction implements IssuesWsAction { this.transitionService = transitionService; this.responseWriter = responseWriter; this.system2 = system2; - this.issueChangeWebhook = issueChangeWebhook; + this.issueChangeTrigger = issueChangeTrigger; } @Override @@ -108,11 +108,11 @@ public class DoTransitionAction implements IssuesWsAction { transitionService.checkTransitionPermission(transitionKey, defaultIssue); if (transitionService.doTransition(defaultIssue, context, transitionKey)) { SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null); - issueChangeWebhook.onChange( - new IssueChangeWebhook.IssueChangeData( + issueChangeTrigger.onChange( + new IssueChangeTrigger.IssueChangeData( searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())), copyOf(searchResponseData.getComponents())), - new IssueChangeWebhook.IssueChange(transitionKey), + new IssueChangeTrigger.IssueChange(transitionKey), context); return searchResponseData; } 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 6deec524da2..2c750380e4d 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 @@ -26,10 +26,12 @@ 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.qualitygate.changeevent.IssueChangeTriggerImpl; +import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl; import org.sonar.server.settings.ProjectConfigurationLoaderImpl; +import org.sonar.server.webhook.WebhookQGChangeEventListener; import org.sonar.server.ws.WsResponseCommonFormat; public class IssueWsModule extends Module { @@ -65,6 +67,8 @@ public class IssueWsModule extends Module { ChangelogAction.class, BulkChangeAction.class, ProjectConfigurationLoaderImpl.class, - IssueChangeWebhookImpl.class); + IssueChangeTriggerImpl.class, + WebhookQGChangeEventListener.class, + QGChangeEventListenersImpl.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 6554cdac0c8..e30f64e749a 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 @@ -37,7 +37,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.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.user.UserSession; import static com.google.common.collect.ImmutableList.copyOf; @@ -55,10 +55,10 @@ public class SetTypeAction implements IssuesWsAction { private final IssueUpdater issueUpdater; private final OperationResponseWriter responseWriter; private final System2 system2; - private final IssueChangeWebhook issueChangeWebhook; + private final IssueChangeTrigger issueChangeTrigger; public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, - OperationResponseWriter responseWriter, System2 system2, IssueChangeWebhook issueChangeWebhook) { + OperationResponseWriter responseWriter, System2 system2, IssueChangeTrigger issueChangeTrigger) { this.userSession = userSession; this.dbClient = dbClient; this.issueFinder = issueFinder; @@ -66,7 +66,7 @@ public class SetTypeAction implements IssuesWsAction { this.issueUpdater = issueUpdater; this.responseWriter = responseWriter; this.system2 = system2; - this.issueChangeWebhook = issueChangeWebhook; + this.issueChangeTrigger = issueChangeTrigger; } @Override @@ -116,11 +116,11 @@ public class SetTypeAction implements IssuesWsAction { IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); if (issueFieldsSetter.setType(issue, ruleType, context)) { SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null); - issueChangeWebhook.onChange( - new IssueChangeWebhook.IssueChangeData( + issueChangeTrigger.onChange( + new IssueChangeTrigger.IssueChangeData( searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())), copyOf(searchResponseData.getComponents())), - new IssueChangeWebhook.IssueChange(ruleType), + new IssueChangeTrigger.IssueChange(ruleType), context); return searchResponseData; } 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/qualitygate/changeevent/IssueChangeTrigger.java similarity index 97% rename from server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java rename to server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTrigger.java index 6d509716194..4fe0b86c1b3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTrigger.java @@ -17,7 +17,7 @@ * 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; +package org.sonar.server.qualitygate.changeevent; import com.google.common.collect.ImmutableList; import java.util.List; @@ -32,7 +32,7 @@ import org.sonar.server.issue.ws.SearchResponseData; import static com.google.common.base.Preconditions.checkArgument; -public interface IssueChangeWebhook { +public interface IssueChangeTrigger { /** * 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. diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java new file mode 100644 index 00000000000..25af68798c9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java @@ -0,0 +1,152 @@ +/* + * 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.qualitygate.changeevent; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.api.config.Configuration; +import org.sonar.api.issue.DefaultTransitions; +import org.sonar.core.issue.DefaultIssue; +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.server.settings.ProjectConfigurationLoader; + +import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; + +public class IssueChangeTriggerImpl implements IssueChangeTrigger { + private static final Set MEANINGFUL_TRANSITIONS = ImmutableSet.of( + DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN); + private final DbClient dbClient; + private final ProjectConfigurationLoader projectConfigurationLoader; + private final QGChangeEventListeners qgEventListeners; + + public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader, QGChangeEventListeners qgEventListeners) { + this.dbClient = dbClient; + this.projectConfigurationLoader = projectConfigurationLoader; + this.qgEventListeners = qgEventListeners; + } + + @Override + public void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) { + if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange) || qgEventListeners.isEmpty()) { + return; + } + + callWebHook(issueChangeData); + } + + private static boolean isRelevant(IssueChange issueChange) { + return issueChange.getTransitionKey().map(IssueChangeTriggerImpl::isMeaningfulTransition).orElse(true); + } + + private static boolean isEmpty(IssueChangeData issueChangeData) { + return issueChangeData.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(IssueChangeData issueChangeData) { + try (DbSession dbSession = dbClient.openSession(false)) { + Map branchesByUuid = getBranchComponents(dbSession, issueChangeData); + if (branchesByUuid.isEmpty()) { + return; + } + + Set branchProjectUuids = branchesByUuid.values().stream() + .map(ComponentDto::uuid) + .collect(toSet(branchesByUuid.size())); + Set shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids) + .stream() + .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT) + .collect(toSet(branchesByUuid.size())); + if (shortBranches.isEmpty()) { + return; + } + + Map configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession, + shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet())); + Set shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size())); + Map analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids( + dbSession, + shortBranchesComponentUuids) + .stream() + .collect(uniqueIndex(SnapshotDto::getComponentUuid)); + + List qgChangeEvents = shortBranches + .stream() + .map(shortBranch -> { + ComponentDto branch = branchesByUuid.get(shortBranch.getUuid()); + SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid()); + if (branch != null && analysis != null) { + Configuration configuration = configurationByUuid.get(shortBranch.getUuid()); + + return new QGChangeEvent(branch, shortBranch, analysis, configuration); + } + return null; + }) + .filter(Objects::nonNull) + .collect(MoreCollectors.toList(shortBranches.size())); + qgEventListeners.broadcast(Trigger.ISSUE_CHANGE, qgChangeEvents); + } + } + + private Map getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) { + Set projectUuids = issueChangeData.getIssues().stream() + .map(DefaultIssue::projectUuid) + .collect(toSet()); + Set missingProjectUuids = ImmutableSet.copyOf(Sets.difference( + projectUuids, + issueChangeData.getComponents() + .stream() + .map(ComponentDto::uuid) + .collect(Collectors.toSet()))); + if (missingProjectUuids.isEmpty()) { + return issueChangeData.getComponents() + .stream() + .filter(c -> projectUuids.contains(c.uuid())) + .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) + .collect(uniqueIndex(ComponentDto::uuid)); + } + return Stream.concat( + issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())), + dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream()) + .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) + .collect(uniqueIndex(ComponentDto::uuid)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java new file mode 100644 index 00000000000..a3d82c69856 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java @@ -0,0 +1,91 @@ +/* + * 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.qualitygate.changeevent; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.config.Configuration; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; + +public class QGChangeEvent { + private final ComponentDto project; + private final BranchDto branch; + private final SnapshotDto analysis; + private final Configuration projectConfiguration; + + public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration) { + this.branch = branch; + this.project = project; + this.analysis = analysis; + this.projectConfiguration = projectConfiguration; + } + + public BranchDto getBranch() { + return branch; + } + + public ComponentDto getProject() { + return project; + } + + public SnapshotDto getAnalysis() { + return analysis; + } + + public Configuration getProjectConfiguration() { + return projectConfiguration; + } + + @Override + public String toString() { + return "QGChangeEvent{" + + "branch=" + toString(branch) + + ", project=" + toString(project) + + ", analysis=" + toString(analysis) + + ", projectConfiguration=" + projectConfiguration + + '}'; + } + + @CheckForNull + private static String toString(@Nullable BranchDto shortBranch) { + if (shortBranch == null) { + return null; + } + return shortBranch.getBranchType() + ":" + shortBranch.getUuid() + ":" + shortBranch.getProjectUuid() + ":" + shortBranch.getMergeBranchUuid(); + } + + @CheckForNull + private static String toString(@Nullable ComponentDto shortBranchComponent) { + if (shortBranchComponent == null) { + return null; + } + return shortBranchComponent.uuid() + ":" + shortBranchComponent.getKey(); + } + + @CheckForNull + private static String toString(@Nullable SnapshotDto latestAnalysis) { + if (latestAnalysis == null) { + return null; + } + return latestAnalysis.getUuid() + ":" + latestAnalysis.getCreatedAt(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java new file mode 100644 index 00000000000..89276021c74 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java @@ -0,0 +1,26 @@ +/* + * 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.qualitygate.changeevent; + +import java.util.Collection; + +public interface QGChangeEventListener { + void onChanges(Trigger trigger, Collection changeEvents); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java new file mode 100644 index 00000000000..50065d9bb26 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java @@ -0,0 +1,28 @@ +/* + * 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.qualitygate.changeevent; + +import java.util.Collection; + +public interface QGChangeEventListeners { + boolean isEmpty(); + + void broadcast(Trigger trigger, Collection changeEvents); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java new file mode 100644 index 00000000000..1c1d3d8750d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java @@ -0,0 +1,74 @@ +/* + * 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.qualitygate.changeevent; + +import java.util.Arrays; +import java.util.Collection; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; + +/** + * Broadcast a given collection of {@link QGChangeEvent} for a specific trigger to all the registered + * {@link QGChangeEventListener} in Pico. + * + * This class ensures that an {@link Exception} occurring calling one of the {@link QGChangeEventListener} doesn't + * prevent from calling the others. + */ +public class QGChangeEventListenersImpl implements QGChangeEventListeners { + private static final Logger LOG = Loggers.get(QGChangeEventListenersImpl.class); + + private final QGChangeEventListener[] listeners; + + /** + * Used by Pico when there is no QGChangeEventListener instance in container. + */ + public QGChangeEventListenersImpl() { + this.listeners = new QGChangeEventListener[0]; + } + + public QGChangeEventListenersImpl(QGChangeEventListener[] listeners) { + this.listeners = listeners; + } + + @Override + public boolean isEmpty() { + return listeners.length == 0; + } + + @Override + public void broadcast(Trigger trigger, Collection changeEvents) { + try { + Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, changeEvents, listener)); + } catch (Error e) { + LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e); + } + } + + private void broadcastTo(Trigger trigger, Collection changeEvents, QGChangeEventListener listener) { + try { + LOG.debug("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvents); + listener.onChanges(trigger, changeEvents); + } catch (Exception e) { + LOG.warn(format("onChange() call failed on listener %s for events %s", listener.getClass().getName(), changeEvents), e); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java new file mode 100644 index 00000000000..6a57448c523 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java @@ -0,0 +1,24 @@ +/* + * 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.qualitygate.changeevent; + +public enum Trigger { + ISSUE_CHANGE +} 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/qualitygate/changeevent/package-info.java similarity index 94% rename from server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java rename to server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java index 9f828ad5e5e..ac361c62ef8 100644 --- 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/qualitygate/changeevent/package-info.java @@ -18,6 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault -package org.sonar.server.issue.webhook; +package org.sonar.server.qualitygate.changeevent; import javax.annotation.ParametersAreNonnullByDefault; 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/webhook/WebhookQGChangeEventListener.java similarity index 58% rename from server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java rename to server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java index 698dae3bf2a..4f3c6baf8fd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java @@ -17,28 +17,23 @@ * 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; +package org.sonar.server.webhook; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; +import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; 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.api.utils.System2; -import org.sonar.core.issue.DefaultIssue; -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.AnalysisPropertyDto; 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.qualitygate.QualityGateConditionDto; @@ -51,6 +46,9 @@ import org.sonar.server.qualitygate.EvaluatedCondition; import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; import org.sonar.server.qualitygate.EvaluatedQualityGate; import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; +import org.sonar.server.qualitygate.changeevent.QGChangeEvent; +import org.sonar.server.qualitygate.changeevent.QGChangeEventListener; +import org.sonar.server.qualitygate.changeevent.Trigger; import org.sonar.server.rule.index.RuleIndex; import org.sonar.server.settings.ProjectConfigurationLoader; import org.sonar.server.webhook.Analysis; @@ -65,99 +63,47 @@ 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 MEANINGFUL_TRANSITIONS = ImmutableSet.of( - DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN); - private final DbClient dbClient; +public class WebhookQGChangeEventListener implements QGChangeEventListener { private final WebHooks webhooks; - private final ProjectConfigurationLoader projectConfigurationLoader; private final WebhookPayloadFactory webhookPayloadFactory; private final IssueIndex issueIndex; + private final DbClient dbClient; private final System2 system2; - public IssueChangeWebhookImpl(DbClient dbClient, WebHooks webhooks, ProjectConfigurationLoader projectConfigurationLoader, - WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex, System2 system2) { - this.dbClient = dbClient; + public WebhookQGChangeEventListener(WebHooks webhooks, WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex, DbClient dbClient, System2 system2) { this.webhooks = webhooks; - this.projectConfigurationLoader = projectConfigurationLoader; this.webhookPayloadFactory = webhookPayloadFactory; this.issueIndex = issueIndex; + this.dbClient = dbClient; this.system2 = system2; } @Override - public void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) { - if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange)) { + public void onChanges(Trigger trigger, Collection changeEvents) { + if (changeEvents.isEmpty()) { return; } - callWebHook(issueChangeData); - } - - private static boolean isRelevant(IssueChange issueChange) { - return issueChange.getTransitionKey().map(IssueChangeWebhookImpl::isMeaningfulTransition).orElse(true); - } - - private static boolean isEmpty(IssueChangeData issueChangeData) { - return issueChangeData.getIssues().isEmpty(); - } - - private static boolean isUserChangeContext(IssueChangeContext context) { - return context.login() != null; - } - - private static boolean isMeaningfulTransition(String transitionKey) { - return MEANINGFUL_TRANSITIONS.contains(transitionKey); - } + List branchesWithWebhooks = changeEvents.stream() + .filter(changeEvent -> webhooks.isEnabled(changeEvent.getProjectConfiguration())) + .collect(MoreCollectors.toList()); + if (branchesWithWebhooks.isEmpty()) { + return; + } - private void callWebHook(IssueChangeData issueChangeData) { try (DbSession dbSession = dbClient.openSession(false)) { - Map branchesByUuid = getBranchComponents(dbSession, issueChangeData); - if (branchesByUuid.isEmpty()) { - return; - } - - Set branchProjectUuids = branchesByUuid.values().stream() - .map(ComponentDto::uuid) - .collect(toSet(branchesByUuid.size())); - Set shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids) - .stream() - .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT) - .collect(toSet(branchesByUuid.size())); - if (shortBranches.isEmpty()) { - return; - } - - Map configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession, - shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet())); - Set branchesWithWebhooks = shortBranches.stream() - .filter(shortBranch -> webhooks.isEnabled(configurationByUuid.get(shortBranch.getUuid()))) - .collect(toSet()); - if (branchesWithWebhooks.isEmpty()) { - return; - } - - Map analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids( - dbSession, - branchesWithWebhooks.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size()))) - .stream() - .collect(uniqueIndex(SnapshotDto::getComponentUuid)); - branchesWithWebhooks - .forEach(shortBranch -> { - ComponentDto branch = branchesByUuid.get(shortBranch.getUuid()); - SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid()); - if (branch != null && analysis != null) { - webhooks.sendProjectAnalysisUpdate( - configurationByUuid.get(shortBranch.getUuid()), - new WebHooks.Analysis(shortBranch.getUuid(), analysis.getUuid(), null), - () -> buildWebHookPayload(dbSession, branch, shortBranch, analysis)); - } - }); + branchesWithWebhooks.forEach(event -> callWebhook(dbSession, event)); } } + private void callWebhook(DbSession dbSession, QGChangeEvent event) { + webhooks.sendProjectAnalysisUpdate( + event.getProjectConfiguration(), + new WebHooks.Analysis(event.getBranch().getUuid(), event.getAnalysis().getUuid(), null), + () -> buildWebHookPayload(dbSession, event.getProject(), event.getBranch(), event.getAnalysis())); + } + private WebhookPayload buildWebHookPayload(DbSession dbSession, ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) { Map analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid()) .stream() @@ -250,28 +196,4 @@ public class IssueChangeWebhookImpl implements IssueChangeWebhook { } return res; } - - private Map getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) { - Set projectUuids = issueChangeData.getIssues().stream() - .map(DefaultIssue::projectUuid) - .collect(toSet()); - Set missingProjectUuids = ImmutableSet.copyOf(Sets.difference( - projectUuids, - issueChangeData.getComponents() - .stream() - .map(ComponentDto::uuid) - .collect(Collectors.toSet()))); - if (missingProjectUuids.isEmpty()) { - return issueChangeData.getComponents() - .stream() - .filter(c -> projectUuids.contains(c.uuid())) - .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) - .collect(uniqueIndex(ComponentDto::uuid)); - } - return Stream.concat( - issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())), - dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream()) - .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) - .collect(uniqueIndex(ComponentDto::uuid)); - } } 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 deleted file mode 100644 index 94c25b67a3b..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java +++ /dev/null @@ -1,835 +0,0 @@ -/* - * 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.ImmutableMap; -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.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Random; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.assertj.core.groups.Tuple; -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.rules.RuleType; -import org.sonar.api.utils.System2; -import org.sonar.core.issue.IssueChangeContext; -import org.sonar.core.util.UuidFactoryFast; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.AnalysisPropertyDto; -import org.sonar.db.component.BranchDao; -import org.sonar.db.component.BranchType; -import org.sonar.db.component.ComponentDao; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.component.SnapshotDao; -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.permission.index.AuthorizationTypeSupport; -import org.sonar.server.qualitygate.Condition; -import org.sonar.server.qualitygate.EvaluatedCondition; -import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; -import org.sonar.server.qualitygate.EvaluatedQualityGate; -import org.sonar.server.qualitygate.QualityGate; -import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; -import org.sonar.server.settings.ProjectConfigurationLoader; -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.WebHooks; -import org.sonar.server.webhook.WebhookPayload; -import org.sonar.server.webhook.WebhookPayloadFactory; - -import static com.google.common.base.Preconditions.checkArgument; -import static java.lang.String.valueOf; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; -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.any; -import static org.mockito.Matchers.anyCollectionOf; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -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.core.util.stream.MoreCollectors.toArrayList; -import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN; - -@RunWith(DataProviderRunner.class) -public class IssueChangeWebhookImplTest { - private static final List OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED); - private static final List 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 DbClient spiedOnDbClient = spy(dbClient); - private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class); - private IssueChangeWebhookImpl underTest = new IssueChangeWebhookImpl(spiedOnDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, issueIndex, System2.INSTANCE); - private DbClient mockedDbClient = mock(DbClient.class); - private IssueIndex spiedOnIssueIndex = spy(issueIndex); - private IssueChangeWebhookImpl mockedUnderTest = new IssueChangeWebhookImpl(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex, - System2.INSTANCE); - - @Test - public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), userChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex); - } - - @Test - public void on_type_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), scanChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex); - } - - @Test - public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomAlphanumeric(12)), userChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, 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 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, projectConfigurationLoader, webhookPayloadFactory); - - mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(transitionKey), userChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex); - } - - @Test - public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(randomAlphanumeric(12)), scanChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex); - } - - @Test - public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex); - } - - @Test - public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, 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 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, projectConfigurationLoader, webhookPayloadFactory); - - mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(randomRuleType, transitionKey), userChangeContext); - - verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, 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); - Configuration configuration = mockLoadProjectConfiguration(branch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - Map properties = new HashMap<>(); - properties.put("sonar.analysis.test1", randomAlphanumeric(50)); - properties.put("sonar.analysis.test2", randomAlphanumeric(5000)); - insertPropertiesFor(analysis.getUuid(), properties); - - underTest.onChange(issueChangeData(newIssueDto(branch)), issueChange, userChangeContext); - - ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); - Condition condition1 = new Condition(BUGS_KEY, GREATER_THAN, "0", null, false); - Condition condition2 = new Condition(VULNERABILITIES_KEY, GREATER_THAN, "0", null, false); - Condition condition3 = new Condition(CODE_SMELLS_KEY, GREATER_THAN, "0", null, false); - 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), - EvaluatedQualityGate.newBuilder() - .setQualityGate(new QualityGate( - valueOf(ShortLivingBranchQualityGate.ID), - ShortLivingBranchQualityGate.NAME, - ImmutableSet.of(condition1, condition2, condition3))) - .setStatus(EvaluatedQualityGate.Status.OK) - .addCondition(condition1, EvaluationStatus.OK, "0") - .addCondition(condition2, EvaluationStatus.OK, "0") - .addCondition(condition3, EvaluationStatus.OK, "0") - .build(), - null, - properties)); - } - - @Test - @UseDataProvider("validIssueChanges") - public void do_not_retrieve_analysis_nor_call_webhook_if_webhook_are_disabled_for_short_branch(IssueChange issueChange) { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPublicProject(organization); - ComponentDto branch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.SHORT) - .setKey("foo")); - ComponentDto branch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.SHORT) - .setKey("bar")); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1, configuration1, - branch2, configuration2); - mockWebhookDisabled(configuration1); - mockWebhookEnabled(configuration2); - mockPayloadSupplierConsumedByWebhooks(); - ImmutableList issueDtos = ImmutableList.of(newIssueDto(branch1), newIssueDto(branch2)); - - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange(issueChangeData(issueDtos), issueChange, userChangeContext); - - verifyWebhookCalled(branch2, configuration2, analysis2); - verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(singleton(branch2.uuid()))); - } - - @Test - @UseDataProvider("validIssueChanges") - public void do_not_load_project_configuration_nor_analysis_nor_call_webhook_if_there_are_no_short_branch(IssueChange issueChange) { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPublicProject(organization); - ComponentDto longBranch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.LONG) - .setKey("foo")); - ComponentDto longBranch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.LONG) - .setKey("bar")); - ImmutableList issueDtos = ImmutableList.of(newIssueDto(project), newIssueDto(longBranch1), newIssueDto(longBranch2)); - - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); - - verifyZeroInteractions(projectConfigurationLoader, webHooks); - verify(snapshotDaoSpy, times(0)).selectLastAnalysesByRootComponentUuids(any(DbSession.class), anyCollectionOf(String.class)); - } - - @Test - @UseDataProvider("validIssueChanges") - public void do_not_load_analysis_nor_call_webhook_if_there_no_short_branch_with_enabled_webhook(IssueChange issueChange) { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPublicProject(organization); - ComponentDto branch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.SHORT) - .setKey("foo")); - ComponentDto branch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.SHORT) - .setKey("bar")); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1, configuration1, - branch2, configuration2); - mockWebhookDisabled(configuration1, configuration2); - mockPayloadSupplierConsumedByWebhooks(); - ImmutableList issueDtos = ImmutableList.of(newIssueDto(branch1), newIssueDto(branch2)); - - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange(issueChangeData(issueDtos), issueChange, userChangeContext); - - verify(webHooks).isEnabled(configuration1); - verify(webHooks).isEnabled(configuration2); - verify(webHooks, times(0)).sendProjectAnalysisUpdate(any(), any(), any()); - verify(snapshotDaoSpy, times(0)).selectLastAnalysesByRootComponentUuids(any(DbSession.class), anyCollectionOf(String.class)); - } - - @Test - public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto branch = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis = insertAnalysisTask(branch); - Configuration configuration = mockLoadProjectConfiguration(branch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext); - - ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); - EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::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, BranchType.SHORT); - 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); - Configuration configuration = mockLoadProjectConfiguration(branch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext); - - ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); - EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue) - .containsOnly(expectedQGConditions); - } - - @Test - public void computes_QG_error_with_all_failing_conditions() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto branch = insertPrivateBranch(organization, BranchType.SHORT); - 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); - Configuration configuration = mockLoadProjectConfiguration(branch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext); - - ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); - EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::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_short_branch_with_at_least_one_issue_in_SearchResponseData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT); - 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 issueDtos = Stream.of( - IntStream.range(0, issuesBranch1).mapToObj(i -> newIssueDto(branch1)), - IntStream.range(0, issuesBranch2).mapToObj(i -> newIssueDto(branch2)), - IntStream.range(0, issuesBranch3).mapToObj(i -> newIssueDto(branch3))) - .flatMap(s -> s) - .collect(MoreCollectors.toList()); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations(branch1, configuration1, - branch2, configuration2, - branch3, configuration3); - mockWebhookEnabled(configuration1, configuration2, configuration3); - mockPayloadSupplierConsumedByWebhooks(); - - ComponentDao componentDaoSpy = spy(dbClient.componentDao()); - BranchDao branchDaoSpy = spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); - - verifyWebhookCalled(branch1, configuration1, analysis1); - verifyWebhookCalled(branch2, configuration2, analysis2); - verifyWebhookCalled(branch3, configuration3, analysis3); - extractPayloadFactoryArguments(3); - - Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); - verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(uuids)); - verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void call_webhood_only_for_short_branches() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto shortBranch = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto longBranch = insertPrivateBranch(organization, BranchType.LONG); - SnapshotDto analysis1 = insertAnalysisTask(shortBranch); - SnapshotDto analysis2 = insertAnalysisTask(longBranch); - Configuration configuration = mockLoadProjectConfiguration(shortBranch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - ComponentDao componentDaoSpy = spy(dbClient.componentDao()); - BranchDao branchDaoSpy = spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange( - issueChangeData(asList(newIssueDto(shortBranch), newIssueDto(longBranch))), - new IssueChange(randomRuleType), - userChangeContext); - - verifyWebhookCalledAndExtractPayloadFactoryArgument(shortBranch, configuration, analysis1); - - Set uuids = ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()); - verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid()))); - verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void do_not_load_componentDto_from_DB_if_all_are_in_inputData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis1 = insertAnalysisTask(branch1); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - SnapshotDto analysis3 = insertAnalysisTask(branch3); - List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1, configuration1, - branch2, configuration2, - branch3, configuration3); - mockWebhookEnabled(configuration1, configuration2, configuration3); - mockPayloadSupplierConsumedByWebhooks(); - - ComponentDao componentDaoSpy = spy(dbClient.componentDao()); - when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - underTest.onChange( - issueChangeData(issueDtos, branch1, branch2, branch3), - new IssueChange(randomRuleType), - userChangeContext); - - verifyWebhookCalled(branch1, configuration1, analysis1); - verifyWebhookCalled(branch2, configuration2, analysis2); - verifyWebhookCalled(branch3, configuration3, analysis3); - - verify(componentDaoSpy, times(0)).selectByUuids(any(DbSession.class), anyCollectionOf(String.class)); - verifyNoMoreInteractions(componentDaoSpy); - } - - @Test - public void call_db_only_for_componentDto_not_in_inputData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis1 = insertAnalysisTask(branch1); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - SnapshotDto analysis3 = insertAnalysisTask(branch3); - List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1, configuration1, - branch2, configuration2, - branch3, configuration3); - mockWebhookEnabled(configuration1, configuration2, configuration3); - mockPayloadSupplierConsumedByWebhooks(); - - ComponentDao componentDaoSpy = spy(dbClient.componentDao()); - BranchDao branchDaoSpy = spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange( - issueChangeData(issueDtos, branch1, branch3), - new IssueChange(randomRuleType), - userChangeContext); - - verifyWebhookCalled(branch1, configuration1, analysis1); - verifyWebhookCalled(branch2, configuration2, analysis2); - verifyWebhookCalled(branch3, configuration3, analysis3); - extractPayloadFactoryArguments(3); - - Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); - verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(ImmutableSet.of(branch2.uuid()))); - verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(uuids)); - verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void supports_issues_on_files_and_filter_on_short_branches_asap_when_calling_db() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPrivateProject(organization); - ComponentDto projectFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); - ComponentDto shortBranch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.SHORT) - .setKey("foo")); - ComponentDto longBranch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(BranchType.LONG) - .setKey("bar")); - ComponentDto shortBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(shortBranch)); - ComponentDto longBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(longBranch)); - SnapshotDto analysis1 = insertAnalysisTask(project); - SnapshotDto analysis2 = insertAnalysisTask(shortBranch); - SnapshotDto analysis3 = insertAnalysisTask(longBranch); - List issueDtos = asList( - newIssueDto(projectFile, project), - newIssueDto(shortBranchFile, shortBranch), - newIssueDto(longBranchFile, longBranch)); - Configuration configuration = mockLoadProjectConfiguration(shortBranch); - mockWebhookEnabled(configuration); - mockPayloadSupplierConsumedByWebhooks(); - - ComponentDao componentDaoSpy = spy(dbClient.componentDao()); - BranchDao branchDaoSpy = spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao()); - when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); - - verifyWebhookCalledAndExtractPayloadFactoryArgument(shortBranch, configuration, analysis2); - - Set uuids = ImmutableSet.of(project.uuid(), shortBranch.uuid(), longBranch.uuid()); - verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids)); - verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()))); - verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid()))); - verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - private void mockWebhookEnabled(Configuration... configurations) { - for (Configuration configuration : configurations) { - when(webHooks.isEnabled(configuration)).thenReturn(true); - } - } - - private void mockWebhookDisabled(Configuration... configurations) { - for (Configuration configuration : configurations) { - when(webHooks.isEnabled(configuration)).thenReturn(false); - } - } - - private Configuration mockLoadProjectConfiguration(ComponentDto shortBranch) { - Configuration configuration = mock(Configuration.class); - when(projectConfigurationLoader.loadProjectConfigurations(any(DbSession.class), eq(singleton(shortBranch)))) - .thenReturn(ImmutableMap.of(shortBranch.uuid(), configuration)); - return configuration; - } - - private void mockLoadProjectConfigurations(Object... branchesAndConfiguration) { - checkArgument(branchesAndConfiguration.length % 2 == 0); - Set components = new HashSet<>(); - Map result = new HashMap<>(); - for (int i = 0; i < branchesAndConfiguration.length; i++) { - ComponentDto component = (ComponentDto) branchesAndConfiguration[i++]; - Configuration configuration = (Configuration) branchesAndConfiguration[i]; - components.add(component); - result.put(component.uuid(), configuration); - } - when(projectConfigurationLoader.loadProjectConfigurations(any(DbSession.class), eq(components))) - .thenReturn(result); - } - - private void mockPayloadSupplierConsumedByWebhooks() { - doAnswer(invocationOnMock -> { - Supplier supplier = (Supplier) invocationOnMock.getArguments()[2]; - supplier.get(); - return null; - }).when(webHooks).sendProjectAnalysisUpdate(any(Configuration.class), any(), any()); - } - - 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, BranchType branchType) { - ComponentDto project = dbTester.components().insertPrivateProject(organization); - return dbTester.components().insertProjectBranch(project, branchDto -> branchDto - .setBranchType(branchType) - .setKey("foo")); - } - - private void insertPropertiesFor(String snapshotUuid, Map properties) { - List analysisProperties = properties.entrySet().stream() - .map(entry -> new AnalysisPropertyDto() - .setUuid(UuidFactoryFast.getInstance().create()) - .setSnapshotUuid(snapshotUuid) - .setKey(entry.getKey()) - .setValue(entry.getValue())) - .collect(toArrayList(properties.size())); - dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties); - dbTester.getSession().commit(); - } - - private SnapshotDto insertAnalysisTask(ComponentDto branch) { - return dbTester.components().insertSnapshot(branch); - } - - private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ComponentDto branch, Configuration configuration, SnapshotDto analysis) { - verifyWebhookCalled(branch, configuration, analysis); - - return extractPayloadFactoryArguments(1).iterator().next(); - } - - private void verifyWebhookCalled(ComponentDto branch, Configuration branchConfiguration, SnapshotDto analysis) { - verify(webHooks).isEnabled(branchConfiguration); - ArgumentCaptor supplierCaptor = ArgumentCaptor.forClass(Supplier.class); - verify(webHooks).sendProjectAnalysisUpdate( - same(branchConfiguration), - eq(new WebHooks.Analysis(branch.uuid(), analysis.getUuid(), null)), - supplierCaptor.capture()); - } - - private List extractPayloadFactoryArguments(int time) { - ArgumentCaptor projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class); - verify(webhookPayloadFactory, times(time)).create(projectAnalysisCaptor.capture()); - return projectAnalysisCaptor.getAllValues(); - } - - 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)} - }; - } - - private IssueChangeWebhook.IssueChangeData issueChangeData() { - return new IssueChangeWebhook.IssueChangeData(emptyList(), emptyList()); - } - - private IssueChangeWebhook.IssueChangeData issueChangeData(IssueDto issueDto) { - return new IssueChangeWebhook.IssueChangeData(singletonList(issueDto.toDefaultIssue()), emptyList()); - } - - private IssueChangeWebhook.IssueChangeData issueChangeData(Collection issueDtos, ComponentDto... components) { - return new IssueChangeWebhook.IssueChangeData( - issueDtos.stream().map(IssueDto::toDefaultIssue).collect(Collectors.toList()), - Arrays.stream(components).collect(Collectors.toList())); - } - - private IssueDto newIssueDto(@Nullable ComponentDto project) { - return newIssueDto(project, project); - } - - private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentDto project) { - RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)]; - String randomStatus = Issue.STATUSES.get(random.nextInt(Issue.STATUSES.size())); - IssueDto res = new IssueDto() - .setType(randomRuleType) - .setStatus(randomStatus) - .setRuleKey(randomAlphanumeric(3), randomAlphanumeric(4)); - if (component != null) { - res.setComponent(component); - } - if (project != null) { - res.setProject(project); - } - return res; - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java index 6efc13e9f2b..a88c1f70004 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java @@ -56,12 +56,12 @@ 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.notification.IssueChangeNotification; -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; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; +import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -117,7 +117,7 @@ public class BulkChangeActionTest { private IssueStorage issueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient))); private NotificationManager notificationManager = mock(NotificationManager.class); - private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class); + private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.class); private List actions = new ArrayList<>(); private RuleDto rule; @@ -126,7 +126,7 @@ public class BulkChangeActionTest { private ComponentDto file; private UserDto user; - private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangeWebhook)); + private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangeTrigger)); @Before public void setUp() throws Exception { @@ -173,7 +173,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getSeverity()).isEqualTo(MINOR); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(issueChangeWebhook); + verifyZeroInteractions(issueChangeTrigger); } @Test @@ -191,7 +191,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getTags()).containsOnly("tag1", "tag2", "tag3"); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(issueChangeWebhook); + verifyZeroInteractions(issueChangeTrigger); } @Test @@ -209,7 +209,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getAssignee()).isNull(); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(issueChangeWebhook); + verifyZeroInteractions(issueChangeTrigger); } @Test @@ -530,12 +530,12 @@ public class BulkChangeActionTest { private void verifyIssueChangeWebhookCalled(@Nullable RuleType expectedRuleType, @Nullable String transitionKey, String[] componentUUids, IssueDto... issueDtos) { - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class); - verify(issueChangeWebhook).onChange( + ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class); + verify(issueChangeTrigger).onChange( issueChangeDataCaptor.capture(), - eq(new IssueChangeWebhook.IssueChange(expectedRuleType, transitionKey)), + eq(new IssueChangeTrigger.IssueChange(expectedRuleType, transitionKey)), eq(IssueChangeContext.createUser(new Date(NOW), userSession.getLogin()))); - IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); + IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); assertThat(issueChangeData.getIssues()) .extracting(DefaultIssue::key) .containsOnly(Arrays.stream(issueDtos).map(IssueDto::getKey).toArray(String[]::new)); 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 3384f42a4ec..69a4815f72f 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 @@ -53,12 +53,12 @@ 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; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; +import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -113,10 +113,10 @@ public class DoTransitionActionTest { private ComponentDto project; private ComponentDto file; private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); - private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class); + private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.class); private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2, - issueChangeWebhook); + issueChangeTrigger); private WsActionTester tester = new WsActionTester(underTest); @Before @@ -140,12 +140,12 @@ public class DoTransitionActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED); - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class); - verify(issueChangeWebhook).onChange( + ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class); + verify(issueChangeTrigger).onChange( issueChangeDataCaptor.capture(), - eq(new IssueChangeWebhook.IssueChange(null, "confirm")), + eq(new IssueChangeTrigger.IssueChange(null, "confirm")), eq(IssueChangeContext.createUser(new Date(now), userSession.getLogin()))); - IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); + IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); assertThat(issueChangeData.getIssues()) .extracting(DefaultIssue::key) .containsOnly(issueDto.getKey()); 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 7ec66707d9b..e4a4bdea80b 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 @@ -30,7 +30,7 @@ public class IssueWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new IssueWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 32); } } 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 ed9320e7884..56b46375d01 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 @@ -51,10 +51,10 @@ 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; +import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -97,11 +97,11 @@ public class SetTypeActionTest { private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); - private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class); + private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.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, system2, issueChangeWebhook)); + responseWriter, system2, issueChangeTrigger)); @Test public void set_type() throws Exception { @@ -117,12 +117,12 @@ public class SetTypeActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getType()).isEqualTo(BUG.getDbConstant()); - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class); - verify(issueChangeWebhook).onChange( + ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class); + verify(issueChangeTrigger).onChange( issueChangeDataCaptor.capture(), - eq(new IssueChangeWebhook.IssueChange(BUG, null)), + eq(new IssueChangeTrigger.IssueChange(BUG, null)), eq(IssueChangeContext.createUser(new Date(now), userSession.getLogin()))); - IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); + IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); assertThat(issueChangeData.getIssues()) .extracting(DefaultIssue::key) .containsOnly(issueDto.getKey()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java new file mode 100644 index 00000000000..d0d48fb01ec --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java @@ -0,0 +1,606 @@ +/* + * 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.qualitygate.changeevent; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +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.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.sonar.api.config.Configuration; +import org.sonar.api.issue.DefaultTransitions; +import org.sonar.api.issue.Issue; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.System2; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.AnalysisPropertyDto; +import org.sonar.db.component.BranchDao; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDao; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.component.SnapshotDao; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger.IssueChange; +import org.sonar.server.settings.ProjectConfigurationLoader; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.webhook.WebhookPayloadFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +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.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.sonar.core.util.stream.MoreCollectors.toArrayList; +import static org.sonar.db.component.ComponentTesting.newBranchDto; + +@RunWith(DataProviderRunner.class) +public class IssueChangeTriggerImplTest { + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + + private DbClient dbClient = dbTester.getDbClient(); + + private Random random = new Random(); + 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 WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class); + private DbClient spiedOnDbClient = Mockito.spy(dbClient); + private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class); + private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class); + private IssueChangeTriggerImpl underTest = new IssueChangeTriggerImpl(spiedOnDbClient, projectConfigurationLoader, qgChangeEventListeners); + private DbClient mockedDbClient = mock(DbClient.class); + private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners); + + @Test + public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() { + mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), userChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + public void on_type_change_has_no_effect_if_scan_IssueChangeContext() { + mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), scanChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { + mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomAlphanumeric(12)), userChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @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 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) { + Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + + mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(transitionKey), userChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() { + mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(randomAlphanumeric(12)), scanChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { + mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() { + mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @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 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) { + Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + + mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(randomRuleType, transitionKey), userChangeContext); + + Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); + } + + @Test + @UseDataProvider("validIssueChanges") + public void broadcast_to_QGEventListeners_for_short_living_branch_of_issue(IssueChange issueChange) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentAndBranch branch = insertProjectBranch(project, BranchType.SHORT, "foo"); + SnapshotDto analysis = insertAnalysisTask(branch); + Configuration configuration = mockLoadProjectConfiguration(branch); + + Map properties = new HashMap<>(); + properties.put("sonar.analysis.test1", randomAlphanumeric(50)); + properties.put("sonar.analysis.test2", randomAlphanumeric(5000)); + insertPropertiesFor(analysis.getUuid(), properties); + + underTest.onChange(issueChangeData(newIssueDto(branch)), issueChange, userChangeContext); + + Collection events = verifyListenersBroadcastedTo(); + assertThat(events).hasSize(1); + QGChangeEvent event = events.iterator().next(); + assertThat(event.getProject()).isEqualTo(branch.component); + assertThat(event.getBranch()).isEqualTo(branch.branch); + assertThat(event.getAnalysis()).isEqualTo(analysis); + assertThat(event.getProjectConfiguration()).isSameAs(configuration); + } + + @Test + public void do_not_load_project_configuration_nor_analysis_nor_call_webhook_if_there_are_no_short_branch() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentAndBranch longBranch1 = insertProjectBranch(project, BranchType.LONG, "foo"); + ComponentAndBranch longBranch2 = insertProjectBranch(project, BranchType.LONG, "bar"); + ImmutableList issueDtos = ImmutableList.of(newIssueDto(project), newIssueDto(longBranch1), newIssueDto(longBranch2)); + + SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); + Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); + underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); + + Mockito.verifyZeroInteractions(projectConfigurationLoader); + Mockito.verify(snapshotDaoSpy, Mockito.times(0)).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class)); + } + + @Test + public void creates_single_QGChangeEvent_per_short_branch_with_at_least_one_issue_in_SearchResponseData() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); + 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 issueDtos = Stream.of( + IntStream.range(0, issuesBranch1).mapToObj(i -> newIssueDto(branch1.component)), + IntStream.range(0, issuesBranch2).mapToObj(i -> newIssueDto(branch2.component)), + IntStream.range(0, issuesBranch3).mapToObj(i -> newIssueDto(branch3.component))) + .flatMap(s -> s) + .collect(MoreCollectors.toList()); + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + Configuration configuration3 = mock(Configuration.class); + mockLoadProjectConfigurations( + branch1.component, configuration1, + branch2.component, configuration2, + branch3.component, configuration3); + + ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); + BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); + SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); + Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); + Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); + Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); + underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); + + Collection qgChangeEvents = verifyListenersBroadcastedTo(); + assertThat(qgChangeEvents) + .hasSize(3) + .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) + .containsOnly( + tuple(branch1.branch, configuration1, analysis1), + tuple(branch2.branch, configuration2, analysis2), + tuple(branch3.branch, configuration3, analysis3)); + + // verifyWebhookCalled(branch1, configuration1, analysis1); + // verifyWebhookCalled(branch2, configuration2, analysis2); + // verifyWebhookCalled(branch3, configuration3, analysis3); + // extractPayloadFactoryArguments(3); + + Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); + Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); + } + + @Test + public void create_QGChangeEvent_only_for_short_branches() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch shortBranch = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch longBranch = insertPrivateBranch(organization, BranchType.LONG); + SnapshotDto analysis1 = insertAnalysisTask(shortBranch); + SnapshotDto analysis2 = insertAnalysisTask(longBranch); + Configuration configuration = mockLoadProjectConfiguration(shortBranch); + + ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); + BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); + SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); + Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); + Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); + Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); + underTest.onChange( + issueChangeData(asList(newIssueDto(shortBranch), newIssueDto(longBranch))), + new IssueChange(randomRuleType), + userChangeContext); + + Collection qgChangeEvents = verifyListenersBroadcastedTo(); + assertThat(qgChangeEvents) + .hasSize(1) + .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) + .containsOnly(tuple(shortBranch.branch, configuration, analysis1)); + + Set uuids = ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()); + Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid()))); + Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); + } + + @Test + public void do_not_load_componentDto_from_DB_if_all_are_in_inputData() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); + SnapshotDto analysis1 = insertAnalysisTask(branch1); + SnapshotDto analysis2 = insertAnalysisTask(branch2); + SnapshotDto analysis3 = insertAnalysisTask(branch3); + List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + Configuration configuration3 = mock(Configuration.class); + mockLoadProjectConfigurations( + branch1.component, configuration1, + branch2.component, configuration2, + branch3.component, configuration3); + + ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); + Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); + underTest.onChange( + issueChangeData(issueDtos, branch1, branch2, branch3), + new IssueChange(randomRuleType), + userChangeContext); + + Collection qgChangeEvents = verifyListenersBroadcastedTo(); + assertThat(qgChangeEvents) + .hasSize(3) + .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) + .containsOnly( + tuple(branch1.branch, configuration1, analysis1), + tuple(branch2.branch, configuration2, analysis2), + tuple(branch3.branch, configuration3, analysis3)); + + Mockito.verify(componentDaoSpy, Mockito.times(0)).selectByUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class)); + Mockito.verifyNoMoreInteractions(componentDaoSpy); + } + + @Test + public void call_db_only_for_componentDto_not_in_inputData() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); + ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); + SnapshotDto analysis1 = insertAnalysisTask(branch1); + SnapshotDto analysis2 = insertAnalysisTask(branch2); + SnapshotDto analysis3 = insertAnalysisTask(branch3); + List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + Configuration configuration3 = mock(Configuration.class); + mockLoadProjectConfigurations( + branch1.component, configuration1, + branch2.component, configuration2, + branch3.component, configuration3); + + ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); + BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); + SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); + Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); + Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); + Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); + underTest.onChange( + issueChangeData(issueDtos, branch1, branch3), + new IssueChange(randomRuleType), + userChangeContext); + + assertThat(verifyListenersBroadcastedTo()).hasSize(3); + + Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); + Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(branch2.uuid()))); + Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); + } + + @Test + public void supports_issues_on_files_and_filter_on_short_branches_asap_when_calling_db() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPrivateProject(organization); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + ComponentAndBranch shortBranch = insertProjectBranch(project, BranchType.SHORT, "foo"); + ComponentAndBranch longBranch = insertProjectBranch(project, BranchType.LONG, "bar"); + ComponentDto shortBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(shortBranch.component)); + ComponentDto longBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(longBranch.component)); + SnapshotDto analysis1 = insertAnalysisTask(project); + SnapshotDto analysis2 = insertAnalysisTask(shortBranch); + SnapshotDto analysis3 = insertAnalysisTask(longBranch); + List issueDtos = asList( + newIssueDto(file, project), + newIssueDto(shortBranchFile, shortBranch), + newIssueDto(longBranchFile, longBranch)); + Configuration configuration = mockLoadProjectConfiguration(shortBranch); + + ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); + BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); + SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); + Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); + Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); + Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); + underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); + + Collection qgChangeEvents = verifyListenersBroadcastedTo(); + assertThat(qgChangeEvents) + .hasSize(1) + .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) + .containsOnly(tuple(shortBranch.branch, configuration, analysis2)); + + Set uuids = ImmutableSet.of(project.uuid(), shortBranch.uuid(), longBranch.uuid()); + Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); + Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()))); + Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid()))); + Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); + } + + private Configuration mockLoadProjectConfiguration(ComponentAndBranch componentAndBranch) { + Configuration configuration = mock(Configuration.class); + Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(singleton(componentAndBranch.component)))) + .thenReturn(ImmutableMap.of(componentAndBranch.uuid(), configuration)); + return configuration; + } + + private void mockLoadProjectConfigurations(Object... branchesAndConfiguration) { + checkArgument(branchesAndConfiguration.length % 2 == 0); + Set components = new HashSet<>(); + Map result = new HashMap<>(); + for (int i = 0; i < branchesAndConfiguration.length; i++) { + ComponentDto component = (ComponentDto) branchesAndConfiguration[i++]; + Configuration configuration = (Configuration) branchesAndConfiguration[i]; + components.add(component); + result.put(component.uuid(), configuration); + } + Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(components))) + .thenReturn(result); + } + + private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) { + ComponentDto project = dbTester.components().insertPrivateProject(organization); + BranchDto branchDto = newBranchDto(project.projectUuid(), branchType) + .setKey("foo"); + ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); + return new ComponentAndBranch(newComponent, branchDto); + } + + public ComponentAndBranch insertProjectBranch(ComponentDto project, BranchType type, String branchKey) { + BranchDto branchDto = newBranchDto(project.projectUuid(), type).setKey(branchKey); + ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); + return new ComponentAndBranch(newComponent, branchDto); + } + + private static class ComponentAndBranch { + private final ComponentDto component; + + private final BranchDto branch; + + private ComponentAndBranch(ComponentDto component, BranchDto branch) { + this.component = component; + this.branch = branch; + } + + public ComponentDto getComponent() { + return component; + } + + public BranchDto getBranch() { + return branch; + } + + public String uuid() { + return component.uuid(); + } + + } + + private void insertPropertiesFor(String snapshotUuid, Map properties) { + List analysisProperties = properties.entrySet().stream() + .map(entry -> new AnalysisPropertyDto() + .setUuid(UuidFactoryFast.getInstance().create()) + .setSnapshotUuid(snapshotUuid) + .setKey(entry.getKey()) + .setValue(entry.getValue())) + .collect(toArrayList(properties.size())); + dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties); + dbTester.getSession().commit(); + } + + private SnapshotDto insertAnalysisTask(ComponentAndBranch componentAndBranch) { + return insertAnalysisTask(componentAndBranch.component); + } + + private SnapshotDto insertAnalysisTask(ComponentDto component) { + return dbTester.components().insertSnapshot(component); + } + + private Collection verifyListenersBroadcastedTo() { + Class> clazz = (Class>) (Class) Collection.class; + ArgumentCaptor> supplierCaptor = ArgumentCaptor.forClass(clazz); + Mockito.verify(qgChangeEventListeners).broadcast( + Matchers.same(Trigger.ISSUE_CHANGE), + supplierCaptor.capture()); + return supplierCaptor.getValue(); + } + + @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)} + }; + } + + private IssueChangeTrigger.IssueChangeData issueChangeData() { + return new IssueChangeTrigger.IssueChangeData(emptyList(), emptyList()); + } + + private IssueChangeTrigger.IssueChangeData issueChangeData(IssueDto issueDto) { + return new IssueChangeTrigger.IssueChangeData(singletonList(issueDto.toDefaultIssue()), emptyList()); + } + + private IssueChangeTrigger.IssueChangeData issueChangeData(Collection issueDtos, ComponentAndBranch... components) { + return new IssueChangeTrigger.IssueChangeData( + issueDtos.stream().map(IssueDto::toDefaultIssue).collect(Collectors.toList()), + Arrays.stream(components).map(ComponentAndBranch::getComponent).collect(Collectors.toList())); + } + + private IssueDto newIssueDto(@Nullable ComponentAndBranch projectAndBranch) { + return projectAndBranch == null ? newIssueDto() : newIssueDto(projectAndBranch.component, projectAndBranch.component); + } + + private IssueDto newIssueDto(ComponentDto componentDto) { + return newIssueDto(componentDto, componentDto); + } + + private IssueDto newIssueDto() { + return newIssueDto((ComponentDto) null, (ComponentDto) null); + } + + private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentAndBranch componentAndBranch) { + return newIssueDto(component, componentAndBranch == null ? null : componentAndBranch.component); + } + + private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentDto project) { + RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)]; + String randomStatus = Issue.STATUSES.get(random.nextInt(Issue.STATUSES.size())); + IssueDto res = new IssueDto() + .setType(randomRuleType) + .setStatus(randomStatus) + .setRuleKey(randomAlphanumeric(3), randomAlphanumeric(4)); + if (component != null) { + res.setComponent(component); + } + if (project != null) { + res.setProject(project); + } + return res; + } +} 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/qualitygate/changeevent/IssueChangeTriggerTest.java similarity index 60% rename from server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java rename to server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerTest.java index b88f25a7228..1a05fce5719 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerTest.java @@ -17,7 +17,7 @@ * 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; +package org.sonar.server.qualitygate.changeevent; import org.junit.Rule; import org.junit.Test; @@ -27,7 +27,7 @@ import org.sonar.api.rules.RuleType; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; -public class IssueChangeWebhookTest { +public class IssueChangeTriggerTest { @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -36,53 +36,53 @@ public class IssueChangeWebhookTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("At least one of ruleType and transitionKey must be non null"); - new IssueChangeWebhook.IssueChange(null, null); + new IssueChangeTrigger.IssueChange(null, null); } @Test public void verify_IssueChange_getters() { - IssueChangeWebhook.IssueChange transitionKeyOnly = new IssueChangeWebhookImpl.IssueChange("foo"); + IssueChangeTrigger.IssueChange transitionKeyOnly = new IssueChangeTriggerImpl.IssueChange("foo"); assertThat(transitionKeyOnly.getTransitionKey()).contains("foo"); assertThat(transitionKeyOnly.getRuleType()).isEmpty(); - IssueChangeWebhook.IssueChange ruleTypeOnly = new IssueChangeWebhookImpl.IssueChange(RuleType.BUG); + IssueChangeTrigger.IssueChange ruleTypeOnly = new IssueChangeTriggerImpl.IssueChange(RuleType.BUG); assertThat(ruleTypeOnly.getTransitionKey()).isEmpty(); assertThat(ruleTypeOnly.getRuleType()).contains(RuleType.BUG); - IssueChangeWebhook.IssueChange transitionKeyAndRuleType = new IssueChangeWebhookImpl.IssueChange(RuleType.VULNERABILITY, "bar"); + IssueChangeTrigger.IssueChange transitionKeyAndRuleType = new IssueChangeTriggerImpl.IssueChange(RuleType.VULNERABILITY, "bar"); assertThat(transitionKeyAndRuleType.getTransitionKey()).contains("bar"); assertThat(transitionKeyAndRuleType.getRuleType()).contains(RuleType.VULNERABILITY); } @Test public void verify_IssueChange_equality() { - IssueChangeWebhook.IssueChange underTest = new IssueChangeWebhook.IssueChange(RuleType.BUG); + IssueChangeTrigger.IssueChange underTest = new IssueChangeTrigger.IssueChange(RuleType.BUG); - assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG)); - assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, null)); + assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG)); + assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, null)); assertThat(underTest).isNotEqualTo(null); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(randomAlphanumeric(10))); String transitionKey = randomAlphanumeric(10); - underTest = new IssueChangeWebhook.IssueChange(transitionKey); + underTest = new IssueChangeTrigger.IssueChange(transitionKey); - assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(transitionKey)); - assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(null, transitionKey)); + assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(transitionKey)); + assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(null, transitionKey)); assertThat(underTest).isNotEqualTo(null); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, transitionKey)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, transitionKey)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, transitionKey)); - assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(randomAlphanumeric(9))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, transitionKey)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, transitionKey)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, transitionKey)); + assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(randomAlphanumeric(9))); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java new file mode 100644 index 00000000000..acbeca51332 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java @@ -0,0 +1,482 @@ +/* + * 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.webhook; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.assertj.core.groups.Tuple; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.issue.Issue; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.AnalysisPropertyDto; +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.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.permission.index.AuthorizationTypeSupport; +import org.sonar.server.qualitygate.Condition; +import org.sonar.server.qualitygate.EvaluatedCondition; +import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; +import org.sonar.server.qualitygate.EvaluatedQualityGate; +import org.sonar.server.qualitygate.QualityGate; +import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; +import org.sonar.server.qualitygate.changeevent.QGChangeEvent; +import org.sonar.server.qualitygate.changeevent.Trigger; +import org.sonar.server.tester.UserSessionRule; + +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.assertj.core.groups.Tuple.tuple; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +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.core.util.stream.MoreCollectors.toArrayList; +import static org.sonar.db.component.BranchType.LONG; +import static org.sonar.db.component.ComponentTesting.newBranchDto; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN; + +public class WebhookQGChangeEventListenerTest { + private static final List OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED); + private static final List 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 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 DbClient spiedOnDbClient = Mockito.spy(dbClient); + private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, issueIndex, spiedOnDbClient, System2.INSTANCE); + private DbClient mockedDbClient = mock(DbClient.class); + private IssueIndex spiedOnIssueIndex = Mockito.spy(issueIndex); + private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient, System2.INSTANCE); + + @Test + public void onChanges_has_no_effect_if_changeEvents_is_empty() { + mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, Collections.emptyList()); + + verifyZeroInteractions(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient); + } + + @Test + public void onChanges_has_no_effect_if_no_webhook_is_configured() { + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + mockWebhookDisabled(configuration1, configuration2); + + mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of( + new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration1), + new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration2))); + + verify(webHooks).isEnabled(configuration1); + verify(webHooks).isEnabled(configuration2); + verifyZeroInteractions(webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient); + } + + @Test + public void onChanges_calls_webhook_for_changeEvent_with_webhook_enabled() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentAndBranch branch = insertProjectBranch(project, BranchType.SHORT, "foo"); + SnapshotDto analysis = insertAnalysisTask(branch); + Configuration configuration = mock(Configuration.class); + mockWebhookEnabled(configuration); + mockPayloadSupplierConsumedByWebhooks(); + Map properties = new HashMap<>(); + properties.put("sonar.analysis.test1", randomAlphanumeric(50)); + properties.put("sonar.analysis.test2", randomAlphanumeric(5000)); + insertPropertiesFor(analysis.getUuid(), properties); + + underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration))); + + ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); + Condition condition1 = new Condition(BUGS_KEY, GREATER_THAN, "0", null, false); + Condition condition2 = new Condition(VULNERABILITIES_KEY, GREATER_THAN, "0", null, false); + Condition condition3 = new Condition(CODE_SMELLS_KEY, GREATER_THAN, "0", null, false); + 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), + EvaluatedQualityGate.newBuilder() + .setQualityGate( + new QualityGate( + valueOf(ShortLivingBranchQualityGate.ID), + ShortLivingBranchQualityGate.NAME, + ImmutableSet.of(condition1, condition2, condition3))) + .setStatus(EvaluatedQualityGate.Status.OK) + .addCondition(condition1, EvaluationStatus.OK, "0") + .addCondition(condition2, EvaluationStatus.OK, "0") + .addCondition(condition3, EvaluationStatus.OK, "0") + .build(), + null, + properties)); + } + + @Test + public void onChanges_does_not_call_webhook_if_disabled_for_QGChangeEvent() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentAndBranch branch1 = insertProjectBranch(project, BranchType.SHORT, "foo"); + ComponentAndBranch branch2 = insertProjectBranch(project, BranchType.SHORT, "bar"); + SnapshotDto analysis1 = insertAnalysisTask(branch1); + SnapshotDto analysis2 = insertAnalysisTask(branch2); + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + mockWebhookDisabled(configuration1); + mockWebhookEnabled(configuration2); + mockPayloadSupplierConsumedByWebhooks(); + + underTest.onChanges( + Trigger.ISSUE_CHANGE, + ImmutableList.of( + newQGChangeEvent(branch1, analysis1, configuration1), + newQGChangeEvent(branch2, analysis2, configuration2))); + + verifyWebhookNotCalled(branch1, analysis1, configuration1); + verifyWebhookCalled(branch2, analysis2, configuration2); + } + + @Test + public void onChanges_calls_webhook_for_any_type_of_branch() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch mainBranch = insertMainBranch(organization); + ComponentAndBranch longBranch = insertProjectBranch(mainBranch.component, BranchType.LONG, "foo"); + SnapshotDto analysis1 = insertAnalysisTask(mainBranch); + SnapshotDto analysis2 = insertAnalysisTask(longBranch); + Configuration configuration1 = mock(Configuration.class); + Configuration configuration2 = mock(Configuration.class); + mockWebhookEnabled(configuration1, configuration2); + + underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of( + newQGChangeEvent(mainBranch, analysis1, configuration1), + newQGChangeEvent(longBranch, analysis2, configuration2))); + + verifyWebhookCalled(mainBranch, analysis1, configuration1); + verifyWebhookCalled(longBranch, analysis2, configuration2); + } + + @Test + public void onChanges_calls_webhook_once_per_QGChangeEvent_even_for_same_branch_and_configuration() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); + SnapshotDto analysis1 = insertAnalysisTask(branch1); + Configuration configuration1 = mock(Configuration.class); + mockWebhookEnabled(configuration1); + mockPayloadSupplierConsumedByWebhooks(); + + underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of( + newQGChangeEvent(branch1, analysis1, configuration1), + newQGChangeEvent(branch1, analysis1, configuration1), + newQGChangeEvent(branch1, analysis1, configuration1))); + + verify(webHooks, times(3)).isEnabled(configuration1); + verify(webHooks, times(3)).sendProjectAnalysisUpdate( + Matchers.same(configuration1), + Matchers.eq(new WebHooks.Analysis(branch1.uuid(), analysis1.getUuid(), null)), + any(Supplier.class)); + extractPayloadFactoryArguments(3); + } + + @Test + public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT); + SnapshotDto analysis = insertAnalysisTask(branch); + Configuration configuration = mock(Configuration.class); + mockWebhookEnabled(configuration); + mockPayloadSupplierConsumedByWebhooks(); + + underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration))); + + ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); + EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); + assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK); + assertThat(qualityGate.getEvaluatedConditions()) + .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue) + .containsOnly(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(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))), + tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")), + 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(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")), + tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))), + 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(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")), + tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")), + tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues)))); + } + + private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions( + int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT); + 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); + Configuration configuration = mock(Configuration.class); + mockWebhookEnabled(configuration); + mockPayloadSupplierConsumedByWebhooks(); + + underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration))); + + ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); + EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); + assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); + assertThat(qualityGate.getEvaluatedConditions()) + .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue) + .containsOnly(expectedQGConditions); + } + + @Test + public void computes_QG_error_with_all_failing_conditions() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT); + 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); + Configuration configuration = mock(Configuration.class); + mockWebhookEnabled(configuration); + mockPayloadSupplierConsumedByWebhooks(); + + underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration))); + + ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis); + EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get(); + assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); + assertThat(qualityGate.getEvaluatedConditions()) + .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::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)))); + } + + private void mockWebhookEnabled(Configuration... configurations) { + for (Configuration configuration : configurations) { + Mockito.when(webHooks.isEnabled(configuration)).thenReturn(true); + } + } + + private void mockWebhookDisabled(Configuration... configurations) { + for (Configuration configuration : configurations) { + Mockito.when(webHooks.isEnabled(configuration)).thenReturn(false); + } + } + + private void mockPayloadSupplierConsumedByWebhooks() { + Mockito.doAnswer(invocationOnMock -> { + Supplier supplier = (Supplier) invocationOnMock.getArguments()[2]; + supplier.get(); + return null; + }).when(webHooks) + .sendProjectAnalysisUpdate(Matchers.any(Configuration.class), Matchers.any(), Matchers.any()); + } + + private void insertIssue(ComponentAndBranch componentAndBranch, RuleType ruleType, String status, @Nullable String resolution) { + ComponentDto component = componentAndBranch.component; + RuleDefinitionDto rule = RuleTesting.newRule(); + dbTester.rules().insert(rule); + dbTester.commit(); + dbTester.issues().insert(rule, component, component, i -> i.setType(ruleType).setStatus(status).setResolution(resolution)); + dbTester.commit(); + } + + private void insertPropertiesFor(String snapshotUuid, Map properties) { + List analysisProperties = properties.entrySet().stream() + .map(entry -> new AnalysisPropertyDto() + .setUuid(UuidFactoryFast.getInstance().create()) + .setSnapshotUuid(snapshotUuid) + .setKey(entry.getKey()) + .setValue(entry.getValue())) + .collect(toArrayList(properties.size())); + dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties); + dbTester.getSession().commit(); + } + + private SnapshotDto insertAnalysisTask(ComponentAndBranch componentAndBranch) { + return dbTester.components().insertSnapshot(componentAndBranch.component); + } + + private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ComponentAndBranch componentAndBranch, Configuration configuration, SnapshotDto analysis) { + verifyWebhookCalled(componentAndBranch, analysis, configuration); + + return extractPayloadFactoryArguments(1).iterator().next(); + } + + private void verifyWebhookCalled(ComponentAndBranch componentAndBranch, SnapshotDto analysis, Configuration branchConfiguration) { + verify(webHooks).isEnabled(branchConfiguration); + verify(webHooks).sendProjectAnalysisUpdate( + Matchers.same(branchConfiguration), + Matchers.eq(new WebHooks.Analysis(componentAndBranch.uuid(), analysis.getUuid(), null)), + any(Supplier.class)); + } + + private void verifyWebhookNotCalled(ComponentAndBranch componentAndBranch, SnapshotDto analysis, Configuration branchConfiguration) { + verify(webHooks).isEnabled(branchConfiguration); + verify(webHooks, times(0)).sendProjectAnalysisUpdate( + Matchers.same(branchConfiguration), + Matchers.eq(new WebHooks.Analysis(componentAndBranch.uuid(), analysis.getUuid(), null)), + any(Supplier.class)); + } + + private List extractPayloadFactoryArguments(int time) { + ArgumentCaptor projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class); + verify(webhookPayloadFactory, Mockito.times(time)).create(projectAnalysisCaptor.capture()); + return projectAnalysisCaptor.getAllValues(); + } + + private void indexIssues(ComponentAndBranch componentAndBranch) { + issueIndexer.indexOnAnalysis(componentAndBranch.uuid()); + } + + private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) { + ComponentDto project = dbTester.components().insertPrivateProject(organization); + BranchDto branchDto = newBranchDto(project.projectUuid(), branchType) + .setKey("foo"); + ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); + return new ComponentAndBranch(newComponent, branchDto); + } + + public ComponentAndBranch insertMainBranch(OrganizationDto organization) { + ComponentDto project = newPrivateProjectDto(organization); + BranchDto branch = newBranchDto(project, LONG).setKey("master"); + dbTester.components().insertComponent(project); + dbClient.branchDao().insert(dbTester.getSession(), branch); + dbTester.commit(); + return new ComponentAndBranch(project, branch); + } + + public ComponentAndBranch insertProjectBranch(ComponentDto project, BranchType type, String branchKey) { + BranchDto branchDto = newBranchDto(project.projectUuid(), type).setKey(branchKey); + ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); + return new ComponentAndBranch(newComponent, branchDto); + } + + private static class ComponentAndBranch { + private final ComponentDto component; + + private final BranchDto branch; + + private ComponentAndBranch(ComponentDto component, BranchDto branch) { + this.component = component; + this.branch = branch; + } + + public ComponentDto getComponent() { + return component; + } + + public BranchDto getBranch() { + return branch; + } + + public String uuid() { + return component.uuid(); + } + + } + + private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration) { + return new QGChangeEvent(branch.component, branch.branch, analysis, configuration); + } + +} -- 2.39.5