3 * Copyright (C) 2009-2023 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.ComponentDto;
46 import org.sonar.db.component.ComponentTesting;
47 import org.sonar.db.project.ProjectDto;
48 import org.sonar.server.notification.NotificationService;
50 import static java.util.Collections.singleton;
51 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
52 import static org.assertj.core.api.Assertions.assertThat;
53 import static org.assertj.core.api.Assertions.assertThatThrownBy;
54 import static org.assertj.core.api.Assertions.fail;
55 import static org.mockito.ArgumentMatchers.any;
56 import static org.mockito.ArgumentMatchers.same;
57 import static org.mockito.Mockito.mock;
58 import static org.mockito.Mockito.verify;
59 import static org.mockito.Mockito.verifyNoInteractions;
60 import static org.mockito.Mockito.when;
61 import static org.sonar.db.component.ComponentTesting.newDirectory;
63 public class ReportAnalysisFailureNotificationExecutionListenerIT {
65 public DbTester dbTester = DbTester.create(System2.INSTANCE);
67 private final Random random = new Random();
68 private final DbClient dbClient = dbTester.getDbClient();
69 private final NotificationService notificationService = mock(NotificationService.class);
70 private final ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class);
71 private final System2 system2 = mock(System2.class);
72 private final DbClient dbClientMock = mock(DbClient.class);
73 private final CeTask ceTaskMock = mock(CeTask.class);
74 private final Throwable throwableMock = mock(Throwable.class);
75 private final CeTaskResult ceTaskResultMock = mock(CeTaskResult.class);
76 private final ReportAnalysisFailureNotificationExecutionListener fullMockedUnderTest = new ReportAnalysisFailureNotificationExecutionListener(
77 notificationService, dbClientMock, serializer, system2);
78 private final ReportAnalysisFailureNotificationExecutionListener underTest = new ReportAnalysisFailureNotificationExecutionListener(
79 notificationService, dbClient, serializer, system2);
82 public void onStart_has_no_effect() {
83 CeTask mockedCeTask = mock(CeTask.class);
85 fullMockedUnderTest.onStart(mockedCeTask);
87 verifyNoInteractions(mockedCeTask, notificationService, dbClientMock, serializer, system2);
91 public void onEnd_has_no_effect_if_status_is_SUCCESS() {
92 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.SUCCESS, randomDuration(), ceTaskResultMock, throwableMock);
94 verifyNoInteractions(ceTaskMock, ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
98 public void onEnd_has_no_effect_if_CeTask_type_is_not_report() {
99 when(ceTaskMock.getType()).thenReturn(randomAlphanumeric(12));
101 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
103 verifyNoInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
107 public void onEnd_has_no_effect_if_CeTask_has_no_component_uuid() {
108 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
110 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
112 verifyNoInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
116 public void onEnd_has_no_effect_if_there_is_no_subscriber_for_ReportAnalysisFailureNotification_type() {
117 String componentUuid = randomAlphanumeric(6);
118 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
119 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
120 when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.class)))
123 fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
125 verifyNoInteractions(ceTaskResultMock, throwableMock, dbClientMock, serializer, system2);
129 public void onEnd_fails_with_ISE_if_project_does_not_exist_in_DB() {
130 String componentUuid = randomAlphanumeric(6);
131 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
132 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
133 when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.class)))
136 Duration randomDuration = randomDuration();
137 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
138 .isInstanceOf(IllegalStateException.class)
139 .hasMessage("Could not find project uuid " + componentUuid);
143 public void onEnd_fails_with_ISE_if_branch_does_not_exist_in_DB() {
144 String componentUuid = randomAlphanumeric(6);
145 ProjectDto project = new ProjectDto().setUuid(componentUuid).setKey(randomAlphanumeric(5)).setQualifier(Qualifiers.PROJECT);
146 dbTester.getDbClient().projectDao().insert(dbTester.getSession(), project);
147 dbTester.getSession().commit();
148 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
149 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
150 when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.class)))
153 Duration randomDuration = randomDuration();
154 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
155 .isInstanceOf(IllegalStateException.class)
156 .hasMessage("Could not find a branch for project uuid " + componentUuid);
160 public void onEnd_fails_with_IAE_if_component_is_not_a_project() {
161 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
162 ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
163 ComponentDto directory = dbTester.components().insertComponent(newDirectory(project, randomAlphanumeric(12)));
164 ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
165 ComponentDto view = dbTester.components().insertComponent(ComponentTesting.newPortfolio());
166 ComponentDto subView = dbTester.components().insertComponent(ComponentTesting.newSubPortfolio(view));
167 ComponentDto projectCopy = dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project, subView));
168 ComponentDto application = dbTester.components().insertComponent(ComponentTesting.newApplication());
170 Arrays.asList(directory, file, view, subView, projectCopy, application)
171 .forEach(component -> {
173 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(component.uuid(), null, null)));
174 when(notificationService.hasProjectSubscribersForTypes(component.uuid(), singleton(ReportAnalysisFailureNotification.class)))
177 Duration randomDuration = randomDuration();
179 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock);
181 fail("An IllegalArgumentException should have been thrown for component " + component);
182 } catch (IllegalArgumentException e) {
183 assertThat(e.getMessage()).isEqualTo(String.format("Component %s must be a project (qualifier=%s)", component.uuid(), component.qualifier()));
184 } catch (IllegalStateException e) {
185 assertThat(e.getMessage()).isEqualTo("Could not find project uuid " + component.uuid());
191 public void onEnd_fails_with_RowNotFoundException_if_activity_for_task_does_not_exist_in_DB() {
192 String componentUuid = randomAlphanumeric(6);
193 String taskUuid = randomAlphanumeric(6);
194 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
195 when(ceTaskMock.getUuid()).thenReturn(taskUuid);
196 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(componentUuid, null, null)));
197 when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.class)))
199 dbTester.components().insertPrivateProject(s -> s.setUuid(componentUuid)).getMainBranchComponent();
201 Duration randomDuration = randomDuration();
202 assertThatThrownBy(() -> underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration, ceTaskResultMock, throwableMock))
203 .isInstanceOf(RowNotFoundException.class)
204 .hasMessage("CeActivity with uuid '" + taskUuid + "' not found");
208 public void onEnd_creates_notification_with_data_from_activity_and_project_and_deliver_it() {
209 String taskUuid = randomAlphanumeric(12);
210 int createdAt = random.nextInt(999_999);
211 long executedAt = random.nextInt(999_999);
212 ComponentDto project = initMocksToPassConditions(taskUuid, createdAt, executedAt);
213 Notification notificationMock = mockSerializer();
215 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
217 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
218 verify(notificationService).deliver(same(notificationMock));
220 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
222 ReportAnalysisFailureNotificationBuilder.Project notificationProject = reportAnalysisFailureNotificationBuilder.project();
223 assertThat(notificationProject.name()).isEqualTo(project.name());
224 assertThat(notificationProject.key()).isEqualTo(project.getKey());
225 assertThat(notificationProject.uuid()).isEqualTo(project.uuid());
226 assertThat(notificationProject.branchName()).isNull();
227 ReportAnalysisFailureNotificationBuilder.Task notificationTask = reportAnalysisFailureNotificationBuilder.task();
228 assertThat(notificationTask.uuid()).isEqualTo(taskUuid);
229 assertThat(notificationTask.createdAt()).isEqualTo(createdAt);
230 assertThat(notificationTask.failedAt()).isEqualTo(executedAt);
234 public void onEnd_creates_notification_with_error_message_from_Throwable_argument_message() {
235 initMocksToPassConditions(randomAlphanumeric(12), random.nextInt(999_999), (long) random.nextInt(999_999));
236 String message = randomAlphanumeric(66);
237 when(throwableMock.getMessage()).thenReturn(message);
239 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, throwableMock);
241 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
243 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
244 assertThat(reportAnalysisFailureNotificationBuilder.errorMessage()).isEqualTo(message);
248 public void onEnd_creates_notification_with_null_error_message_if_Throwable_is_null() {
249 String taskUuid = randomAlphanumeric(12);
250 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
251 Notification notificationMock = mockSerializer();
253 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
255 verify(notificationService).deliver(same(notificationMock));
256 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
258 ReportAnalysisFailureNotificationBuilder reportAnalysisFailureNotificationBuilder = notificationCaptor.getValue();
259 assertThat(reportAnalysisFailureNotificationBuilder.errorMessage()).isNull();
263 public void onEnd_ignores_null_CeTaskResult_argument() {
264 String taskUuid = randomAlphanumeric(12);
265 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
266 Notification notificationMock = mockSerializer();
268 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), null, null);
270 verify(notificationService).deliver(same(notificationMock));
274 public void onEnd_ignores_CeTaskResult_argument() {
275 String taskUuid = randomAlphanumeric(12);
276 initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
277 Notification notificationMock = mockSerializer();
279 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
281 verify(notificationService).deliver(same(notificationMock));
282 verifyNoInteractions(ceTaskResultMock);
286 public void onEnd_uses_system_data_as_failedAt_if_task_has_no_executedAt() {
287 String taskUuid = randomAlphanumeric(12);
288 initMocksToPassConditions(taskUuid, random.nextInt(999_999), null);
289 long now = random.nextInt(999_999);
290 when(system2.now()).thenReturn(now);
291 Notification notificationMock = mockSerializer();
293 underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, randomDuration(), ceTaskResultMock, null);
295 verify(notificationService).deliver(same(notificationMock));
296 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = verifyAndCaptureSerializedNotification();
297 assertThat(notificationCaptor.getValue().task().failedAt()).isEqualTo(now);
300 private ReportAnalysisFailureNotification mockSerializer() {
301 ReportAnalysisFailureNotification notificationMock = mock(ReportAnalysisFailureNotification.class);
302 when(serializer.toNotification(any(ReportAnalysisFailureNotificationBuilder.class))).thenReturn(notificationMock);
303 return notificationMock;
306 private ComponentDto initMocksToPassConditions(String taskUuid, int createdAt, @Nullable Long executedAt) {
307 ComponentDto project = random.nextBoolean() ? dbTester.components().insertPrivateProject().getMainBranchComponent() : dbTester.components().insertPublicProject().getMainBranchComponent();
308 when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
309 when(ceTaskMock.getComponent()).thenReturn(Optional.of(new CeTask.Component(project.uuid(), null, null)));
310 when(ceTaskMock.getUuid()).thenReturn(taskUuid);
311 when(notificationService.hasProjectSubscribersForTypes(project.uuid(), singleton(ReportAnalysisFailureNotification.class)))
313 insertActivityDto(taskUuid, createdAt, executedAt, project);
317 private void insertActivityDto(String taskUuid, int createdAt, @Nullable Long executedAt, ComponentDto project) {
318 dbClient.ceActivityDao().insert(dbTester.getSession(), new CeActivityDto(new CeQueueDto()
320 .setTaskType(CeTaskTypes.REPORT)
321 .setComponentUuid(project.uuid())
322 .setCreatedAt(createdAt))
323 .setExecutedAt(executedAt)
324 .setStatus(CeActivityDto.Status.FAILED));
325 dbTester.getSession().commit();
328 private ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> verifyAndCaptureSerializedNotification() {
329 ArgumentCaptor<ReportAnalysisFailureNotificationBuilder> notificationCaptor = ArgumentCaptor.forClass(ReportAnalysisFailureNotificationBuilder.class);
330 verify(serializer).toNotification(notificationCaptor.capture());
331 return notificationCaptor;
334 private Duration randomDuration() {
335 return Duration.of(random.nextLong(), ChronoUnit.MILLIS);