You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

WebhookQGChangeEventListenerIT.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.webhook;
  21. import com.tngtech.java.junit.dataprovider.DataProvider;
  22. import com.tngtech.java.junit.dataprovider.DataProviderRunner;
  23. import com.tngtech.java.junit.dataprovider.UseDataProvider;
  24. import java.util.HashMap;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Optional;
  28. import java.util.Random;
  29. import java.util.Set;
  30. import java.util.function.Supplier;
  31. import java.util.stream.Collectors;
  32. import javax.annotation.Nullable;
  33. import org.junit.Rule;
  34. import org.junit.Test;
  35. import org.junit.runner.RunWith;
  36. import org.mockito.ArgumentCaptor;
  37. import org.mockito.Mockito;
  38. import org.sonar.api.config.Configuration;
  39. import org.sonar.api.measures.Metric;
  40. import org.sonar.api.utils.System2;
  41. import org.sonar.core.util.UuidFactoryFast;
  42. import org.sonar.db.DbClient;
  43. import org.sonar.db.DbTester;
  44. import org.sonar.db.component.AnalysisPropertyDto;
  45. import org.sonar.db.component.BranchDto;
  46. import org.sonar.db.component.BranchType;
  47. import org.sonar.db.component.ProjectData;
  48. import org.sonar.db.component.SnapshotDto;
  49. import org.sonar.db.project.ProjectDto;
  50. import org.sonar.server.qualitygate.EvaluatedQualityGate;
  51. import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
  52. import org.sonar.server.qualitygate.changeevent.QGChangeEventListener;
  53. import static java.util.Arrays.stream;
  54. import static java.util.Collections.emptySet;
  55. import static java.util.stream.Stream.concat;
  56. import static java.util.stream.Stream.of;
  57. import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
  58. import static org.assertj.core.api.Assertions.assertThat;
  59. import static org.mockito.ArgumentMatchers.any;
  60. import static org.mockito.ArgumentMatchers.eq;
  61. import static org.mockito.Mockito.mock;
  62. import static org.mockito.Mockito.verify;
  63. import static org.mockito.Mockito.verifyNoInteractions;
  64. import static org.mockito.Mockito.when;
  65. import static org.sonar.db.component.BranchType.BRANCH;
  66. @RunWith(DataProviderRunner.class)
  67. public class WebhookQGChangeEventListenerIT {
  68. private static final Set<QGChangeEventListener.ChangedIssue> CHANGED_ISSUES_ARE_IGNORED = emptySet();
  69. @Rule
  70. public DbTester dbTester = DbTester.create(System2.INSTANCE);
  71. private DbClient dbClient = dbTester.getDbClient();
  72. private EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
  73. private WebHooks webHooks = mock(WebHooks.class);
  74. private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
  75. private DbClient spiedOnDbClient = Mockito.spy(dbClient);
  76. private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnDbClient);
  77. private DbClient mockedDbClient = mock(DbClient.class);
  78. private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, mockedDbClient);
  79. @Test
  80. @UseDataProvider("allCombinationsOfStatuses")
  81. public void onIssueChanges_has_no_effect_if_no_webhook_is_configured(Metric.Level previousStatus, Metric.Level newStatus) {
  82. Configuration configuration1 = mock(Configuration.class);
  83. when(newQualityGate.getStatus()).thenReturn(newStatus);
  84. QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration1, previousStatus, newQualityGate);
  85. mockWebhookDisabled(qualityGateEvent.getProject());
  86. mockedUnderTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  87. verify(webHooks).isEnabled(qualityGateEvent.getProject());
  88. verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
  89. }
  90. @DataProvider
  91. public static Object[][] allCombinationsOfStatuses() {
  92. Metric.Level[] levelsAndNull = concat(of((Metric.Level) null), stream(Metric.Level.values()))
  93. .toArray(Metric.Level[]::new);
  94. Object[][] res = new Object[levelsAndNull.length * levelsAndNull.length][2];
  95. int i = 0;
  96. for (Metric.Level previousStatus : levelsAndNull) {
  97. for (Metric.Level newStatus : levelsAndNull) {
  98. res[i][0] = previousStatus;
  99. res[i][1] = newStatus;
  100. i++;
  101. }
  102. }
  103. return res;
  104. }
  105. @Test
  106. public void onIssueChanges_has_no_effect_if_event_has_neither_previousQGStatus_nor_qualityGate() {
  107. Configuration configuration = mock(Configuration.class);
  108. QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration, null, null);
  109. mockWebhookEnabled(qualityGateEvent.getProject());
  110. underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  111. verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
  112. }
  113. @Test
  114. public void onIssueChanges_has_no_effect_if_event_has_same_status_in_previous_and_new_QG() {
  115. Configuration configuration = mock(Configuration.class);
  116. Metric.Level previousStatus = randomLevel();
  117. when(newQualityGate.getStatus()).thenReturn(previousStatus);
  118. QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration, previousStatus, newQualityGate);
  119. mockWebhookEnabled(qualityGateEvent.getProject());
  120. underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  121. verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
  122. }
  123. @Test
  124. @UseDataProvider("newQGorNot")
  125. public void onIssueChanges_calls_webhook_for_changeEvent_with_webhook_enabled(@Nullable EvaluatedQualityGate newQualityGate) {
  126. ProjectAndBranch projectBranch = insertBranch(BRANCH, "foo");
  127. SnapshotDto analysis = insertAnalysisTask(projectBranch);
  128. Configuration configuration = mock(Configuration.class);
  129. mockPayloadSupplierConsumedByWebhooks();
  130. Map<String, String> properties = new HashMap<>();
  131. properties.put("sonar.analysis.test1", randomAlphanumeric(50));
  132. properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
  133. insertPropertiesFor(analysis.getUuid(), properties);
  134. QGChangeEvent qualityGateEvent = newQGChangeEvent(projectBranch, analysis, configuration, newQualityGate);
  135. mockWebhookEnabled(qualityGateEvent.getProject());
  136. underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  137. ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(projectBranch, analysis, qualityGateEvent.getProject());
  138. assertThat(projectAnalysis).isEqualTo(
  139. new ProjectAnalysis(
  140. new Project(projectBranch.project.getUuid(), projectBranch.project.getKey(), projectBranch.project.getName()),
  141. null,
  142. new Analysis(analysis.getUuid(), analysis.getCreatedAt(), analysis.getRevision()),
  143. new Branch(false, "foo", Branch.Type.BRANCH),
  144. newQualityGate,
  145. null,
  146. properties));
  147. }
  148. @Test
  149. @UseDataProvider("newQGorNot")
  150. public void onIssueChanges_calls_webhook_on_main_branch(@Nullable EvaluatedQualityGate newQualityGate) {
  151. ProjectAndBranch mainBranch = insertMainBranch();
  152. SnapshotDto analysis = insertAnalysisTask(mainBranch);
  153. Configuration configuration = mock(Configuration.class);
  154. QGChangeEvent qualityGateEvent = newQGChangeEvent(mainBranch, analysis, configuration, newQualityGate);
  155. mockWebhookEnabled(qualityGateEvent.getProject());
  156. underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  157. verifyWebhookCalled(mainBranch, analysis, qualityGateEvent.getProject());
  158. }
  159. @Test
  160. public void onIssueChanges_calls_webhook_on_branch() {
  161. onIssueChangesCallsWebhookOnBranch(BRANCH);
  162. }
  163. @Test
  164. public void onIssueChanges_calls_webhook_on_pr() {
  165. onIssueChangesCallsWebhookOnBranch(BranchType.PULL_REQUEST);
  166. }
  167. public void onIssueChangesCallsWebhookOnBranch(BranchType branchType) {
  168. ProjectAndBranch nonMainBranch = insertBranch(branchType, "foo");
  169. SnapshotDto analysis = insertAnalysisTask(nonMainBranch);
  170. Configuration configuration = mock(Configuration.class);
  171. QGChangeEvent qualityGateEvent = newQGChangeEvent(nonMainBranch, analysis, configuration, null);
  172. mockWebhookEnabled(qualityGateEvent.getProject());
  173. underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
  174. verifyWebhookCalled(nonMainBranch, analysis, qualityGateEvent.getProject());
  175. }
  176. @DataProvider
  177. public static Object[][] newQGorNot() {
  178. EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
  179. return new Object[][] {
  180. {null},
  181. {newQualityGate}
  182. };
  183. }
  184. private void mockWebhookEnabled(ProjectDto... projects) {
  185. for (ProjectDto dto : projects) {
  186. when(webHooks.isEnabled(dto)).thenReturn(true);
  187. }
  188. }
  189. private void mockWebhookDisabled(ProjectDto... projects) {
  190. for (ProjectDto dto : projects) {
  191. when(webHooks.isEnabled(dto)).thenReturn(false);
  192. }
  193. }
  194. private void mockPayloadSupplierConsumedByWebhooks() {
  195. Mockito.doAnswer(invocationOnMock -> {
  196. Supplier<WebhookPayload> supplier = (Supplier<WebhookPayload>) invocationOnMock.getArguments()[1];
  197. supplier.get();
  198. return null;
  199. }).when(webHooks)
  200. .sendProjectAnalysisUpdate(any(), any());
  201. }
  202. private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
  203. List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
  204. .map(entry -> new AnalysisPropertyDto()
  205. .setUuid(UuidFactoryFast.getInstance().create())
  206. .setAnalysisUuid(snapshotUuid)
  207. .setKey(entry.getKey())
  208. .setValue(entry.getValue()))
  209. .toList();
  210. dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties);
  211. dbTester.getSession().commit();
  212. }
  213. private SnapshotDto insertAnalysisTask(ProjectAndBranch projectAndBranch) {
  214. return dbTester.components().insertSnapshot(projectAndBranch.getBranch());
  215. }
  216. private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ProjectAndBranch projectAndBranch, SnapshotDto analysis, ProjectDto project) {
  217. verifyWebhookCalled(projectAndBranch, analysis, project);
  218. return extractPayloadFactoryArguments(1).iterator().next();
  219. }
  220. private void verifyWebhookCalled(ProjectAndBranch projectAndBranch, SnapshotDto analysis, ProjectDto project) {
  221. verify(webHooks).isEnabled(project);
  222. verify(webHooks).sendProjectAnalysisUpdate(
  223. eq(new WebHooks.Analysis(projectAndBranch.uuid(), analysis.getUuid(), null)),
  224. any());
  225. }
  226. private List<ProjectAnalysis> extractPayloadFactoryArguments(int time) {
  227. ArgumentCaptor<ProjectAnalysis> projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class);
  228. verify(webhookPayloadFactory, Mockito.times(time)).create(projectAnalysisCaptor.capture());
  229. return projectAnalysisCaptor.getAllValues();
  230. }
  231. public ProjectAndBranch insertMainBranch() {
  232. ProjectData project = dbTester.components().insertPrivateProject();
  233. return new ProjectAndBranch(project.getProjectDto(), project.getMainBranchDto());
  234. }
  235. public ProjectAndBranch insertBranch(BranchType type, String branchKey) {
  236. ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
  237. BranchDto branch = dbTester.components().insertProjectBranch(project, b -> b.setKey(branchKey).setBranchType(type));
  238. return new ProjectAndBranch(project, branch);
  239. }
  240. public ProjectAndBranch insertBranch(ProjectDto project, BranchType type, String branchKey) {
  241. BranchDto branch = dbTester.components().insertProjectBranch(project, b -> b.setKey(branchKey).setBranchType(type));
  242. return new ProjectAndBranch(project, branch);
  243. }
  244. private static class ProjectAndBranch {
  245. private final ProjectDto project;
  246. private final BranchDto branch;
  247. private ProjectAndBranch(ProjectDto project, BranchDto branch) {
  248. this.project = project;
  249. this.branch = branch;
  250. }
  251. public ProjectDto getProject() {
  252. return project;
  253. }
  254. public BranchDto getBranch() {
  255. return branch;
  256. }
  257. public String uuid() {
  258. return project.getUuid();
  259. }
  260. }
  261. private static QGChangeEvent newQGChangeEvent(Configuration configuration, @Nullable Metric.Level previousQQStatus, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
  262. return new QGChangeEvent(new ProjectDto(), new BranchDto(), new SnapshotDto(), configuration, previousQQStatus, () -> Optional.ofNullable(evaluatedQualityGate));
  263. }
  264. private static QGChangeEvent newQGChangeEvent(ProjectAndBranch branch, SnapshotDto analysis, Configuration configuration, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
  265. Metric.Level previousStatus = randomLevel();
  266. if (evaluatedQualityGate != null) {
  267. Metric.Level otherLevel = stream(Metric.Level.values())
  268. .filter(s -> s != previousStatus)
  269. .toArray(Metric.Level[]::new)[new Random().nextInt(Metric.Level.values().length - 1)];
  270. when(evaluatedQualityGate.getStatus()).thenReturn(otherLevel);
  271. }
  272. return new QGChangeEvent(branch.project, branch.branch, analysis, configuration, previousStatus, () -> Optional.ofNullable(evaluatedQualityGate));
  273. }
  274. private static Metric.Level randomLevel() {
  275. return Metric.Level.values()[new Random().nextInt(Metric.Level.values().length)];
  276. }
  277. }