]> source.dussan.org Git - sonarqube.git/blob
cc530b0e9469e245e3281674ad89272c8cde4220
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 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
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.Random;
30 import java.util.Set;
31 import java.util.function.Supplier;
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
54 import static java.util.Arrays.stream;
55 import static java.util.Collections.emptySet;
56 import static java.util.stream.Stream.concat;
57 import static java.util.stream.Stream.of;
58 import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
59 import static org.assertj.core.api.Assertions.assertThat;
60 import static org.mockito.ArgumentMatchers.any;
61 import static org.mockito.ArgumentMatchers.eq;
62 import static org.mockito.Mockito.mock;
63 import static org.mockito.Mockito.verify;
64 import static org.mockito.Mockito.verifyNoInteractions;
65 import static org.mockito.Mockito.when;
66 import static org.sonar.db.component.BranchType.BRANCH;
67
68 @RunWith(DataProviderRunner.class)
69 public class WebhookQGChangeEventListenerIT {
70
71   private static final Set<QGChangeEventListener.ChangedIssue> CHANGED_ISSUES_ARE_IGNORED = emptySet();
72
73   @Rule
74   public DbTester dbTester = DbTester.create(System2.INSTANCE);
75
76   private DbClient dbClient = dbTester.getDbClient();
77
78   private EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
79   private WebHooks webHooks = mock(WebHooks.class);
80   private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
81   private DbClient spiedOnDbClient = Mockito.spy(dbClient);
82   private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnDbClient);
83   private DbClient mockedDbClient = mock(DbClient.class);
84   private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, mockedDbClient);
85
86   @Test
87   @UseDataProvider("allCombinationsOfStatuses")
88   public void onIssueChanges_has_no_effect_if_no_webhook_is_configured(Metric.Level previousStatus, Metric.Level newStatus) {
89     Configuration configuration1 = mock(Configuration.class);
90     when(newQualityGate.getStatus()).thenReturn(newStatus);
91     QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration1, previousStatus, newQualityGate);
92     mockWebhookDisabled(qualityGateEvent.getProject());
93
94     mockedUnderTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
95
96     verify(webHooks).isEnabled(qualityGateEvent.getProject());
97     verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
98   }
99
100   @DataProvider
101   public static Object[][] allCombinationsOfStatuses() {
102     Metric.Level[] levelsAndNull = concat(of((Metric.Level) null), stream(Metric.Level.values()))
103       .toArray(Metric.Level[]::new);
104     Object[][] res = new Object[levelsAndNull.length * levelsAndNull.length][2];
105     int i = 0;
106     for (Metric.Level previousStatus : levelsAndNull) {
107       for (Metric.Level newStatus : levelsAndNull) {
108         res[i][0] = previousStatus;
109         res[i][1] = newStatus;
110         i++;
111       }
112     }
113     return res;
114   }
115
116   @Test
117   public void onIssueChanges_has_no_effect_if_event_has_neither_previousQGStatus_nor_qualityGate() {
118     Configuration configuration = mock(Configuration.class);
119     QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration, null, null);
120     mockWebhookEnabled(qualityGateEvent.getProject());
121
122     underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
123
124     verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
125   }
126
127   @Test
128   public void onIssueChanges_has_no_effect_if_event_has_same_status_in_previous_and_new_QG() {
129     Configuration configuration = mock(Configuration.class);
130     Metric.Level previousStatus = randomLevel();
131     when(newQualityGate.getStatus()).thenReturn(previousStatus);
132     QGChangeEvent qualityGateEvent = newQGChangeEvent(configuration, previousStatus, newQualityGate);
133     mockWebhookEnabled(qualityGateEvent.getProject());
134
135     underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
136
137     verifyNoInteractions(webhookPayloadFactory, mockedDbClient);
138   }
139
140   @Test
141   @UseDataProvider("newQGorNot")
142   public void onIssueChanges_calls_webhook_for_changeEvent_with_webhook_enabled(@Nullable EvaluatedQualityGate newQualityGate) {
143     ProjectAndBranch projectBranch = insertBranch(BRANCH, "foo");
144     SnapshotDto analysis = insertAnalysisTask(projectBranch);
145     Configuration configuration = mock(Configuration.class);
146     mockPayloadSupplierConsumedByWebhooks();
147     Map<String, String> properties = new HashMap<>();
148     properties.put("sonar.analysis.test1", randomAlphanumeric(50));
149     properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
150     insertPropertiesFor(analysis.getUuid(), properties);
151     QGChangeEvent qualityGateEvent = newQGChangeEvent(projectBranch, analysis, configuration, newQualityGate);
152     mockWebhookEnabled(qualityGateEvent.getProject());
153
154     underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
155
156     ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(projectBranch, analysis, qualityGateEvent.getProject());
157     assertThat(projectAnalysis).isEqualTo(
158       new ProjectAnalysis(
159         new Project(projectBranch.project.getUuid(), projectBranch.project.getKey(), projectBranch.project.getName()),
160         null,
161         new Analysis(analysis.getUuid(), analysis.getCreatedAt(), analysis.getRevision()),
162         new Branch(false, "foo", Branch.Type.BRANCH),
163         newQualityGate,
164         null,
165         properties));
166   }
167
168   @Test
169   @UseDataProvider("newQGorNot")
170   public void onIssueChanges_calls_webhook_on_main_branch(@Nullable EvaluatedQualityGate newQualityGate) {
171     ProjectAndBranch mainBranch = insertMainBranch();
172     SnapshotDto analysis = insertAnalysisTask(mainBranch);
173     Configuration configuration = mock(Configuration.class);
174     QGChangeEvent qualityGateEvent = newQGChangeEvent(mainBranch, analysis, configuration, newQualityGate);
175     mockWebhookEnabled(qualityGateEvent.getProject());
176
177     underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
178
179     verifyWebhookCalled(mainBranch, analysis, qualityGateEvent.getProject());
180   }
181
182   @Test
183   public void onIssueChanges_calls_webhook_on_branch() {
184     onIssueChangesCallsWebhookOnBranch(BRANCH);
185   }
186
187   @Test
188   public void onIssueChanges_calls_webhook_on_pr() {
189     onIssueChangesCallsWebhookOnBranch(BranchType.PULL_REQUEST);
190   }
191
192   public void onIssueChangesCallsWebhookOnBranch(BranchType branchType) {
193     ProjectAndBranch nonMainBranch = insertBranch(branchType, "foo");
194     SnapshotDto analysis = insertAnalysisTask(nonMainBranch);
195     Configuration configuration = mock(Configuration.class);
196     QGChangeEvent qualityGateEvent = newQGChangeEvent(nonMainBranch, analysis, configuration, null);
197     mockWebhookEnabled(qualityGateEvent.getProject());
198
199     underTest.onIssueChanges(qualityGateEvent, CHANGED_ISSUES_ARE_IGNORED);
200
201     verifyWebhookCalled(nonMainBranch, analysis, qualityGateEvent.getProject());
202   }
203
204   @DataProvider
205   public static Object[][] newQGorNot() {
206     EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
207     return new Object[][] {
208       {null},
209       {newQualityGate}
210     };
211   }
212
213   private void mockWebhookEnabled(ProjectDto... projects) {
214     for (ProjectDto dto : projects) {
215       when(webHooks.isEnabled(dto)).thenReturn(true);
216     }
217   }
218
219   private void mockWebhookDisabled(ProjectDto... projects) {
220     for (ProjectDto dto : projects) {
221       when(webHooks.isEnabled(dto)).thenReturn(false);
222     }
223   }
224
225   private void mockPayloadSupplierConsumedByWebhooks() {
226     Mockito.doAnswer(invocationOnMock -> {
227       Supplier<WebhookPayload> supplier = (Supplier<WebhookPayload>) invocationOnMock.getArguments()[1];
228       supplier.get();
229       return null;
230     }).when(webHooks)
231       .sendProjectAnalysisUpdate(any(), any());
232   }
233
234   private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
235     List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
236       .map(entry -> new AnalysisPropertyDto()
237         .setUuid(UuidFactoryFast.getInstance().create())
238         .setAnalysisUuid(snapshotUuid)
239         .setKey(entry.getKey())
240         .setValue(entry.getValue()))
241       .toList();
242     dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties);
243     dbTester.getSession().commit();
244   }
245
246   private SnapshotDto insertAnalysisTask(ProjectAndBranch projectAndBranch) {
247     return dbTester.components().insertSnapshot(projectAndBranch.getBranch());
248   }
249
250   private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ProjectAndBranch projectAndBranch, SnapshotDto analysis, ProjectDto project) {
251     verifyWebhookCalled(projectAndBranch, analysis, project);
252
253     return extractPayloadFactoryArguments(1).iterator().next();
254   }
255
256   private void verifyWebhookCalled(ProjectAndBranch projectAndBranch, SnapshotDto analysis, ProjectDto project) {
257     verify(webHooks).isEnabled(project);
258     verify(webHooks).sendProjectAnalysisUpdate(
259       eq(new WebHooks.Analysis(projectAndBranch.uuid(), analysis.getUuid(), null)),
260       any());
261   }
262
263   private List<ProjectAnalysis> extractPayloadFactoryArguments(int time) {
264     ArgumentCaptor<ProjectAnalysis> projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class);
265     verify(webhookPayloadFactory, Mockito.times(time)).create(projectAnalysisCaptor.capture());
266     return projectAnalysisCaptor.getAllValues();
267   }
268
269   public ProjectAndBranch insertMainBranch() {
270     ProjectData project = dbTester.components().insertPrivateProject();
271     return new ProjectAndBranch(project.getProjectDto(), project.getMainBranchDto());
272   }
273
274   public ProjectAndBranch insertBranch(BranchType type, String branchKey) {
275     ProjectDto project = dbTester.components().insertPrivateProject().getProjectDto();
276     BranchDto branch = dbTester.components().insertProjectBranch(project, b -> b.setKey(branchKey).setBranchType(type));
277     return new ProjectAndBranch(project, branch);
278   }
279
280   public ProjectAndBranch insertBranch(ProjectDto project, BranchType type, String branchKey) {
281     BranchDto branch = dbTester.components().insertProjectBranch(project, b -> b.setKey(branchKey).setBranchType(type));
282     return new ProjectAndBranch(project, branch);
283   }
284
285   private static class ProjectAndBranch {
286     private final ProjectDto project;
287     private final BranchDto branch;
288
289     private ProjectAndBranch(ProjectDto project, BranchDto branch) {
290       this.project = project;
291       this.branch = branch;
292     }
293
294     public ProjectDto getProject() {
295       return project;
296     }
297
298     public BranchDto getBranch() {
299       return branch;
300     }
301
302     public String uuid() {
303       return project.getUuid();
304     }
305
306   }
307
308   private static QGChangeEvent newQGChangeEvent(Configuration configuration, @Nullable Metric.Level previousQQStatus, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
309     return new QGChangeEvent(new ProjectDto(), new BranchDto(), new SnapshotDto(), configuration, previousQQStatus, () -> Optional.ofNullable(evaluatedQualityGate));
310   }
311
312   private static QGChangeEvent newQGChangeEvent(ProjectAndBranch branch, SnapshotDto analysis, Configuration configuration, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
313     Metric.Level previousStatus = randomLevel();
314     if (evaluatedQualityGate != null) {
315       Metric.Level otherLevel = stream(Metric.Level.values())
316         .filter(s -> s != previousStatus)
317         .toArray(Metric.Level[]::new)[new Random().nextInt(Metric.Level.values().length - 1)];
318       when(evaluatedQualityGate.getStatus()).thenReturn(otherLevel);
319     }
320     return new QGChangeEvent(branch.project, branch.branch, analysis, configuration, previousStatus, () -> Optional.ofNullable(evaluatedQualityGate));
321   }
322
323   private static Metric.Level randomLevel() {
324     return Metric.Level.values()[new Random().nextInt(Metric.Level.values().length)];
325   }
326
327 }