From: Belen Pruvost Date: Wed, 11 May 2022 20:17:29 +0000 (+0200) Subject: SONAR-16372 - Push IssueChangedEvents X-Git-Tag: 9.5.0.56709~132 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e774fdb3e5fbb4d6153b4379fc0af5606d1fcab3;p=sonarqube.git SONAR-16372 - Push IssueChangedEvents --- diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java index 3c4b5235f5c..13581a4d364 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppNodesClusterHostsConsistencyTest.java @@ -40,8 +40,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.sonar.application.config.TestAppSettings; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.process.cluster.hz.DistributedAnswer; import org.sonar.process.cluster.hz.DistributedCall; import org.sonar.process.cluster.hz.DistributedCallback; @@ -206,6 +208,16 @@ public class AppNodesClusterHostsConsistencyTest { } + @Override + public void subscribeIssueChangeTopic(IssueChangeListener listener) { + + } + + @Override + public void publishEvent(IssueChangedEvent event) { + + } + @Override public void close() { diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java index 2f0ec8cb8e0..833864b8e96 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java +++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java @@ -26,8 +26,10 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.locks.Lock; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.process.ProcessId; public interface HazelcastMember extends AutoCloseable { @@ -112,6 +114,10 @@ public interface HazelcastMember extends AutoCloseable { void publishEvent(RuleSetChangedEvent event); + void subscribeIssueChangeTopic(IssueChangeListener listener); + + void publishEvent(IssueChangedEvent event); + @Override void close(); } diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java index 89a9b75f994..bb6a541087d 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java +++ b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java @@ -39,8 +39,10 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Lock; import java.util.stream.Collectors; import org.slf4j.LoggerFactory; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; class HazelcastMemberImpl implements HazelcastMember { @@ -141,6 +143,18 @@ class HazelcastMemberImpl implements HazelcastMember { hzInstance.getTopic("ruleActivated").publish(event); } + @Override + public void subscribeIssueChangeTopic(IssueChangeListener listener) { + ITopic topic = hzInstance.getTopic("issueChanged"); + MessageListener hzListener = message -> listener.listen(message.getMessageObject()); + topic.addMessageListener(hzListener); + } + + @Override + public void publishEvent(IssueChangedEvent event) { + hzInstance.getTopic("issueChanged").publish(event); + } + @Override public void close() { try { diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java index 86418d5f637..032143d68d3 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberImplTest.java @@ -36,8 +36,8 @@ import org.junit.Test; import org.junit.rules.DisableOnDebug; import org.junit.rules.TestRule; import org.junit.rules.Timeout; -import org.mockito.verification.VerificationMode; -import org.sonar.core.util.RuleActivationListener; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.rule.RuleActivationListener; import org.sonar.process.NetworkUtilsImpl; import org.sonar.process.ProcessId; @@ -128,6 +128,19 @@ public class HazelcastMemberImplTest { verify(topic, times(1)).addMessageListener(any()); } + @Test + public void subscribeIssueChangeTopic_listenerAdded() { + IssueChangeListener listener = mock(IssueChangeListener.class); + HazelcastInstance hzInstance = mock(HazelcastInstance.class); + ITopic topic = mock(ITopic.class); + when(hzInstance.getTopic(any())).thenReturn(topic); + HazelcastMemberImpl underTest = new HazelcastMemberImpl(hzInstance); + + underTest.subscribeIssueChangeTopic(listener); + + verify(topic, times(1)).addMessageListener(any()); + } + private static HazelcastMember newHzMember(int port, int... otherPorts) { return new HazelcastMemberBuilder(JoinConfigurationType.TCP_IP) .setProcessId(ProcessId.COMPUTE_ENGINE) diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java new file mode 100644 index 00000000000..ae5bbc7e5dc --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributor.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import org.sonar.api.server.ServerSide; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.process.cluster.hz.HazelcastMember; + +@ServerSide +public class DistributedIssueChangeEventsDistributor implements IssueChangeEventsDistributor { + + private HazelcastMember hazelcastMember; + + public DistributedIssueChangeEventsDistributor(HazelcastMember hazelcastMember) { + this.hazelcastMember = hazelcastMember; + } + + @Override + public void subscribe(IssueChangeListener listener) { + hazelcastMember.subscribeIssueChangeTopic(listener); + } + + @Override + public void pushEvent(IssueChangedEvent event) { + hazelcastMember.publishEvent(event); + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java new file mode 100644 index 00000000000..b784f0eead5 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtils.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import org.json.JSONArray; +import org.json.JSONObject; +import org.sonar.core.util.issue.Issue; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.server.pushapi.sonarlint.SonarLintClient; + +import static java.util.Arrays.asList; + +public class IssueChangeBroadcastUtils { + private IssueChangeBroadcastUtils() { + + } + + public static Predicate getFilterForEvent(IssueChangedEvent issueChangedEvent) { + List affectedProjects = asList(issueChangedEvent.getProjectKey()); + return client -> { + Set clientProjectKeys = client.getClientProjectKeys(); + return !Collections.disjoint(clientProjectKeys, affectedProjects); + }; + } + + public static String getMessage(IssueChangedEvent issueChangedEvent) { + return "event: " + issueChangedEvent.getEvent() + "\n" + + "data: " + toJson(issueChangedEvent); + } + + private static String toJson(IssueChangedEvent issueChangedEvent) { + JSONObject data = new JSONObject(); + data.put("projectKey", issueChangedEvent.getProjectKey()); + + JSONArray issuesJson = new JSONArray(); + for (Issue issue : issueChangedEvent.getIssues()) { + issuesJson.put(toJson(issue)); + } + data.put("issues", issuesJson); + data.put("userSeverity", issueChangedEvent.getUserSeverity()); + data.put("userType", issueChangedEvent.getUserType()); + data.put("resolved", issueChangedEvent.getResolved()); + + return data.toString(); + } + + private static JSONObject toJson(Issue issue) { + JSONObject ruleJson = new JSONObject(); + ruleJson.put("issueKey", issue.getIssueKey()); + ruleJson.put("branchName", issue.getBranchName()); + return ruleJson; + } + +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java new file mode 100644 index 00000000000..7decf7279f5 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventService.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nullable; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ComponentDto; + +public interface IssueChangeEventService { + void distributeIssueChangeEvent(DefaultIssue issue, @Nullable String severity, @Nullable String type, + @Nullable String transitionKey, BranchDto branch, String projectKey); + + void distributeIssueChangeEvent(Collection issues, Map projectsByUuid, + Map branchesByProjectUuid); +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java new file mode 100644 index 00000000000..54dc1c71ba1 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImpl.java @@ -0,0 +1,154 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.server.ServerSide; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs.Diff; +import org.sonar.core.util.issue.Issue; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ComponentDto; + +import static org.elasticsearch.common.Strings.isNullOrEmpty; +import static org.sonar.api.issue.DefaultTransitions.CONFIRM; +import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE; +import static org.sonar.api.issue.DefaultTransitions.UNCONFIRM; +import static org.sonar.api.issue.DefaultTransitions.WONT_FIX; +import static org.sonar.db.component.BranchType.BRANCH; + +@ServerSide +public class IssueChangeEventServiceImpl implements IssueChangeEventService { + private static final String FALSE_POSITIVE_KEY = "FALSE-POSITIVE"; + private static final String WONT_FIX_KEY = "WONTFIX"; + + private static final String RESOLUTION_KEY = "resolution"; + private static final String SEVERITY_KEY = "severity"; + private static final String TYPE_KEY = "type"; + + private final IssueChangeEventsDistributor eventsDistributor; + + public IssueChangeEventServiceImpl(IssueChangeEventsDistributor eventsDistributor) { + this.eventsDistributor = eventsDistributor; + } + + @Override + public void distributeIssueChangeEvent(DefaultIssue issue, @Nullable String severity, @Nullable String type, @Nullable String transition, + BranchDto branch, String projectKey) { + Issue changedIssue = new Issue(issue.key(), branch.getKey()); + + Boolean resolved = isResolved(transition); + + if (severity == null && type == null && resolved == null) { + return; + } + + IssueChangedEvent event = new IssueChangedEvent(projectKey, new Issue[]{changedIssue}, + resolved, severity, type); + eventsDistributor.pushEvent(event); + } + + @Override + public void distributeIssueChangeEvent(Collection issues, Map projectsByUuid, + Map branchesByProjectUuid) { + + for (Entry entry : projectsByUuid.entrySet()) { + String projectKey = entry.getValue().getKey(); + + Set issuesInProject = issues + .stream() + .filter(i -> i.projectUuid().equals(entry.getKey())) + .collect(Collectors.toSet()); + + Issue[] issueChanges = issuesInProject.stream() + .filter(i -> branchesByProjectUuid.get(i.projectUuid()).getBranchType().equals(BRANCH)) + .map(i -> new Issue(i.key(), branchesByProjectUuid.get(i.projectUuid()).getKey())) + .toArray(Issue[]::new); + + if (issueChanges.length == 0) { + continue; + } + + IssueChangedEvent event = getIssueChangedEvent(projectKey, issuesInProject, issueChanges); + + if (event != null) { + eventsDistributor.pushEvent(event); + } + } + } + + @CheckForNull + private static IssueChangedEvent getIssueChangedEvent(String projectKey, Set issuesInProject, Issue[] issueChanges) { + DefaultIssue firstIssue = issuesInProject.stream().iterator().next(); + + if (firstIssue.currentChange() == null) { + return null; + } + + Boolean resolved = null; + String severity = null; + String type = null; + + boolean isRelevantEvent = false; + Map diffs = firstIssue.currentChange().diffs(); + + if (diffs.containsKey(RESOLUTION_KEY)) { + resolved = diffs.get(RESOLUTION_KEY).newValue() == null ? false : isResolved(diffs.get(RESOLUTION_KEY).newValue().toString()); + isRelevantEvent = true; + } + + if (diffs.containsKey(SEVERITY_KEY)) { + severity = diffs.get(SEVERITY_KEY).newValue() == null ? null : diffs.get(SEVERITY_KEY).newValue().toString(); + isRelevantEvent = true; + } + + if (diffs.containsKey(TYPE_KEY)) { + type = diffs.get(TYPE_KEY).newValue() == null ? null : diffs.get(TYPE_KEY).newValue().toString(); + isRelevantEvent = true; + } + + if (!isRelevantEvent) { + return null; + } + + return new IssueChangedEvent(projectKey, issueChanges, resolved, severity, type); + } + + @CheckForNull + private static Boolean isResolved(@Nullable String transitionOrStatus) { + if (isNullOrEmpty(transitionOrStatus)) { + return null; + } + + if (transitionOrStatus.equals(CONFIRM) || transitionOrStatus.equals(UNCONFIRM)) { + return null; + } + + return transitionOrStatus.equals(WONT_FIX) || transitionOrStatus.equals(FALSE_POSITIVE) || + transitionOrStatus.equals(FALSE_POSITIVE_KEY) || transitionOrStatus.equals(WONT_FIX_KEY); + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java new file mode 100644 index 00000000000..486cc87068f --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/IssueChangeEventsDistributor.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; + +public interface IssueChangeEventsDistributor { + + void subscribe(IssueChangeListener listener); + + void pushEvent(IssueChangedEvent event); +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java new file mode 100644 index 00000000000..53aa3f34054 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributor.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.ArrayList; +import java.util.List; +import org.sonar.api.server.ServerSide; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; + +@ServerSide +public class StandaloneIssueChangeEventsDistributor implements IssueChangeEventsDistributor { + + private List listeners = new ArrayList<>(); + + @Override + public void subscribe(IssueChangeListener listener) { + listeners.add(listener); + } + + @Override + public void pushEvent(IssueChangedEvent event) { + listeners.forEach(l -> l.listen(event)); + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java new file mode 100644 index 00000000000..497a7e13420 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/issues/package-info.java @@ -0,0 +1,21 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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. + */ +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.server.pushapi.issues; diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java index c5b4b9fae9d..a45ff9ce87f 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/DistributedRuleActivatorEventsDistributor.java @@ -20,8 +20,8 @@ package org.sonar.server.pushapi.qualityprofile; import org.sonar.api.server.ServerSide; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.process.cluster.hz.HazelcastMember; @ServerSide diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java index 568dc5eb9f3..76fc0799986 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImpl.java @@ -34,8 +34,8 @@ import org.jetbrains.annotations.NotNull; import org.sonar.api.rule.RuleKey; import org.sonar.api.server.ServerSide; import org.sonar.core.util.ParamChange; -import org.sonar.core.util.RuleChange; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.rule.RuleChange; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.project.ProjectDto; diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java index 0d8a6c26c8b..8914610b28a 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleActivatorEventsDistributor.java @@ -19,8 +19,8 @@ */ package org.sonar.server.pushapi.qualityprofile; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; public interface RuleActivatorEventsDistributor { diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java new file mode 100644 index 00000000000..159d67fa593 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtils.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.qualityprofile; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import org.json.JSONArray; +import org.json.JSONObject; +import org.sonar.core.util.ParamChange; +import org.sonar.core.util.rule.RuleChange; +import org.sonar.core.util.rule.RuleSetChangedEvent; +import org.sonar.server.pushapi.sonarlint.SonarLintClient; + +import static java.util.Arrays.asList; + +public class RuleSetChangeBroadcastUtils { + private RuleSetChangeBroadcastUtils() { + } + + public static Predicate getFilterForEvent(RuleSetChangedEvent ruleSetChangedEvent) { + List affectedProjects = asList(ruleSetChangedEvent.getProjects()); + return client -> { + Set clientProjectKeys = client.getClientProjectKeys(); + Set languages = client.getLanguages(); + return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleSetChangedEvent.getLanguage()); + }; + } + + public static String getMessage(RuleSetChangedEvent ruleSetChangedEvent) { + return "event: " + ruleSetChangedEvent.getEvent() + "\n" + + "data: " + toJson(ruleSetChangedEvent); + } + + private static String toJson(RuleSetChangedEvent ruleSetChangedEvent) { + JSONObject data = new JSONObject(); + data.put("projects", ruleSetChangedEvent.getProjects()); + + JSONArray activatedRulesJson = new JSONArray(); + for (RuleChange rule : ruleSetChangedEvent.getActivatedRules()) { + activatedRulesJson.put(toJson(rule)); + } + data.put("activatedRules", activatedRulesJson); + + JSONArray deactivatedRulesJson = new JSONArray(); + for (String ruleKey : ruleSetChangedEvent.getDeactivatedRules()) { + deactivatedRulesJson.put(ruleKey); + } + data.put("deactivatedRules", deactivatedRulesJson); + + return data.toString(); + } + + private static JSONObject toJson(RuleChange rule) { + JSONObject ruleJson = new JSONObject(); + ruleJson.put("key", rule.getKey()); + ruleJson.put("language", rule.getLanguage()); + ruleJson.put("severity", rule.getSeverity()); + ruleJson.put("templateKey", rule.getTemplateKey()); + + JSONArray params = new JSONArray(); + for (ParamChange paramChange : rule.getParams()) { + params.put(toJson(paramChange)); + } + ruleJson.put("params", params); + return ruleJson; + } + + private static JSONObject toJson(ParamChange paramChange) { + JSONObject param = new JSONObject(); + param.put("key", paramChange.getKey()); + param.put("value", paramChange.getValue()); + return param; + } + +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java index 3207154ac2e..64a5d15e10a 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/qualityprofile/StandaloneRuleActivatorEventsDistributor.java @@ -22,8 +22,8 @@ package org.sonar.server.pushapi.qualityprofile; import java.util.ArrayList; import java.util.List; import org.sonar.api.server.ServerSide; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; @ServerSide public class StandaloneRuleActivatorEventsDistributor implements RuleActivatorEventsDistributor { diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java index 88d87433773..d2508ada331 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java @@ -20,7 +20,6 @@ package org.sonar.server.pushapi.sonarlint; import java.io.IOException; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -28,34 +27,36 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Predicate; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; -import org.json.JSONArray; -import org.json.JSONObject; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.core.util.ParamChange; -import org.sonar.core.util.RuleActivationListener; -import org.sonar.core.util.RuleChange; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.core.util.rule.RuleActivationListener; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.pushapi.issues.IssueChangeBroadcastUtils; +import org.sonar.server.pushapi.issues.IssueChangeEventsDistributor; import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor; - -import static java.util.Arrays.asList; +import org.sonar.server.pushapi.qualityprofile.RuleSetChangeBroadcastUtils; @ServerSide -public class SonarLintClientsRegistry implements RuleActivationListener { +public class SonarLintClientsRegistry implements RuleActivationListener, IssueChangeListener { private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class); private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator; private final List clients = new CopyOnWriteArrayList<>(); - private final RuleActivatorEventsDistributor eventsDistributor; + private final RuleActivatorEventsDistributor ruleEventsDistributor; + private final IssueChangeEventsDistributor issueChangeEventsDistributor; private boolean registeredToEvents = false; - public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) { + public SonarLintClientsRegistry(IssueChangeEventsDistributor issueChangeEventsDistributor, + RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) { + this.issueChangeEventsDistributor = issueChangeEventsDistributor; this.sonarLintClientPermissionsValidator = permissionsValidator; - this.eventsDistributor = ruleActivatorEventsDistributor; + this.ruleEventsDistributor = ruleActivatorEventsDistributor; } public void registerClient(SonarLintClient sonarLintClient) { @@ -71,10 +72,11 @@ public class SonarLintClientsRegistry implements RuleActivationListener { return; } try { - eventsDistributor.subscribe(this); + ruleEventsDistributor.subscribe(this); + issueChangeEventsDistributor.subscribe(this); registeredToEvents = true; } catch (RuntimeException e) { - LOG.warn("Can not listen to rule activation events for server push. Web Server might not have started fully yet.", e); + LOG.warn("Can not listen to rule activation or issue events for server push. Web Server might not have started fully yet.", e); } } @@ -90,16 +92,12 @@ public class SonarLintClientsRegistry implements RuleActivationListener { @Override public void listen(RuleSetChangedEvent ruleSetChangedEvent) { - broadcastMessage(ruleSetChangedEvent, getFilterForEvent(ruleSetChangedEvent)); + broadcastMessage(ruleSetChangedEvent, RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent)); } - private static Predicate getFilterForEvent(RuleSetChangedEvent ruleSetChangedEvent) { - List affectedProjects = asList(ruleSetChangedEvent.getProjects()); - return client -> { - Set clientProjectKeys = client.getClientProjectKeys(); - Set languages = client.getLanguages(); - return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleSetChangedEvent.getLanguage()); - }; + @Override + public void listen(IssueChangedEvent issueChangedEvent) { + broadcastMessage(issueChangedEvent, IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent)); } public void broadcastMessage(RuleSetChangedEvent event, Predicate filter) { @@ -110,7 +108,7 @@ public class SonarLintClientsRegistry implements RuleActivationListener { sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient); RuleSetChangedEvent personalizedEvent = new RuleSetChangedEvent(projectKeysInterestingForClient.toArray(String[]::new), event.getActivatedRules(), event.getDeactivatedRules(), event.getLanguage()); - String message = getMessage(personalizedEvent); + String message = RuleSetChangeBroadcastUtils.getMessage(personalizedEvent); c.writeAndFlush(message); } catch (ForbiddenException forbiddenException) { LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage()); @@ -121,50 +119,23 @@ public class SonarLintClientsRegistry implements RuleActivationListener { } }); } - private static String getMessage(RuleSetChangedEvent ruleSetChangedEvent) { - return "event: " + ruleSetChangedEvent.getEvent() + "\n" - + "data: " + toJson(ruleSetChangedEvent); - } - - private static String toJson(RuleSetChangedEvent ruleSetChangedEvent) { - JSONObject data = new JSONObject(); - data.put("projects", ruleSetChangedEvent.getProjects()); - - JSONArray activatedRulesJson = new JSONArray(); - for (RuleChange rule : ruleSetChangedEvent.getActivatedRules()) { - activatedRulesJson.put(toJson(rule)); - } - data.put("activatedRules", activatedRulesJson); - - JSONArray deactivatedRulesJson = new JSONArray(); - for (String ruleKey : ruleSetChangedEvent.getDeactivatedRules()) { - deactivatedRulesJson.put(ruleKey); - } - data.put("deactivatedRules", deactivatedRulesJson); - - return data.toString(); - } - - private static JSONObject toJson(RuleChange rule) { - JSONObject ruleJson = new JSONObject(); - ruleJson.put("key", rule.getKey()); - ruleJson.put("language", rule.getLanguage()); - ruleJson.put("severity", rule.getSeverity()); - ruleJson.put("templateKey", rule.getTemplateKey()); - - JSONArray params = new JSONArray(); - for (ParamChange paramChange : rule.getParams()) { - params.put(toJson(paramChange)); - } - ruleJson.put("params", params); - return ruleJson; - } - private static JSONObject toJson(ParamChange paramChange) { - JSONObject param = new JSONObject(); - param.put("key", paramChange.getKey()); - param.put("value", paramChange.getValue()); - return param; + public void broadcastMessage(IssueChangedEvent event, Predicate filter) { + clients.stream().filter(filter).forEach(c -> { + Set projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys()); + projectKeysInterestingForClient.retainAll(Set.of(event.getProjectKey())); + try { + sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient); + String message = IssueChangeBroadcastUtils.getMessage(event); + c.writeAndFlush(message); + } catch (ForbiddenException forbiddenException) { + LOG.debug("Client is no longer authenticated: " + forbiddenException.getMessage()); + unregisterClient(c); + } catch (IllegalStateException | IOException e) { + LOG.error("Unable to send message to a client: " + e.getMessage()); + unregisterClient(c); + } + }); } class SonarLintClientEventsListener implements AsyncListener { diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java new file mode 100644 index 00000000000..b08a831cb47 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/DistributedIssueChangeEventsDistributorTest.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import org.junit.Test; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.process.cluster.hz.HazelcastMember; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class DistributedIssueChangeEventsDistributorTest { + HazelcastMember hazelcastMember = mock(HazelcastMember.class); + IssueChangeListener issueChangeListener = mock(IssueChangeListener.class); + IssueChangedEvent event = mock(IssueChangedEvent.class); + + public final DistributedIssueChangeEventsDistributor underTest = new DistributedIssueChangeEventsDistributor(hazelcastMember); + + @Test + public void subscribe_subscribesHazelCastMember() { + underTest.subscribe(issueChangeListener); + verify(hazelcastMember).subscribeIssueChangeTopic(issueChangeListener); + } + + @Test + public void pushEvent_publishesEvent() { + underTest.pushEvent(event); + verify(hazelcastMember).publishEvent(event); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java new file mode 100644 index 00000000000..8e65c839b02 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeBroadcastUtilsTest.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.Set; +import java.util.function.Predicate; +import javax.servlet.AsyncContext; +import org.junit.Test; +import org.sonar.core.util.issue.Issue; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.server.pushapi.sonarlint.SonarLintClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class IssueChangeBroadcastUtilsTest { + + private final String PROJECT_KEY = "projectKey"; + private final String USER_UUID = "userUUID"; + private final AsyncContext asyncContext = mock(AsyncContext.class); + + @Test + public void getsFilterForEvent() { + Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")}; + IssueChangedEvent issueChangedEvent = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG"); + Predicate predicate = IssueChangeBroadcastUtils.getFilterForEvent(issueChangedEvent); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY), Set.of(), USER_UUID))).isTrue(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(), Set.of(), USER_UUID))).isFalse(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of(), USER_UUID))).isFalse(); + } + + @Test + public void getsMessageForEvent() { + Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")}; + IssueChangedEvent issueChangedEvent = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG"); + String message = IssueChangeBroadcastUtils.getMessage(issueChangedEvent); + + assertThat(message).isEqualTo("event: IssueChangedEvent\n" + + "data: {\"projectKey\":\""+ PROJECT_KEY+"\"," + + "\"userType\":\"BUG\"," + + "\"issues\":[{\"issueKey\":\"issue-1\",\"branchName\":\"branch-1\"}]," + + "\"userSeverity\":\"BLOCKER\"," + + "\"resolved\":true}"); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java new file mode 100644 index 00000000000..91d543e2e73 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/IssueChangeEventServiceImplTest.java @@ -0,0 +1,226 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.rules.RuleType; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs; +import org.sonar.core.util.issue.IssueChangedEvent; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.rule.RuleDto; +import org.sonarqube.ws.Common; + +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.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.sonar.api.issue.DefaultTransitions.CONFIRM; +import static org.sonar.api.issue.DefaultTransitions.FALSE_POSITIVE; +import static org.sonar.api.issue.DefaultTransitions.REOPEN; +import static org.sonar.api.issue.DefaultTransitions.RESOLVE; +import static org.sonar.api.issue.DefaultTransitions.UNCONFIRM; +import static org.sonar.api.issue.DefaultTransitions.WONT_FIX; +import static org.sonar.api.rules.RuleType.CODE_SMELL; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonarqube.ws.Common.Severity.BLOCKER; +import static org.sonarqube.ws.Common.Severity.CRITICAL; +import static org.sonarqube.ws.Common.Severity.MAJOR; + +public class IssueChangeEventServiceImplTest { + + @Rule + public DbTester db = DbTester.create(); + + IssueChangeEventsDistributor eventsDistributor = mock(IssueChangeEventsDistributor.class); + + public final IssueChangeEventServiceImpl underTest = new IssueChangeEventServiceImpl(eventsDistributor); + + @Test + public void distributeIssueChangeEvent_singleIssueChange_severityChange() { + ComponentDto componentDto = db.components().insertPublicProject(); + ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get(); + BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get(); + RuleDto rule = db.rules().insert(); + IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name())); + + assertIssueDistribution(project, branch, issue, BLOCKER.name(), null, null, null, 1); + } + + @Test + public void distributeIssueChangeEvent_singleIssueChange_typeChange() { + ComponentDto componentDto = db.components().insertPublicProject(); + ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get(); + BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get(); + RuleDto rule = db.rules().insert(); + IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name())); + + assertIssueDistribution(project, branch, issue, null, Common.RuleType.BUG.name(), null, null, 1); + } + + @Test + public void distributeIssueChangeEvent_singleIssueChange_transitionChanges() { + ComponentDto componentDto = db.components().insertPublicProject(); + ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get(); + BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get(); + RuleDto rule = db.rules().insert(); + IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name())); + + assertIssueDistribution(project, branch, issue, null, null, WONT_FIX, true, 1); + assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 2); + assertIssueDistribution(project, branch, issue, null, null, FALSE_POSITIVE, true, 3); + assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 4); + assertIssueDistribution(project, branch, issue, null, null, RESOLVE, false, 5); + assertIssueDistribution(project, branch, issue, null, null, REOPEN, false, 6); + assertNoIssueDistribution(project, branch, issue, null, null, CONFIRM); + assertNoIssueDistribution(project, branch, issue, null, null, UNCONFIRM); + } + + @Test + public void distributeIssueChangeEvent_singleIssueChange_severalChanges() { + ComponentDto componentDto = db.components().insertPublicProject(); + ProjectDto project = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto.uuid()).get(); + BranchDto branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.getUuid()).get(); + RuleDto rule = db.rules().insert(); + IssueDto issue = db.issues().insert(rule, project, componentDto, i-> i.setSeverity(MAJOR.name())); + + assertIssueDistribution(project, branch, issue, BLOCKER.name(), Common.RuleType.BUG.name(), WONT_FIX, true, 1); + } + + @Test + public void distributeIssueChangeEvent_bulkIssueChange() { + RuleDto rule = db.rules().insert(); + + ComponentDto componentDto1 = db.components().insertPublicProject(); + ProjectDto project1 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto1.uuid()).get(); + BranchDto branch1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project1.getUuid()).get(); + IssueDto issue1 = db.issues().insert(rule, project1, componentDto1, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG)); + + ComponentDto componentDto2 = db.components().insertPublicProject(); + ProjectDto project2 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto2.uuid()).get(); + BranchDto branch2 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project2.getUuid()).get(); + IssueDto issue2 = db.issues().insert(rule, project2, componentDto2, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG)); + + ComponentDto componentDto3 = db.components().insertPublicProject(); + ProjectDto project3 = db.getDbClient().projectDao().selectByUuid(db.getSession(), componentDto3.uuid()).get(); + BranchDto branch3 = db.getDbClient().branchDao().selectByUuid(db.getSession(), project3.getUuid()).get(); + IssueDto issue3 = db.issues().insert(rule, project3, componentDto3, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG)); + + DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs() + .setDiff("resolution", null, null) + .setDiff("severity", MAJOR.name(), CRITICAL.name()) + .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name())); + DefaultIssue defaultIssue2 = issue2.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs() + .setDiff("resolution", "OPEN", "FALSE-POSITIVE") + .setDiff("severity", MAJOR.name(), CRITICAL.name()) + .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name())); + + Set issues = Set.of(defaultIssue1, defaultIssue2, issue3.toDefaultIssue()); + Map projectsByUuid = new HashMap<>(); + projectsByUuid.put(componentDto1.projectUuid(), componentDto1); + projectsByUuid.put(componentDto2.projectUuid(), componentDto2); + projectsByUuid.put(componentDto3.projectUuid(), componentDto3); + Map branchesByProjectUuid = new HashMap<>(); + branchesByProjectUuid.put(componentDto1.projectUuid(), branch1); + branchesByProjectUuid.put(componentDto2.projectUuid(), branch2); + branchesByProjectUuid.put(componentDto3.projectUuid(), branch3); + + underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class); + verify(eventsDistributor, times(2)).pushEvent(eventCaptor.capture()); + + List issueChangedEvents = eventCaptor.getAllValues(); + assertThat(issueChangedEvents).hasSize(2); + + assertThat(issueChangedEvents) + .extracting(IssueChangedEvent::getEvent, IssueChangedEvent::getProjectKey, + IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType, IssueChangedEvent::getResolved) + .containsExactlyInAnyOrder( + tuple("IssueChangedEvent", project1.getKey(), CRITICAL.name(), CODE_SMELL.name(), false), + tuple("IssueChangedEvent", project2.getKey(), CRITICAL.name(), CODE_SMELL.name(), true)); + } + + @Test + public void doNotDistributeIssueChangeEvent_forPullRequestIssues() { + RuleDto rule = db.rules().insert(); + + ComponentDto project = db.components().insertPublicProject(); + ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("myBranch1") + .setBranchType(BranchType.PULL_REQUEST) + .setMergeBranchUuid(project.uuid())); + BranchDto branch1 = db.getDbClient().branchDao().selectByUuid(db.getSession(), pullRequest.uuid()).get(); + ComponentDto file = db.components().insertComponent(newFileDto(pullRequest)); + IssueDto issue1 = db.issues().insert(rule, pullRequest, file, i-> i.setSeverity(MAJOR.name()).setType(RuleType.BUG)); + + DefaultIssue defaultIssue1 = issue1.toDefaultIssue().setCurrentChangeWithoutAddChange(new FieldDiffs() + .setDiff("resolution", null, null) + .setDiff("severity", MAJOR.name(), CRITICAL.name()) + .setDiff("type", RuleType.BUG.name(), CODE_SMELL.name())); + + Set issues = Set.of(defaultIssue1); + Map projectsByUuid = new HashMap<>(); + projectsByUuid.put(project.projectUuid(), project); + Map branchesByProjectUuid = new HashMap<>(); + branchesByProjectUuid.put(project.projectUuid(), branch1); + + underTest.distributeIssueChangeEvent(issues, projectsByUuid, branchesByProjectUuid); + + verifyNoInteractions(eventsDistributor); + } + + private void assertIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity, + @Nullable String type, @Nullable String transition, Boolean resolved, int times) { + underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey()); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class); + verify(eventsDistributor, times(times)).pushEvent(eventCaptor.capture()); + + IssueChangedEvent issueChangedEvent = eventCaptor.getValue(); + assertThat(issueChangedEvent).isNotNull(); + assertThat(issueChangedEvent).extracting(IssueChangedEvent::getEvent, IssueChangedEvent::getProjectKey, + IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType, IssueChangedEvent::getResolved) + .containsExactly("IssueChangedEvent", project.getKey(), severity, type, resolved); + } + + private void assertNoIssueDistribution(ProjectDto project, BranchDto branch, IssueDto issue, @Nullable String severity, + @Nullable String type, @Nullable String transition) { + underTest.distributeIssueChangeEvent(issue.toDefaultIssue(), severity, type, transition, branch, project.getKey()); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(IssueChangedEvent.class); + verifyNoMoreInteractions(eventsDistributor); + } + +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java new file mode 100644 index 00000000000..b3b9e0b84d9 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/issues/StandaloneIssueChangeEventsDistributorTest.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.issues; + +import org.junit.Test; +import org.sonar.core.util.issue.IssueChangeListener; +import org.sonar.core.util.issue.IssueChangedEvent; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class StandaloneIssueChangeEventsDistributorTest { + IssueChangeListener issueChangeListener = mock(IssueChangeListener.class); + IssueChangedEvent event = mock(IssueChangedEvent.class); + + public final StandaloneIssueChangeEventsDistributor underTest = new StandaloneIssueChangeEventsDistributor(); + + @Test + public void subscribe_and_push_publishesToListener() { + underTest.subscribe(issueChangeListener); + underTest.pushEvent(event); + verify(issueChangeListener).listen(event); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java new file mode 100644 index 00000000000..1b4c10e0fc9 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/QualityProfileChangeEventServiceImplTest.java @@ -0,0 +1,165 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.qualityprofile; + +import java.util.Collection; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.rule.RuleKey; +import org.sonar.core.util.ParamChange; +import org.sonar.core.util.rule.RuleChange; +import org.sonar.core.util.rule.RuleSetChangedEvent; +import org.sonar.db.DbTester; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.qualityprofile.ActiveRuleDto; +import org.sonar.db.qualityprofile.ActiveRuleParamDto; +import org.sonar.db.qualityprofile.QProfileDto; +import org.sonar.db.qualityprofile.QualityProfileTesting; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.rule.RuleParamDto; +import org.sonar.server.qualityprofile.ActiveRuleChange; + +import static java.util.List.of; +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.mockito.Mockito.verify; +import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; +import static org.sonar.db.rule.RuleTesting.newCustomRule; +import static org.sonar.db.rule.RuleTesting.newTemplateRule; +import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED; + +public class QualityProfileChangeEventServiceImplTest { + + @Rule + public DbTester db = DbTester.create(); + + RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class); + + public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor); + + @Test + public void distributeRuleChangeEvent() { + QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto(); + + // Template rule + RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key")); + db.rules().insert(templateRule); + // Custom rule + RuleDto rule1 = newCustomRule(templateRule) + .setLanguage("xoo") + .setRepositoryKey("repo") + .setRuleKey("ruleKey") + .setDescriptionFormat(RuleDto.Format.MARKDOWN) + .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "
line1\nline2
")); + db.rules().insert(rule1); + + ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1); + + ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1); + activeRuleChange.setParameter("paramChangeKey", "paramChangeValue"); + + Collection profiles = Collections.singleton(qualityProfileDto); + + ProjectDto project = db.components().insertPrivateProjectDto(); + db.qualityProfiles().associateWithProject(project, qualityProfileDto); + + underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class); + verify(eventsDistributor).pushEvent(eventCaptor.capture()); + + RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue(); + assertThat(ruleSetChangedEvent).isNotNull(); + assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent, + RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects) + .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()}); + + assertThat(ruleSetChangedEvent.getActivatedRules()) + .extracting(RuleChange::getKey, RuleChange::getLanguage, + RuleChange::getSeverity, RuleChange::getTemplateKey) + .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key")); + + assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1); + ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0]; + assertThat(actualParamChange) + .extracting(ParamChange::getKey, ParamChange::getValue) + .containsExactly("paramChangeKey", "paramChangeValue"); + + assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty(); + + } + + @Test + public void publishRuleActivationToSonarLintClients() { + ProjectDto projectDao = new ProjectDto(); + QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto(); + activatedQualityProfile.setLanguage("xoo"); + db.qualityProfiles().insert(activatedQualityProfile); + RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey")); + RuleParamDto rule1Param = db.rules().insertRuleParam(rule1); + + ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1); + ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20)); + db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1); + db.getSession().commit(); + + QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto(); + db.qualityProfiles().insert(deactivatedQualityProfile); + RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2")); + RuleParamDto rule2Param = db.rules().insertRuleParam(rule2); + + ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2); + ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20)); + db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2); + db.getSession().commit(); + + underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class); + verify(eventsDistributor).pushEvent(eventCaptor.capture()); + + RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue(); + assertThat(ruleSetChangedEvent).isNotNull(); + assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent, + RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects) + .containsExactly("RuleSetChanged", "xoo", new String[]{null}); + + // activated rule + assertThat(ruleSetChangedEvent.getActivatedRules()) + .extracting(RuleChange::getKey, RuleChange::getLanguage, + RuleChange::getSeverity, RuleChange::getTemplateKey) + .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null)); + + assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1); + ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0]; + assertThat(actualParamChange) + .extracting(ParamChange::getKey, ParamChange::getValue) + .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue()); + + // deactivated rule + assertThat(ruleSetChangedEvent.getDeactivatedRules()) + .containsExactly("repo2:ruleKey2"); + } + +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java new file mode 100644 index 00000000000..e36a5df6fc5 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/qualityprofile/RuleSetChangeBroadcastUtilsTest.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pushapi.qualityprofile; + +import java.util.Set; +import java.util.function.Predicate; +import javax.servlet.AsyncContext; +import org.junit.Test; +import org.sonar.api.rule.Severity; +import org.sonar.core.util.ParamChange; +import org.sonar.core.util.rule.RuleChange; +import org.sonar.core.util.rule.RuleSetChangedEvent; +import org.sonar.server.pushapi.sonarlint.SonarLintClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class RuleSetChangeBroadcastUtilsTest { + + private final static String JAVA_KEY = "java"; + private final static String PROJECT_KEY_1 = "projectKey1"; + private final static String PROJECT_KEY_2 = "projectKey2"; + private final static String USER_UUID = "userUUID"; + private final static String[] DEACTIVATED_RULES = {"repo2:rule-key2"}; + + private final static Set EXAMPLE_KEYS = Set.of(PROJECT_KEY_1, PROJECT_KEY_2); + + private final AsyncContext asyncContext = mock(AsyncContext.class); + + @Test + public void getsFilterForEvent() { + RuleChange javaRule = new RuleChange(); + javaRule.setLanguage(JAVA_KEY); + javaRule.setParams(new ParamChange[]{new ParamChange("param-key", "param-value")}); + javaRule.setTemplateKey("repo:template-key"); + javaRule.setSeverity(Severity.CRITICAL); + javaRule.setKey("repo:rule-key"); + + RuleChange[] activatedRules = {javaRule}; + RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(EXAMPLE_KEYS.toArray(String[]::new), activatedRules, + DEACTIVATED_RULES, JAVA_KEY); + Predicate predicate = RuleSetChangeBroadcastUtils.getFilterForEvent(ruleSetChangedEvent); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_1), Set.of(JAVA_KEY), USER_UUID))).isTrue(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_2), Set.of(JAVA_KEY), USER_UUID))).isTrue(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(PROJECT_KEY_1), Set.of(), USER_UUID))).isFalse(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(), Set.of(JAVA_KEY), USER_UUID))).isFalse(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of(), USER_UUID))).isFalse(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of(""), Set.of("another-language"), USER_UUID))).isFalse(); + assertThat(predicate.test(new SonarLintClient(asyncContext, Set.of("another-project"), Set.of("another-language"), USER_UUID))).isFalse(); + } + + @Test + public void getsMessageForEvent() { + RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(new String[]{PROJECT_KEY_1}, new RuleChange[0], + DEACTIVATED_RULES, JAVA_KEY); + + String message = RuleSetChangeBroadcastUtils.getMessage(ruleSetChangedEvent); + + assertThat(message).isEqualTo("event: RuleSetChanged\n" + + "data: {\"activatedRules\":[]," + + "\"projects\":[\"" + PROJECT_KEY_1 + "\"]," + + "\"deactivatedRules\":[\"repo2:rule-key2\"]}"); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java index 2147bf50ecf..48cbb218411 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java @@ -28,10 +28,13 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.rule.Severity; +import org.sonar.core.util.issue.Issue; +import org.sonar.core.util.issue.IssueChangedEvent; import org.sonar.core.util.ParamChange; -import org.sonar.core.util.RuleChange; -import org.sonar.core.util.RuleSetChangedEvent; +import org.sonar.core.util.rule.RuleChange; +import org.sonar.core.util.rule.RuleSetChangedEvent; import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.pushapi.issues.StandaloneIssueChangeEventsDistributor; import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor; import static org.assertj.core.api.Assertions.assertThat; @@ -58,13 +61,14 @@ public class SonarLintClientsRegistryTest { private final ServletOutputStream outputStream = mock(ServletOutputStream.class); private final SonarLintClientPermissionsValidator permissionsValidator = mock(SonarLintClientPermissionsValidator.class); - private final StandaloneRuleActivatorEventsDistributor eventsDistributor = mock(StandaloneRuleActivatorEventsDistributor.class); + private final StandaloneRuleActivatorEventsDistributor ruleEventsDistributor = mock(StandaloneRuleActivatorEventsDistributor.class); + private final StandaloneIssueChangeEventsDistributor issueChangeEventsDistributor = mock(StandaloneIssueChangeEventsDistributor.class); private SonarLintClientsRegistry underTest; @Before public void before() { - underTest = new SonarLintClientsRegistry(eventsDistributor, permissionsValidator); + underTest = new SonarLintClientsRegistry(issueChangeEventsDistributor, ruleEventsDistributor, permissionsValidator); } @Test @@ -154,7 +158,7 @@ public class SonarLintClientsRegistryTest { } @Test - public void listen_givenUserNotPermittedToReceiveEvent_closeConnection() { + public void listen_givenUserNotPermittedToReceiveRuleSetChangedEvent_closeConnection() { RuleChange[] activatedRules = {}; String[] deactivatedRules = {"repo:rule-key"}; RuleSetChangedEvent ruleSetChangedEvent = new RuleSetChangedEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules, "java"); @@ -168,6 +172,20 @@ public class SonarLintClientsRegistryTest { verify(sonarLintClient).close(); } + @Test + public void listen_givenUserNotPermittedToReceiveIssueChangeEvent_closeConnection() { + Issue[] issues = new Issue[]{ new Issue("issue-1", "branch-1")}; + IssueChangedEvent issueChangedEvent = new IssueChangedEvent("project1", issues, true, "BLOCKER", "BUG"); + + SonarLintClient sonarLintClient = createSampleSLClient(); + underTest.registerClient(sonarLintClient); + doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet()); + + underTest.listen(issueChangedEvent); + + verify(sonarLintClient).close(); + } + @Test public void listen_givenUnregisteredClient_closeConnection() throws IOException { RuleChange[] activatedRules = {}; @@ -192,26 +210,39 @@ public class SonarLintClientsRegistryTest { public void registerClient_whenCalledFirstTime_registerAlsoToListenToEvents() { underTest.registerClient(createSampleSLClient()); - verify(eventsDistributor).subscribe(underTest); + verify(ruleEventsDistributor).subscribe(underTest); + verify(issueChangeEventsDistributor).subscribe(underTest); } @Test public void registerClient_whenCalledSecondTime_doNotRegisterToEvents() { underTest.registerClient(createSampleSLClient()); - clearInvocations(eventsDistributor); + clearInvocations(ruleEventsDistributor); + clearInvocations(issueChangeEventsDistributor); + + underTest.registerClient(createSampleSLClient()); + verifyNoInteractions(ruleEventsDistributor); + verifyNoInteractions(issueChangeEventsDistributor); + } + + @Test + public void registerClient_whenExceptionAndCalledSecondTime_registerToRuleEvents() { + doThrow(new RuntimeException()).when(ruleEventsDistributor).subscribe(any()); + underTest.registerClient(createSampleSLClient()); + clearInvocations(ruleEventsDistributor); underTest.registerClient(createSampleSLClient()); - verifyNoInteractions(eventsDistributor); + verify(ruleEventsDistributor).subscribe(underTest); } @Test - public void registerClient_whenExceptionAndCalledSecondTime_registerToEvents() { - doThrow(new RuntimeException()).when(eventsDistributor).subscribe(any()); + public void registerClient_whenExceptionAndCalledSecondTime_registerToIssueChangeEvents() { + doThrow(new RuntimeException()).when(issueChangeEventsDistributor).subscribe(any()); underTest.registerClient(createSampleSLClient()); - clearInvocations(eventsDistributor); + clearInvocations(issueChangeEventsDistributor); underTest.registerClient(createSampleSLClient()); - verify(eventsDistributor).subscribe(underTest); + verify(issueChangeEventsDistributor).subscribe(underTest); } private SonarLintClient createSampleSLClient() { diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java deleted file mode 100644 index a67e8ded258..00000000000 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/qualityprofile/builtin/QualityProfileChangeEventServiceImplTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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.qualityprofile.builtin; - -import java.util.Collection; -import java.util.Collections; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.sonar.api.rule.RuleKey; -import org.sonar.core.util.ParamChange; -import org.sonar.core.util.RuleChange; -import org.sonar.core.util.RuleSetChangedEvent; -import org.sonar.db.DbTester; -import org.sonar.db.project.ProjectDto; -import org.sonar.db.qualityprofile.ActiveRuleDto; -import org.sonar.db.qualityprofile.ActiveRuleParamDto; -import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.db.qualityprofile.QualityProfileTesting; -import org.sonar.db.rule.RuleDto; -import org.sonar.db.rule.RuleParamDto; -import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl; -import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor; -import org.sonar.server.qualityprofile.ActiveRuleChange; - -import static java.util.List.of; -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.mockito.Mockito.verify; -import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; -import static org.sonar.db.rule.RuleTesting.newCustomRule; -import static org.sonar.db.rule.RuleTesting.newTemplateRule; -import static org.sonar.server.qualityprofile.ActiveRuleChange.Type.ACTIVATED; - -public class QualityProfileChangeEventServiceImplTest { - - @Rule - public DbTester db = DbTester.create(); - - RuleActivatorEventsDistributor eventsDistributor = mock(RuleActivatorEventsDistributor.class); - - public final QualityProfileChangeEventServiceImpl underTest = new QualityProfileChangeEventServiceImpl(db.getDbClient(), eventsDistributor); - - @Test - public void distributeRuleChangeEvent() { - QProfileDto qualityProfileDto = QualityProfileTesting.newQualityProfileDto(); - - // Template rule - RuleDto templateRule = newTemplateRule(RuleKey.of("xoo", "template-key")); - db.rules().insert(templateRule); - // Custom rule - RuleDto rule1 = newCustomRule(templateRule) - .setLanguage("xoo") - .setRepositoryKey("repo") - .setRuleKey("ruleKey") - .setDescriptionFormat(RuleDto.Format.MARKDOWN) - .addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", "
line1\nline2
")); - db.rules().insert(rule1); - - ActiveRuleDto activeRuleDto = ActiveRuleDto.createFor(qualityProfileDto, rule1); - - ActiveRuleChange activeRuleChange = new ActiveRuleChange(ACTIVATED, activeRuleDto, rule1); - activeRuleChange.setParameter("paramChangeKey", "paramChangeValue"); - - Collection profiles = Collections.singleton(qualityProfileDto); - - ProjectDto project = db.components().insertPrivateProjectDto(); - db.qualityProfiles().associateWithProject(project, qualityProfileDto); - - underTest.distributeRuleChangeEvent(profiles, of(activeRuleChange), "xoo"); - - ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class); - verify(eventsDistributor).pushEvent(eventCaptor.capture()); - - RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue(); - assertThat(ruleSetChangedEvent).isNotNull(); - assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent, - RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects) - .containsExactly("RuleSetChanged", "xoo", new String[]{project.getKey()}); - - assertThat(ruleSetChangedEvent.getActivatedRules()) - .extracting(RuleChange::getKey, RuleChange::getLanguage, - RuleChange::getSeverity, RuleChange::getTemplateKey) - .containsExactly(tuple("repo:ruleKey", "xoo", null, "xoo:template-key")); - - assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1); - ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0]; - assertThat(actualParamChange) - .extracting(ParamChange::getKey, ParamChange::getValue) - .containsExactly("paramChangeKey", "paramChangeValue"); - - assertThat(ruleSetChangedEvent.getDeactivatedRules()).isEmpty(); - - } - - @Test - public void publishRuleActivationToSonarLintClients() { - ProjectDto projectDao = new ProjectDto(); - QProfileDto activatedQualityProfile = QualityProfileTesting.newQualityProfileDto(); - activatedQualityProfile.setLanguage("xoo"); - db.qualityProfiles().insert(activatedQualityProfile); - RuleDto rule1 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo").setRuleKey("ruleKey")); - RuleParamDto rule1Param = db.rules().insertRuleParam(rule1); - - ActiveRuleDto activeRule1 = db.qualityProfiles().activateRule(activatedQualityProfile, rule1); - ActiveRuleParamDto activeRuleParam1 = ActiveRuleParamDto.createFor(rule1Param).setValue(randomAlphanumeric(20)); - db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule1, activeRuleParam1); - db.getSession().commit(); - - QProfileDto deactivatedQualityProfile = QualityProfileTesting.newQualityProfileDto(); - db.qualityProfiles().insert(deactivatedQualityProfile); - RuleDto rule2 = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("repo2").setRuleKey("ruleKey2")); - RuleParamDto rule2Param = db.rules().insertRuleParam(rule2); - - ActiveRuleDto activeRule2 = db.qualityProfiles().activateRule(deactivatedQualityProfile, rule2); - ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(rule2Param).setValue(randomAlphanumeric(20)); - db.getDbClient().activeRuleDao().insertParam(db.getSession(), activeRule2, activeRuleParam2); - db.getSession().commit(); - - underTest.publishRuleActivationToSonarLintClients(projectDao, activatedQualityProfile, deactivatedQualityProfile); - - ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(RuleSetChangedEvent.class); - verify(eventsDistributor).pushEvent(eventCaptor.capture()); - - RuleSetChangedEvent ruleSetChangedEvent = eventCaptor.getValue(); - assertThat(ruleSetChangedEvent).isNotNull(); - assertThat(ruleSetChangedEvent).extracting(RuleSetChangedEvent::getEvent, - RuleSetChangedEvent::getLanguage, RuleSetChangedEvent::getProjects) - .containsExactly("RuleSetChanged", "xoo", new String[]{null}); - - // activated rule - assertThat(ruleSetChangedEvent.getActivatedRules()) - .extracting(RuleChange::getKey, RuleChange::getLanguage, - RuleChange::getSeverity, RuleChange::getTemplateKey) - .containsExactly(tuple("repo:ruleKey", "xoo", rule1.getSeverityString(), null)); - - assertThat(ruleSetChangedEvent.getActivatedRules()[0].getParams()).hasSize(1); - ParamChange actualParamChange = ruleSetChangedEvent.getActivatedRules()[0].getParams()[0]; - assertThat(actualParamChange) - .extracting(ParamChange::getKey, ParamChange::getValue) - .containsExactly(activeRuleParam1.getKey(), activeRuleParam1.getValue()); - - // deactivated rule - assertThat(ruleSetChangedEvent.getDeactivatedRules()) - .containsExactly("repo2:ruleKey2"); - } - -} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java index 9a170755d84..c45a8e54d47 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java @@ -70,6 +70,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Issues; @@ -117,6 +118,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SET_TYPE; public class BulkChangeAction implements IssuesWsAction { private static final Logger LOG = Loggers.get(BulkChangeAction.class); + private static final List ACTIONS_TO_DISTRIBUTE = List.of(SET_SEVERITY_KEY, SET_TYPE_KEY, DO_TRANSITION_KEY); private final System2 system2; private final UserSession userSession; @@ -126,10 +128,12 @@ public class BulkChangeAction implements IssuesWsAction { private final List actions; private final IssueChangePostProcessor issueChangePostProcessor; private final IssuesChangesNotificationSerializer notificationSerializer; + private final IssueChangeEventService issueChangeEventService; public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, WebIssueStorage issueStorage, - NotificationManager notificationService, List actions, - IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer) { + NotificationManager notificationService, List actions, + IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer, + IssueChangeEventService issueChangeEventService) { this.system2 = system2; this.userSession = userSession; this.dbClient = dbClient; @@ -138,6 +142,7 @@ public class BulkChangeAction implements IssuesWsAction { this.actions = actions; this.issueChangePostProcessor = issueChangePostProcessor; this.notificationSerializer = notificationSerializer; + this.issueChangeEventService = issueChangeEventService; } @Override @@ -217,6 +222,7 @@ public class BulkChangeAction implements IssuesWsAction { UserDto author = dbClient.userDao().selectByUuid(dbSession, authorUuid); checkState(author != null, "User with uuid '%s' does not exist"); sendNotification(items, bulkChangeData, userDtoByUuid, author); + distributeEvents(items, bulkChangeData); return result; } @@ -285,6 +291,28 @@ public class BulkChangeAction implements IssuesWsAction { notificationService.scheduleForSending(notificationSerializer.serialize(builder)); } + private void distributeEvents(Collection issues, BulkChangeData bulkChangeData) { + boolean anyActionToDistribute = bulkChangeData.availableActions + .stream() + .anyMatch(a -> ACTIONS_TO_DISTRIBUTE.contains(a.key())); + + if (!anyActionToDistribute) { + return; + } + + Set changedIssues = issues.stream() + // should not happen but filter it out anyway to avoid NPE in oldestUpdateDate call below + .filter(issue -> issue.updateDate() != null) + .filter(Objects::nonNull) + .collect(toSet(issues.size())); + + if (changedIssues.isEmpty()) { + return; + } + + issueChangeEventService.distributeIssueChangeEvent(issues, bulkChangeData.projectsByUuid, bulkChangeData.branchesByProjectUuid); + } + @CheckForNull private ChangedIssue toNotification(BulkChangeData bulkChangeData, Map userDtoByUuid, DefaultIssue issue) { BranchDto branchDto = bulkChangeData.branchesByProjectUuid.get(issue.projectUuid()); @@ -366,7 +394,7 @@ public class BulkChangeAction implements IssuesWsAction { issues.stream().map(DefaultIssue::componentUuid).collect(MoreCollectors.toSet())).stream() .collect(uniqueIndex(ComponentDto::uuid, identity())); this.rulesByKey = dbClient.ruleDao().selectByKeys(dbSession, - issues.stream().map(DefaultIssue::ruleKey).collect(MoreCollectors.toSet())).stream() + issues.stream().map(DefaultIssue::ruleKey).collect(MoreCollectors.toSet())).stream() .collect(uniqueIndex(RuleDto::getKey, identity())); this.availableActions = actions.stream() diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java index cfd1f718017..2776f28ca5e 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java @@ -32,9 +32,11 @@ import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.util.Uuids; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; import org.sonar.db.issue.IssueDto; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.TransitionService; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.user.UserSession; import static java.lang.String.format; @@ -42,6 +44,7 @@ import static org.sonar.api.issue.DefaultTransitions.OPEN_AS_VULNERABILITY; import static org.sonar.api.issue.DefaultTransitions.RESET_AS_TO_REVIEW; import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_REVIEWED; import static org.sonar.api.issue.DefaultTransitions.SET_AS_IN_REVIEW; +import static org.sonar.db.component.BranchType.BRANCH; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION; @@ -50,16 +53,19 @@ public class DoTransitionAction implements IssuesWsAction { private final DbClient dbClient; private final UserSession userSession; + private final IssueChangeEventService issueChangeEventService; private final IssueFinder issueFinder; private final IssueUpdater issueUpdater; private final TransitionService transitionService; private final OperationResponseWriter responseWriter; private final System2 system2; - public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService, + public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueChangeEventService issueChangeEventService, + IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService, OperationResponseWriter responseWriter, System2 system2) { this.dbClient = dbClient; this.userSession = userSession; + this.issueChangeEventService = issueChangeEventService; this.issueFinder = issueFinder; this.issueUpdater = issueUpdater; this.transitionService = transitionService; @@ -110,7 +116,14 @@ public class DoTransitionAction implements IssuesWsAction { IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); transitionService.checkTransitionPermission(transitionKey, defaultIssue); if (transitionService.doTransition(defaultIssue, context, transitionKey)) { - return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, true); + BranchDto branch = issueUpdater.getBranch(session, defaultIssue, defaultIssue.projectUuid()); + SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, true, branch); + + if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(defaultIssue.projectUuid()) != null) { + issueChangeEventService.distributeIssueChangeEvent(defaultIssue, null, null, transitionKey, branch, + response.getComponentByUuid(defaultIssue.projectUuid()).getKey()); + } + return response; } return new SearchResponseData(issueDto); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java index dee115bf149..494c9bd4828 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java @@ -70,10 +70,15 @@ public class IssueUpdater { public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, boolean refreshMeasures) { + BranchDto branch = getBranch(dbSession, issue, issue.projectUuid()); + return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, refreshMeasures, branch); + } + + public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, + IssueChangeContext context, boolean refreshMeasures, BranchDto branch) { Optional rule = getRuleByKey(dbSession, issue.getRuleKey()); ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid()); - BranchDto branch = getBranch(dbSession, issue, issue.projectUuid()); ComponentDto component = getComponent(dbSession, issue, issue.componentUuid()); IssueDto issueDto = doSaveIssue(dbSession, issue, context, rule.orElse(null), project, branch); @@ -90,6 +95,14 @@ public class IssueUpdater { return result; } + protected BranchDto getBranch(DbSession dbSession, DefaultIssue issue, @Nullable String projectUuid) { + String issueKey = issue.key(); + checkState(projectUuid != null, "Issue '%s' has no project", issueKey); + BranchDto component = dbClient.branchDao().selectByUuid(dbSession, projectUuid).orElse(null); + checkState(component != null, "Branch uuid '%s' for issue key '%s' cannot be found", projectUuid, issueKey); + return component; + } + private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable RuleDto ruleDto, ComponentDto project, BranchDto branchDto) { IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next(); @@ -137,14 +150,6 @@ public class IssueUpdater { return component; } - private BranchDto getBranch(DbSession dbSession, DefaultIssue issue, @Nullable String projectUuid) { - String issueKey = issue.key(); - checkState(projectUuid != null, "Issue '%s' has no project", issueKey); - BranchDto component = dbClient.branchDao().selectByUuid(dbSession, projectUuid).orElse(null); - checkState(component != null, "Branch uuid '%s' for issue key '%s' cannot be found", projectUuid, issueKey); - return component; - } - private Optional getRuleByKey(DbSession session, RuleKey ruleKey) { Optional rule = dbClient.ruleDao().selectByKey(session, ruleKey); return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java index 1b998ed234c..0150ad31962 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java @@ -31,12 +31,15 @@ import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.util.Uuids; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; import org.sonar.db.issue.IssueDto; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.user.UserSession; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; +import static org.sonar.db.component.BranchType.BRANCH; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_SEVERITY; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITY; @@ -45,15 +48,18 @@ public class SetSeverityAction implements IssuesWsAction { private final UserSession userSession; private final DbClient dbClient; + private final IssueChangeEventService issueChangeEventService; private final IssueFinder issueFinder; private final IssueFieldsSetter issueFieldsSetter; private final IssueUpdater issueUpdater; private final OperationResponseWriter responseWriter; - public SetSeverityAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, + public SetSeverityAction(UserSession userSession, DbClient dbClient, IssueChangeEventService issueChangeEventService, + IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, OperationResponseWriter responseWriter) { this.userSession = userSession; this.dbClient = dbClient; + this.issueChangeEventService = issueChangeEventService; this.issueFinder = issueFinder; this.issueFieldsSetter = issueFieldsSetter; this.issueUpdater = issueUpdater; @@ -106,7 +112,14 @@ public class SetSeverityAction implements IssuesWsAction { IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getUuid()); if (issueFieldsSetter.setManualSeverity(issue, severity, context)) { - return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true); + BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid()); + SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true, branch); + + if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) { + issueChangeEventService.distributeIssueChangeEvent(issue, severity, null, null, + branch, response.getComponentByUuid(issue.projectUuid()).getKey()); + } + return response; } return new SearchResponseData(issueDto); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java index 3e08765ee6a..aee2c8a7fb4 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java @@ -33,12 +33,15 @@ import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.util.Uuids; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; import org.sonar.db.issue.IssueDto; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.user.UserSession; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; +import static org.sonar.db.component.BranchType.BRANCH; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPE; @@ -48,16 +51,18 @@ public class SetTypeAction implements IssuesWsAction { private final UserSession userSession; private final DbClient dbClient; + private final IssueChangeEventService issueChangeEventService; private final IssueFinder issueFinder; private final IssueFieldsSetter issueFieldsSetter; private final IssueUpdater issueUpdater; private final OperationResponseWriter responseWriter; private final System2 system2; - public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, - OperationResponseWriter responseWriter, System2 system2) { + public SetTypeAction(UserSession userSession, DbClient dbClient, IssueChangeEventService issueChangeEventService, IssueFinder issueFinder, + IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, OperationResponseWriter responseWriter, System2 system2) { this.userSession = userSession; this.dbClient = dbClient; + this.issueChangeEventService = issueChangeEventService; this.issueFinder = issueFinder; this.issueFieldsSetter = issueFieldsSetter; this.issueUpdater = issueUpdater; @@ -112,7 +117,13 @@ public class SetTypeAction implements IssuesWsAction { IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); if (issueFieldsSetter.setType(issue, ruleType, context)) { - return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true); + BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid()); + SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true, branch); + if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) { + issueChangeEventService.distributeIssueChangeEvent(issue, null, ruleType.name(), null, branch, + response.getComponentByUuid(issue.projectUuid()).getKey()); + } + return response; } return new SearchResponseData(issueDto); } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java index 2d671139ae4..d9115d8fd0c 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/TransitionActionTest.java @@ -51,7 +51,6 @@ import static org.sonar.db.rule.RuleTesting.newRule; public class TransitionActionTest { - @Rule public UserSessionRule userSession = UserSessionRule.standalone(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java index 9797f60a958..f4ff321ce5b 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java @@ -59,6 +59,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.RuleDescriptionFormatter; import org.sonar.server.tester.UserSessionRule; @@ -75,6 +76,7 @@ import static java.util.Optional.ofNullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -113,6 +115,7 @@ public class BulkChangeActionTest { private DbClient dbClient = db.getDbClient(); + private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class); private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter); private WebIssueStorage issueStorage = new WebIssueStorage(system2, dbClient, @@ -125,7 +128,7 @@ public class BulkChangeActionTest { private List actions = new ArrayList<>(); private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, - issueChangePostProcessor, issuesChangesSerializer)); + issueChangePostProcessor, issuesChangesSerializer, issueChangeEventService)); @Before public void setUp() { @@ -155,6 +158,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); verifyPostProcessorCalled(file); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any()); } @Test @@ -179,6 +183,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); verifyPostProcessorCalled(file); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any()); } @Test @@ -204,6 +209,7 @@ public class BulkChangeActionTest { // no need to refresh measures verifyPostProcessorNotCalled(); + verifyNoInteractions(issueChangeEventService); } @Test @@ -230,6 +236,7 @@ public class BulkChangeActionTest { // no need to refresh measures verifyPostProcessorNotCalled(); + verifyNoInteractions(issueChangeEventService); } @Test @@ -255,6 +262,7 @@ public class BulkChangeActionTest { assertThat(issueComment.getChangeData()).isEqualTo("type was badly defined"); verifyPostProcessorCalled(file); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any()); } @Test @@ -290,6 +298,7 @@ public class BulkChangeActionTest { tuple(issue3.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW)); verifyPostProcessorCalled(file); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any()); } @Test diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java index 8fefd3eaa06..babaff80e60 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java @@ -32,6 +32,7 @@ import org.sonar.api.utils.System2; import org.sonar.core.util.SequenceUuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.rule.RuleDto; @@ -50,6 +51,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.RuleDescriptionFormatter; import org.sonar.server.tester.UserSessionRule; @@ -64,12 +66,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; import static org.sonar.api.issue.Issue.STATUS_OPEN; +import static org.sonar.api.rule.Severity.MAJOR; import static org.sonar.api.rules.RuleType.CODE_SMELL; import static org.sonar.api.web.UserRole.CODEVIEWER; import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.issue.IssueTesting.newIssue; public class DoTransitionActionTest { @@ -77,7 +82,6 @@ public class DoTransitionActionTest { private System2 system2 = new TestSystem2().setNow(NOW); - @Rule public DbTester db = DbTester.create(system2); @@ -89,6 +93,7 @@ public class DoTransitionActionTest { private DbClient dbClient = db.getDbClient(); + private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class); private IssueFieldsSetter updater = new IssueFieldsSetter(); private IssueWorkflow workflow = new IssueWorkflow(new FunctionExecutor(updater), updater); private TransitionService transitionService = new TransitionService(userSession, workflow); @@ -101,7 +106,8 @@ public class DoTransitionActionTest { mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); - private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2); + private WsAction underTest = new DoTransitionAction(dbClient, userSession, issueChangeEventService, + new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2); private WsActionTester tester = new WsActionTester(underTest); @Before @@ -121,11 +127,31 @@ public class DoTransitionActionTest { verify(responseWriter).write(eq(issue.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class)); verifyContentOfPreloadedSearchResponseData(issue); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any()); IssueDto issueReloaded = db.getDbClient().issueDao().selectByKey(db.getSession(), issue.getKey()).get(); assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED); assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(file); } + @Test + public void do_transition_is_not_distributed_for_pull_request() { + RuleDto rule = db.rules().insertIssueRule(); + ComponentDto project = db.components().insertPrivateProject(); + + ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("myBranch1") + .setBranchType(BranchType.PULL_REQUEST) + .setMergeBranchUuid(project.uuid())); + + ComponentDto file = db.components().insertComponent(newFileDto(pullRequest)); + IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR); + db.issues().insertIssue(issue); + userSession.logIn(db.users().insertUser()).addProjectPermission(USER, pullRequest, file); + + call(issue.getKey(), "confirm"); + + verifyNoInteractions(issueChangeEventService); + } + @Test public void fail_if_external_issue() { ComponentDto project = db.components().insertPrivateProject(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java index e334d80a7a3..565fb7fd3e8 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueUpdaterTest.java @@ -68,7 +68,6 @@ public class IssueUpdaterTest { private System2 system2 = mock(System2.class); - @Rule public DbTester db = DbTester.create(system2); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java index 3936efae996..fc7c9791577 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java @@ -32,6 +32,7 @@ import org.sonar.core.issue.FieldDiffs; import org.sonar.core.util.SequenceUuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDbTester; import org.sonar.db.issue.IssueDto; @@ -49,6 +50,7 @@ import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.RuleDescriptionFormatter; import org.sonar.server.tester.UserSessionRule; @@ -63,10 +65,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.sonar.api.rule.Severity.MAJOR; import static org.sonar.api.rule.Severity.MINOR; +import static org.sonar.api.rules.RuleType.CODE_SMELL; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; import static org.sonar.api.web.UserRole.USER; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.issue.IssueTesting.newIssue; public class SetSeverityActionTest { @@ -84,10 +90,12 @@ public class SetSeverityActionTest { private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); + private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null); private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); - private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), + private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, issueChangeEventService, + new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), new IssueUpdater(dbClient, new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, mock(RuleDescriptionFormatter.class)), issueIndexer, new SequenceUuidFactory()), mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer), @@ -102,6 +110,7 @@ public class SetSeverityActionTest { verify(responseWriter).write(eq(issueDto.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class)); verifyContentOfPreloadedSearchResponseData(issueDto); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any()); IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getSeverity()).isEqualTo(MINOR); @@ -111,6 +120,26 @@ public class SetSeverityActionTest { .containsExactlyInAnyOrder(issueDto.getComponentUuid()); } + @Test + public void set_severity_is_not_distributed_for_pull_request() { + RuleDto rule = dbTester.rules().insertIssueRule(); + ComponentDto project = dbTester.components().insertPrivateProject(); + + ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1") + .setBranchType(BranchType.PULL_REQUEST) + .setMergeBranchUuid(project.uuid())); + + ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest)); + IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR); + issueDbTester.insertIssue(issue); + + setUserWithBrowseAndAdministerIssuePermission(issue); + + call(issue.getKey(), MINOR); + + verifyNoInteractions(issueChangeEventService); + } + @Test public void insert_entry_in_changelog_when_setting_severity() { IssueDto issueDto = issueDbTester.insertIssue(i -> i.setSeverity(MAJOR)); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java index d4b0cfb5cab..979fd7067dc 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java @@ -41,6 +41,7 @@ import org.sonar.core.issue.FieldDiffs; import org.sonar.core.util.SequenceUuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDbTester; import org.sonar.db.issue.IssueDto; @@ -57,6 +58,7 @@ import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; import org.sonar.server.notification.NotificationManager; +import org.sonar.server.pushapi.issues.IssueChangeEventService; import org.sonar.server.rule.DefaultRuleFinder; import org.sonar.server.rule.RuleDescriptionFormatter; import org.sonar.server.tester.UserSessionRule; @@ -71,13 +73,16 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import static org.sonar.api.rule.Severity.MAJOR; import static org.sonar.api.rules.RuleType.BUG; import static org.sonar.api.rules.RuleType.CODE_SMELL; import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.issue.IssueTesting.newIssue; @RunWith(DataProviderRunner.class) public class SetTypeActionTest { @@ -97,10 +102,12 @@ public class SetTypeActionTest { private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); + private IssueChangeEventService issueChangeEventService = mock(IssueChangeEventService.class); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null); private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); - private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), + private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, issueChangeEventService, + new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), new IssueUpdater(dbClient, new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, mock(RuleDescriptionFormatter.class)), issueIndexer, new SequenceUuidFactory()), mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer), responseWriter, system2)); @@ -124,12 +131,33 @@ public class SetTypeActionTest { assertThat(issueChangePostProcessor.calledComponents()) .extracting(ComponentDto::uuid) .containsExactlyInAnyOrder(issueDto.getComponentUuid()); + verify(issueChangeEventService).distributeIssueChangeEvent(any(), any(), any(), any(), any(), any()); } else { assertThat(issueChangePostProcessor.wasCalled()) .isFalse(); } } + @Test + public void set_type_is_not_distributed_for_pull_request() { + RuleDto rule = dbTester.rules().insertIssueRule(); + ComponentDto project = dbTester.components().insertPrivateProject(); + + ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, b -> b.setKey("myBranch1") + .setBranchType(BranchType.PULL_REQUEST) + .setMergeBranchUuid(project.uuid())); + + ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest)); + IssueDto issue = newIssue(rule, pullRequest, file).setType(CODE_SMELL).setSeverity(MAJOR); + issueDbTester.insertIssue(issue); + + setUserWithBrowseAndAdministerIssuePermission(issue); + + call(issue.getKey(), BUG.name()); + + verifyNoInteractions(issueChangeEventService); + } + @Test public void insert_entry_in_changelog_when_setting_type() { IssueDto issueDto = newIssueWithProject(CODE_SMELL); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 6ccdff45a30..aef8a7d416c 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -184,6 +184,9 @@ import org.sonar.server.projectlink.ws.ProjectLinksModule; import org.sonar.server.projecttag.ws.ProjectTagsWsModule; import org.sonar.server.property.InternalPropertiesImpl; import org.sonar.server.pushapi.ServerPushWsModule; +import org.sonar.server.pushapi.issues.DistributedIssueChangeEventsDistributor; +import org.sonar.server.pushapi.issues.IssueChangeEventServiceImpl; +import org.sonar.server.pushapi.issues.StandaloneIssueChangeEventsDistributor; import org.sonar.server.pushapi.qualityprofile.DistributedRuleActivatorEventsDistributor; import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl; import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor; @@ -285,6 +288,9 @@ public class PlatformLevel4 extends PlatformLevel { addIfCluster(DistributedRuleActivatorEventsDistributor.class); addIfStandalone(StandaloneRuleActivatorEventsDistributor.class); + addIfCluster(DistributedIssueChangeEventsDistributor.class); + addIfStandalone(StandaloneIssueChangeEventsDistributor.class); + add( RuleDescriptionFormatter.class, ClusterVerification.class, @@ -456,6 +462,7 @@ public class PlatformLevel4 extends PlatformLevel { NewIssuesNotificationHandler.newMetadata(), MyNewIssuesNotificationHandler.class, MyNewIssuesNotificationHandler.newMetadata(), + IssueChangeEventServiceImpl.class, // issues actions AssignAction.class, diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java b/sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java deleted file mode 100644 index 3a50054ca98..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/util/RuleActivationListener.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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.core.util; - -public interface RuleActivationListener { - - void listen(RuleSetChangedEvent event); -} diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleChange.java b/sonar-core/src/main/java/org/sonar/core/util/RuleChange.java deleted file mode 100644 index 3bb3af5b745..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/util/RuleChange.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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.core.util; - -import java.io.Serializable; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; - -public class RuleChange implements Serializable { - String key; - String language; - String templateKey; - String severity; - ParamChange[] params = new ParamChange[0]; - - public String getKey() { - return key; - } - - public RuleChange setKey(String key) { - this.key = key; - return this; - } - - @CheckForNull - public String getLanguage() { - return language; - } - - public RuleChange setLanguage(@Nullable String language) { - this.language = language; - return this; - } - - public String getTemplateKey() { - return templateKey; - } - - public RuleChange setTemplateKey(String templateKey) { - this.templateKey = templateKey; - return this; - } - - @CheckForNull - public String getSeverity() { - return severity; - } - - public RuleChange setSeverity(@Nullable String severity) { - this.severity = severity; - return this; - } - - public ParamChange[] getParams() { - return params; - } - - public RuleChange setParams(ParamChange[] params) { - this.params = params; - return this; - } -} diff --git a/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java deleted file mode 100644 index a37b7633233..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/util/RuleSetChangedEvent.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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.core.util; - -import java.io.Serializable; - -public class RuleSetChangedEvent implements Serializable { - - private static final String EVENT = "RuleSetChanged"; - - private final String[] projects; - private final String language; - private final RuleChange[] activatedRules; - private final String[] deactivatedRules; - - public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) { - this.projects = projects; - this.activatedRules = activatedRules; - this.deactivatedRules = deactivatedRules; - if (activatedRules.length == 0 && deactivatedRules.length == 0) { - throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed"); - } - this.language = language; - } - - public String getEvent() { - return EVENT; - } - - public String[] getProjects() { - return projects; - } - - public String getLanguage() { - return language; - } - - public RuleChange[] getActivatedRules() { - return activatedRules; - } - - public String[] getDeactivatedRules() { - return deactivatedRules; - } -} diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java b/sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java new file mode 100644 index 00000000000..d662f26d06e --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/issue/Issue.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.issue; + +import java.io.Serializable; + +public class Issue implements Serializable { + private String issueKey; + private String branchName; + + public Issue(String issueKey, String branchName) { + this.issueKey = issueKey; + this.branchName = branchName; + } + + public String getIssueKey() { + return issueKey; + } + + public String getBranchName() { + return branchName; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java new file mode 100644 index 00000000000..6c1bcfe4b19 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangeListener.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.issue; + +public interface IssueChangeListener { + void listen(IssueChangedEvent event); +} diff --git a/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java new file mode 100644 index 00000000000..0d7c8afce6e --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/issue/IssueChangedEvent.java @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.issue; + +import java.io.Serializable; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public class IssueChangedEvent implements Serializable { + private static final String EVENT = "IssueChangedEvent"; + + private final String projectKey; + private final Issue[] issues; + @CheckForNull + private final Boolean resolved; + @CheckForNull + private final String userSeverity; + @CheckForNull + private final String userType; + + public IssueChangedEvent(String projectKey, Issue[] issues, @Nullable Boolean resolved, @Nullable String userSeverity, + @Nullable String userType) { + if (issues.length == 0) { + throw new IllegalArgumentException("Can't create IssueChangedEvent without any issues that have changed"); + } + this.projectKey = projectKey; + this.issues = issues; + this.resolved = resolved; + this.userSeverity = userSeverity; + this.userType = userType; + } + + public String getEvent() { + return EVENT; + } + + public String getProjectKey() { + return projectKey; + } + + public Issue[] getIssues() { + return issues; + } + + @CheckForNull + public Boolean getResolved() { + return resolved; + } + + @CheckForNull + public String getUserSeverity() { + return userSeverity; + } + + @CheckForNull + public String getUserType() { + return userType; + } + +} diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java new file mode 100644 index 00000000000..56f3df113fe --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleActivationListener.java @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.rule; + +public interface RuleActivationListener { + + void listen(RuleSetChangedEvent event); +} diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java new file mode 100644 index 00000000000..131a0daa6ea --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleChange.java @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.rule; + +import java.io.Serializable; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.core.util.ParamChange; + +public class RuleChange implements Serializable { + private String key; + private String language; + private String templateKey; + private String severity; + private ParamChange[] params = new ParamChange[0]; + + public String getKey() { + return key; + } + + public RuleChange setKey(String key) { + this.key = key; + return this; + } + + @CheckForNull + public String getLanguage() { + return language; + } + + public RuleChange setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public String getTemplateKey() { + return templateKey; + } + + public RuleChange setTemplateKey(String templateKey) { + this.templateKey = templateKey; + return this; + } + + @CheckForNull + public String getSeverity() { + return severity; + } + + public RuleChange setSeverity(@Nullable String severity) { + this.severity = severity; + return this; + } + + public ParamChange[] getParams() { + return params; + } + + public RuleChange setParams(ParamChange[] params) { + this.params = params; + return this; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java new file mode 100644 index 00000000000..ad16db983a1 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/rule/RuleSetChangedEvent.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.rule; + +import java.io.Serializable; + +public class RuleSetChangedEvent implements Serializable { + + private static final String EVENT = "RuleSetChanged"; + + private final String[] projects; + private final String language; + private final RuleChange[] activatedRules; + private final String[] deactivatedRules; + + public RuleSetChangedEvent(String[] projects, RuleChange[] activatedRules, String[] deactivatedRules, String language) { + this.projects = projects; + this.activatedRules = activatedRules; + this.deactivatedRules = deactivatedRules; + if (activatedRules.length == 0 && deactivatedRules.length == 0) { + throw new IllegalArgumentException("Can't create RuleSetChangedEvent without any rules that have changed"); + } + this.language = language; + } + + public String getEvent() { + return EVENT; + } + + public String[] getProjects() { + return projects; + } + + public String getLanguage() { + return language; + } + + public RuleChange[] getActivatedRules() { + return activatedRules; + } + + public String[] getDeactivatedRules() { + return deactivatedRules; + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java deleted file mode 100644 index 174788d8c4f..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/util/RuleSetChangedEventTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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.core.util; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class RuleSetChangedEventTest { - - @Test - public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() { - String[] projects = {"sonarqube"}; - RuleChange[] activatedRules = {createRuleChange("java")}; - String[] deactivatedRules = {}; - RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"); - - String language = event.getLanguage(); - - assertThat(language).isEqualTo("java"); - } - - @Test - public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() { - String[] projects = {"sonarqube"}; - RuleChange[] activatedRules = {}; - String[] deactivatedRules = {"ruleKey"}; - RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"); - - String language = event.getLanguage(); - - assertThat(language).isEqualTo("java"); - } - - @Test - public void getLanguage_givenBothArraysEmpty_throwException() { - String[] projects = {"sonarqube"}; - RuleChange[] activatedRules = {}; - String[] deactivatedRules = {}; - - assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java")) - .isInstanceOf(IllegalArgumentException.class); - } - - private RuleChange createRuleChange(String language) { - RuleChange ruleChange = new RuleChange(); - ruleChange.setLanguage(language); - return ruleChange; - } -} diff --git a/sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java new file mode 100644 index 00000000000..0693c406c65 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueChangedEventTest.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.issue; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class IssueChangedEventTest { + private static final String BRANCH_NAME = "branch-name"; + private static final String ISSUE_KEY = "issue-key"; + private static final String PROJECT_KEY = "project-key"; + + @Test + public void issueChangedEvent_instantiation_accepts_nulls() { + Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)}; + IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, null, null, null); + + assertThat(event.getEvent()).isEqualTo("IssueChangedEvent"); + assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY); + assertThat(event.getResolved()).isNull(); + assertThat(event.getUserSeverity()).isNull(); + assertThat(event.getUserType()).isNull(); + assertThat(event.getIssues()).hasSize(1); + } + + @Test + public void issueChangedEvent_instantiation_accepts_actual_values() { + Issue[] issues = new Issue[]{new Issue(ISSUE_KEY, BRANCH_NAME)}; + IssueChangedEvent event = new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG"); + + assertThat(event.getEvent()).isEqualTo("IssueChangedEvent"); + assertThat(event.getProjectKey()).isEqualTo(PROJECT_KEY); + assertThat(event.getResolved()).isTrue(); + assertThat(event.getUserSeverity()).isEqualTo("BLOCKER"); + assertThat(event.getUserType()).isEqualTo("BUG"); + assertThat(event.getIssues()).hasSize(1); + } + + @Test + public void issueChangedEvent_instantiation_doesNotAccept_emptyIssues() { + Issue[] issues = new Issue[0]; + + assertThatThrownBy(() -> new IssueChangedEvent(PROJECT_KEY, issues, true, "BLOCKER", "BUG")) + .isInstanceOf(IllegalArgumentException.class) + .withFailMessage("Can't create IssueChangedEvent without any issues that have changed"); + } + +} diff --git a/sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java new file mode 100644 index 00000000000..5066a27c4ca --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/util/issue/IssueTest.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.issue; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IssueTest { + private static final String BRANCH_NAME = "branch-name"; + private static final String ISSUE_KEY = "issue-key"; + + @Test + public void issue_instantiation_accepts_values() { + Issue issue = new Issue(ISSUE_KEY, BRANCH_NAME); + + assertThat(issue.getIssueKey()).isEqualTo(ISSUE_KEY); + assertThat(issue.getBranchName()).isEqualTo(BRANCH_NAME); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java b/sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java new file mode 100644 index 00000000000..5c1b52e9c14 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/util/rule/RuleSetChangedEventTest.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.core.util.rule; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RuleSetChangedEventTest { + + @Test + public void getLanguage_givenNoDeactivatedRules_languageIsCorrectlyIdentified() { + String[] projects = {"sonarqube"}; + RuleChange[] activatedRules = {createRuleChange("java")}; + String[] deactivatedRules = {}; + RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"); + + String language = event.getLanguage(); + + assertThat(language).isEqualTo("java"); + } + + @Test + public void getLanguage_givenNoActivatedRules_languageIsCorrectlyIdentified() { + String[] projects = {"sonarqube"}; + RuleChange[] activatedRules = {}; + String[] deactivatedRules = {"ruleKey"}; + RuleSetChangedEvent event = new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java"); + + String language = event.getLanguage(); + + assertThat(language).isEqualTo("java"); + } + + @Test + public void getLanguage_givenBothArraysEmpty_throwException() { + String[] projects = {"sonarqube"}; + RuleChange[] activatedRules = {}; + String[] deactivatedRules = {}; + + assertThatThrownBy(() -> new RuleSetChangedEvent(projects, activatedRules, deactivatedRules, "java")) + .isInstanceOf(IllegalArgumentException.class); + } + + private RuleChange createRuleChange(String language) { + RuleChange ruleChange = new RuleChange(); + ruleChange.setLanguage(language); + return ruleChange; + } +}