3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.ce.notification;
22 import java.time.Duration;
23 import java.time.temporal.ChronoUnit;
24 import java.util.Arrays;
25 import java.util.Optional;
26 import java.util.Random;
27 import javax.annotation.Nullable;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.mockito.ArgumentCaptor;
31 import org.sonar.api.notifications.Notification;
32 import org.sonar.api.resources.Qualifiers;
33 import org.sonar.api.utils.System2;
34 import org.sonar.ce.task.CeTask;
35 import org.sonar.ce.task.CeTaskResult;
36 import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotification;
37 import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationBuilder;
38 import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationSerializer;
39 import org.sonar.db.DbClient;
40 import org.sonar.db.DbTester;
41 import org.sonar.db.RowNotFoundException;
42 import org.sonar.db.ce.CeActivityDto;
43 import org.sonar.db.ce.CeQueueDto;
44 import org.sonar.db.ce.CeTaskTypes;
45 import org.sonar.db.component.BranchDto;
46 import org.sonar.db.component.ComponentDto;
47 import org.sonar.db.component.ComponentTesting;
48 import org.sonar.db.component.ProjectData;
49 import org.sonar.db.project.CreationMethod;
50 import org.sonar.db.project.ProjectDto;
51 import org.sonar.server.notification.NotificationService;
53 import static java.util.Collections.singleton;
54 import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
55 import static org.assertj.core.api.Assertions.assertThat;
56 import static org.assertj.core.api.Assertions.assertThatThrownBy;
57 import static org.assertj.core.api.Assertions.fail;
58 import static org.mockito.ArgumentMatchers.any;
59 import static org.mockito.ArgumentMatchers.same;
60 import static org.mockito.Mockito.mock;
61 import static org.mockito.Mockito.verify;
62 import static org.mockito.Mockito.verifyNoInteractions;
63 import static org.mockito.Mockito.when;
64 import static org.sonar.db.component.ComponentTesting.newDirectory;
66 public class ReportAnalysisFailureNotificationExecutionListenerIT {
68 public DbTester dbTester = DbTester.create(System2.INSTANCE);
70 private final Random random = new Random();
71 private final DbClient dbClient = dbTester.getDbClient();
72 private final NotificationService notificationService = mock(NotificationService.class);
73 private final ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class);
74 private final System2 system2 = mock(System2.class);
75 private final DbClient dbClientMock = mock(DbClient.class);
76 private final CeTask ceTaskMock = mock(CeTask.class);
77 private final Throwable throwableMock = mock(Throwable.class);
78 private final CeTaskResult ceTaskResultMock = mock(CeTaskResult.class);
79 private final ReportAnalysisFailureNotificationExecutionListener fullMockedUnderTest = new ReportAnalysisFailureNotificationExecutionListener(
80 notificationService, dbClientMock, serializer, system2);
81 private final ReportAnalysisFailureNotificationExecutionListener underTest = new ReportAnalysisFailureNotificationExecutionListener(
82 notificationService, dbClient, serializer, system2);
85 public void onStart_has_no_effect() {
86 CeTask mockedCeTask = mock(CeTask.class);
88 fullMockedUnderTest.onStart(mockedCeTask);
90 verifyNoInteractions(mockedCeTask, notificationService, dbClientMock, serializer, system2);
94 public void onEnd_has_no_effect_if_status_is_SUCCESS() {
95 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.SUCCESS, randomDuration(), ceTaskResultMock, throwableMock);
97 verifyNoInteractions(ceTaskMock, ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
101 public void onEnd_has_no_effect_if_CeTask_type_is_not_report() {
102 when(ceTaskMock.getType()).thenReturn(randomAlphanumeric(12));
104 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
106 verifyNoInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
110 public void onEnd_has_no_effect_if_CeTask_has_no_component_uuid() {
111 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
113 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
115 verifyNoInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
119 public void onEnd_has_no_effect_if_there_is_no_subscriber_for_ReportAnalysisFailureNotification_type() {
120 ProjectData projectData = dbTester.components().insertPrivateProject();
121 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
122 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(projectData.getMainBranchDto().getUuid(), null, null)));
123 when(notificationService.hasProjectSubscribersForTypes(projectData.projectUuid(), singleton(ReportAnalysisFailureNotification.class)))
126 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
128 verifyNoInteractions(ceTaskResultMock, throwableMock, serializer, system2);
132 public void onEnd_fails_with_ISE_if_project_does_not_exist_in_DB() {
133 BranchDto branchDto = dbTester.components().insertProjectBranch(new ProjectDto().setUuid("uuid").setKee("kee").setName("name"));
134 String componentUuid = branchDto.getUuid();
135 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
136 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
137 when(notificationService.hasProjectSubscribersForTypes(branchDto.getProjectUuid(), singleton(ReportAnalysisFailureNotification.class)))
140 Duration randomDuration = randomDuration();
141 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
142 .isInstanceOf(IllegalStateException.class)
143 .hasMessage("Could not find project uuid " + branchDto.getProjectUuid());
147 public void onEnd_fails_with_ISE_if_branch_does_not_exist_in_DB() {
148 String componentUuid = randomAlphanumeric(6);
149 ProjectDto project = new ProjectDto().setUuid(componentUuid).setKey(randomAlphanumeric(5)).setQualifier(Qualifiers.PROJECT).setCreationMethod(CreationMethod.LOCAL_API);
150 dbTester.getDbClient().projectDao().insert(dbTester.getSession(), project);
151 dbTester.getSession().commit();
152 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
153 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
154 when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.class)))
157 Duration randomDuration = randomDuration();
158 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
159 .isInstanceOf(IllegalStateException.class)
160 .hasMessage("Could not find a branch with uuid " + componentUuid);
164 public void onEnd_fails_with_IAE_if_component_is_not_a_branch() {
165 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
166 ComponentDto mainBranch = dbTester.components().insertPrivateProject().getMainBranchComponent();
167 ComponentDto directory = dbTester.components().insertComponent(newDirectory(mainBranch, randomAlphanumeric(12)));
168 ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(mainBranch));
169 ComponentDto view = dbTester.components().insertComponent(ComponentTesting.newPortfolio());
170 ComponentDto subView = dbTester.components().insertComponent(ComponentTesting.newSubPortfolio(view));
171 ComponentDto projectCopy = dbTester.components().insertComponent(ComponentTesting.newProjectCopy(mainBranch, subView));
172 ComponentDto application = dbTester.components().insertComponent(ComponentTesting.newApplication());
174 Arrays.asList(directory, file, view, subView, projectCopy, application)
175 .forEach(component -> {
177 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(component.uuid(), null, null)));
178 when(notificationService.hasProjectSubscribersForTypes(component.uuid(), singleton(ReportAnalysisFailureNotification.class)))
181 Duration randomDuration = randomDuration();
183 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock);
185 fail("An IllegalArgumentException should have been thrown for component " + component);
186 } catch (IllegalStateException e) {
187 assertThat(e.getMessage()).isIn("Could not find project uuid " + component.uuid(), "Could not find a branch with uuid " + component.uuid());
193 public void onEnd_fails_with_RowNotFoundException_if_activity_for_task_does_not_exist_in_DB() {
194 ProjectData projectData = dbTester.components().insertPrivateProject();
195 ComponentDto mainBranch = projectData.getMainBranchComponent();
196 String taskUuid = randomAlphanumeric(6);
197 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
198 when(ceTaskMock.getUuid()).thenReturn(taskUuid);
199 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(mainBranch.uuid(), null, null)));
200 when(notificationService.hasProjectSubscribersForTypes(projectData.projectUuid(), singleton(ReportAnalysisFailureNotification.class)))
203 Duration randomDuration = randomDuration();
204 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
205 .isInstanceOf(RowNotFoundException.class)
206 .hasMessage("CeActivity with uuid '" + taskUuid + "' not found");
210 public void onEnd_creates_notification_with_data_from_activity_and_project_and_deliver_it() {
211 String taskUuid = randomAlphanumeric(12);
212 int createdAt = random.nextInt(999_999);
213 long executedAt = random.nextInt(999_999);
214 ProjectData project = initMocksToPassConditions(taskUuid, createdAt, executedAt);
215 Notification notificationMock = mockSerializer();
217 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
219 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
220 verify(notificationService).deliver(same(notificationMock));
222 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
224 ReportAnalysisFailureNotificationBuilder.Project notificationProject = reportAnalysisFailureNotificationBuilder.project();
225 assertThat(notificationProject.name()).isEqualTo(project.getProjectDto().getName());
226 assertThat(notificationProject.key()).isEqualTo(project.getProjectDto().getKey());
227 assertThat(notificationProject.uuid()).isEqualTo(project.getProjectDto().getUuid());
228 assertThat(notificationProject.branchName()).isNull();
229 ReportAnalysisFailureNotificationBuilder.Task notificationTask = reportAnalysisFailureNotificationBuilder.task();
230 assertThat(notificationTask.uuid()).isEqualTo(taskUuid);
231 assertThat(notificationTask.createdAt()).isEqualTo(createdAt);
232 assertThat(notificationTask.failedAt()).isEqualTo(executedAt);
236 public void onEnd_shouldCreateNotificationWithDataFromActivity_whenNonMainBranchIsFailing() {
237 String taskUuid = randomAlphanumeric(12);
238 int createdAt = random.nextInt(999_999);
239 long executedAt = random.nextInt(999_999);
241 ProjectData project = random.nextBoolean() ? dbTester.components().insertPrivateProject() : dbTester.components().insertPublicProject();
242 ComponentDto branchComponent = dbTester.components().insertProjectBranch(project.getMainBranchComponent(), b -> b.setKey("otherbranch"));
243 initMocksToPassConditionsForBranch(branchComponent, project, taskUuid, createdAt, executedAt);
245 Notification notificationMock = mockSerializer();
247 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
249 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
250 verify(notificationService).deliver(same(notificationMock));
252 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
254 ReportAnalysisFailureNotificationBuilder.Project notificationProject = reportAnalysisFailureNotificationBuilder.project();
255 assertThat(notificationProject.name()).isEqualTo(project.getProjectDto().getName());
256 assertThat(notificationProject.key()).isEqualTo(project.getProjectDto().getKey());
257 assertThat(notificationProject.uuid()).isEqualTo(project.getProjectDto().getUuid());
258 assertThat(notificationProject.branchName()).isEqualTo("otherbranch");
259 ReportAnalysisFailureNotificationBuilder.Task notificationTask = reportAnalysisFailureNotificationBuilder.task();
260 assertThat(notificationTask.uuid()).isEqualTo(taskUuid);
261 assertThat(notificationTask.createdAt()).isEqualTo(createdAt);
262 assertThat(notificationTask.failedAt()).isEqualTo(executedAt);
266 public void onEnd_creates_notification_with_error_message_from_Throwable_argument_message() {
267 initMocksToPassConditions(randomAlphanumeric(12), random.nextInt(999_999), (long) random.nextInt(999_999));
268 String message = randomAlphanumeric(66);
269 when(throwableMock.getMessage()).thenReturn(message);
271 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
273 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
275 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
276 assertThat(reportAnalysisFailureNotificationBuilder.errorMessage()).isEqualTo(message);
280 public void onEnd_creates_notification_with_null_error_message_if_Throwable_is_null() {
281 String taskUuid = randomAlphanumeric(12);
282 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
283 Notification notificationMock = mockSerializer();
285 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
287 verify(notificationService).deliver(same(notificationMock));
288 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
290 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
291 assertThat(reportAnalysisFailureNotificationBuilder.errorMessage()).isNull();
295 public void onEnd_ignores_null_CeTaskResult_argument() {
296 String taskUuid = randomAlphanumeric(12);
297 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
298 Notification notificationMock = mockSerializer();
300 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), null, null);
302 verify(notificationService).deliver(same(notificationMock));
306 public void onEnd_ignores_CeTaskResult_argument() {
307 String taskUuid = randomAlphanumeric(12);
308 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
309 Notification notificationMock = mockSerializer();
311 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
313 verify(notificationService).deliver(same(notificationMock));
314 verifyNoInteractions(ceTaskResultMock);
318 public void onEnd_uses_system_data_as_failedAt_if_task_has_no_executedAt() {
319 String taskUuid = randomAlphanumeric(12);
320 initMocksToPassConditions(taskUuid, random.nextInt(999_999), null);
321 long now = random.nextInt(999_999);
322 when(system2.now()).thenReturn(now);
323 Notification notificationMock = mockSerializer();
325 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
327 verify(notificationService).deliver(same(notificationMock));
328 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
329 assertThat(notificationCaptor.getValue().task().failedAt()).isEqualTo(now);
332 private ReportAnalysisFailureNotification mockSerializer() {
333 ReportAnalysisFailureNotification notificationMock = mock(ReportAnalysisFailureNotification.class);
334 when(serializer.toNotification(any(ReportAnalysisFailureNotificationBuilder.class))).thenReturn(notificationMock);
335 return notificationMock;
338 private ProjectData initMocksToPassConditions(String taskUuid, int createdAt, @Nullable Long executedAt) {
339 ProjectData projectData = random.nextBoolean() ? dbTester.components().insertPrivateProject() : dbTester.components().insertPublicProject();
340 mockCeTask(taskUuid, createdAt, executedAt, projectData, projectData.getMainBranchComponent());
344 private ComponentDto initMocksToPassConditionsForBranch(ComponentDto branchComponent, ProjectData projectData, String taskUuid, int createdAt, @Nullable Long executedAt) {
345 mockCeTask(taskUuid, createdAt, executedAt, projectData, branchComponent);
346 return branchComponent;
349 private void mockCeTask(String taskUuid, int createdAt, @org.jetbrains.annotations.Nullable Long executedAt, ProjectData projectData, ComponentDto branchComponent) {
350 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
351 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(branchComponent.uuid(), null, null)));
352 when(ceTaskMock.getUuid()).thenReturn(taskUuid);
353 when(notificationService.hasProjectSubscribersForTypes(projectData.projectUuid(), singleton(ReportAnalysisFailureNotification.class)))
355 insertActivityDto(taskUuid, createdAt, executedAt, branchComponent);
358 private void insertActivityDto(String taskUuid, int createdAt, @Nullable Long executedAt, ComponentDto branch) {
359 dbClient.ceActivityDao().insert(dbTester.getSession(), new CeActivityDto(new CeQueueDto()
361 .setTaskType(CeTaskTypes.REPORT)
362 .setComponentUuid(branch.uuid())
363 .setCreatedAt(createdAt))
364 .setExecutedAt(executedAt)
365 .setStatus(CeActivityDto.Status.FAILED));
366 dbTester.getSession().commit();
369 private ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> verifyAndCaptureSerializedNotification() {
370 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = ArgumentCaptor.forClass(ReportAnalysisFailureNotificationBuilder.class);
371 verify(serializer).toNotification(notificationCaptor.capture());
372 return notificationCaptor;
375 private Duration randomDuration() {
376 return Duration.of(random.nextLong(), ChronoUnit.MILLIS);