]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7024 add notification on Report Analysis failures
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 14 Sep 2017 15:16:49 +0000 (17:16 +0200)
committerEric Hartmann <hartmann.eric@gmail.Com>
Mon, 2 Oct 2017 11:03:35 +0000 (13:03 +0200)
22 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java [new file with mode: 0644]
server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java [new file with mode: 0644]
server/sonar-ce/src/main/java/org/sonar/ce/taskprocessor/CeTaskProcessorModule.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java [new file with mode: 0644]
server/sonar-ce/src/test/java/org/sonar/ce/taskprocessor/CeTaskProcessorModuleTest.java
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 3b17e7b545cb7994eb5fdc7a77aa2735fe3eb76c..da99c929216fd723c99e0509186f639e8eef966f 100644 (file)
@@ -49,6 +49,7 @@ import org.sonar.ce.StandaloneCeDistributedInformation;
 import org.sonar.ce.cleaning.CeCleaningModule;
 import org.sonar.ce.db.ReadOnlyPropertiesDao;
 import org.sonar.ce.log.CeProcessLogging;
+import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule;
 import org.sonar.ce.platform.ComputeEngineExtensionInstaller;
 import org.sonar.ce.queue.CeQueueCleaner;
 import org.sonar.ce.queue.PurgeCeActivities;
@@ -398,6 +399,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       NotificationService.class,
       DefaultNotificationManager.class,
       EmailNotificationChannel.class,
+      ReportAnalysisFailureNotificationModule.class,
 
       // Tests
       TestIndexer.class,
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java b/server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java
new file mode 100644 (file)
index 0000000..4a4c92a
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import javax.annotation.Nullable;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.queue.CeTask;
+import org.sonar.ce.queue.CeTaskResult;
+import org.sonar.ce.taskprocessor.CeWorker;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.RowNotFoundException;
+import org.sonar.db.ce.CeActivityDto;
+import org.sonar.db.ce.CeTaskTypes;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.notification.NotificationService;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static java.util.Collections.singleton;
+
+public class ReportAnalysisFailureNotificationExecutionListener implements CeWorker.ExecutionListener {
+  private final NotificationService notificationService;
+  private final DbClient dbClient;
+  private final ReportAnalysisFailureNotificationSerializer taskFailureNotificationSerializer;
+  private final System2 system2;
+
+  public ReportAnalysisFailureNotificationExecutionListener(NotificationService notificationService, DbClient dbClient,
+    ReportAnalysisFailureNotificationSerializer taskFailureNotificationSerializer, System2 system2) {
+    this.notificationService = notificationService;
+    this.dbClient = dbClient;
+    this.taskFailureNotificationSerializer = taskFailureNotificationSerializer;
+    this.system2 = system2;
+  }
+
+  @Override
+  public void onStart(CeTask ceTask) {
+    // nothing to do
+  }
+
+  @Override
+  public void onEnd(CeTask ceTask, CeActivityDto.Status status, @Nullable CeTaskResult taskResult, @Nullable Throwable error) {
+    if (status == CeActivityDto.Status.SUCCESS) {
+      return;
+    }
+    String projectUuid = ceTask.getComponentUuid();
+    if (!CeTaskTypes.REPORT.equals(ceTask.getType()) || projectUuid == null) {
+      return;
+    }
+
+    if (notificationService.hasProjectSubscribersForTypes(projectUuid, singleton(ReportAnalysisFailureNotification.TYPE))) {
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        ComponentDto projectDto = dbClient.componentDao().selectOrFailByUuid(dbSession, projectUuid);
+        checkScopeAndQualifier(projectDto);
+        CeActivityDto ceActivityDto = dbClient.ceActivityDao().selectByUuid(dbSession, ceTask.getUuid())
+          .orElseThrow(() -> new RowNotFoundException(format("CeActivity with uuid '%s' not found", ceTask.getUuid())));
+        ReportAnalysisFailureNotification taskFailureNotification = buildNotification(ceActivityDto, projectDto, error);
+        notificationService.deliver(taskFailureNotificationSerializer.toNotification(taskFailureNotification));
+      }
+    }
+  }
+
+  /**
+   * @throws IllegalArgumentException if specified {@link ComponentDto} is not a project.
+   */
+  private static void checkScopeAndQualifier(ComponentDto projectDto) {
+    String scope = projectDto.scope();
+    String qualifier = projectDto.qualifier();
+    checkArgument(
+      scope.equals(Scopes.PROJECT) && qualifier.equals(Qualifiers.PROJECT),
+      "Component %s must be a project (scope=%s, qualifier=%s)", projectDto.uuid(), scope, qualifier);
+  }
+
+  private ReportAnalysisFailureNotification buildNotification(CeActivityDto ceActivityDto, ComponentDto projectDto, @Nullable Throwable error) {
+    Long executedAt = ceActivityDto.getExecutedAt();
+    return new ReportAnalysisFailureNotification(
+      new ReportAnalysisFailureNotification.Project(
+        projectDto.uuid(),
+        projectDto.getKey(),
+        projectDto.name(),
+        projectDto.getBranch()),
+      new ReportAnalysisFailureNotification.Task(
+        ceActivityDto.getUuid(),
+        ceActivityDto.getSubmittedAt(),
+        executedAt == null ? system2.now() : executedAt),
+      error == null ? null : error.getMessage());
+  }
+}
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java b/server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java
new file mode 100644 (file)
index 0000000..da3dc0b
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.ce.notification;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index ece8e9d69259d57f0dc053d2b223967fa5f77d10..8cfde5df5d3f400b940f15a38f426da75cf2f0a8 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.ce.taskprocessor;
 
+import org.sonar.ce.notification.ReportAnalysisFailureNotificationExecutionListener;
 import org.sonar.core.platform.Module;
 
 public class CeTaskProcessorModule extends Module {
@@ -27,6 +28,7 @@ public class CeTaskProcessorModule extends Module {
     add(
       CeTaskProcessorRepositoryImpl.class,
       CeLoggingWorkerExecutionListener.class,
+      ReportAnalysisFailureNotificationExecutionListener.class,
       CeWorkerFactoryImpl.class,
       EnabledCeWorkerControllerImpl.class,
       CeProcessingSchedulerExecutorServiceImpl.class,
index 74fed0f8a2e435ca558c125182300ce5d7a18f7c..fefec4144f957770991bd7158ca2c2ef1c072c31 100644 (file)
@@ -129,13 +129,14 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getComponentAdapters())
       .hasSize(
         CONTAINER_ITSELF
-          + 74 // level 4
+          + 75 // level 4
           + 4 // content of CeConfigurationModule
           + 7 // content of CeQueueModule
           + 4 // content of CeHttpModule
           + 3 // content of CeTaskCommonsModule
           + 4 // content of ProjectAnalysisTaskModule
-          + 6 // content of CeTaskProcessorModule
+          + 7 // content of CeTaskProcessorModule
+          + 4 // content of ReportAnalysisFailureNotificationModule
           + 3 // CeCleaningModule + its content
           + 1 // CeDistributedInformation
     );
diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java
new file mode 100644 (file)
index 0000000..2c305d3
--- /dev/null
@@ -0,0 +1,318 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import java.util.Arrays;
+import java.util.Random;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.utils.System2;
+import org.sonar.ce.queue.CeTask;
+import org.sonar.ce.queue.CeTaskResult;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.RowNotFoundException;
+import org.sonar.db.ce.CeActivityDto;
+import org.sonar.db.ce.CeQueueDto;
+import org.sonar.db.ce.CeTaskTypes;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.server.notification.NotificationService;
+
+import static java.util.Collections.singleton;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.component.ComponentTesting.newDirectory;
+import static org.sonar.db.component.ComponentTesting.newModuleDto;
+
+public class ReportAnalysisFailureNotificationExecutionListenerTest {
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private final Random random = new Random();
+  private DbClient dbClient = dbTester.getDbClient();
+  private NotificationService notificationService = mock(NotificationService.class);
+  private ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class);
+  private System2 system2 = mock(System2.class);
+  private DbClient dbClientMock = mock(DbClient.class);
+  private CeTask ceTaskMock = mock(CeTask.class);
+  private Throwable throwableMock = mock(Throwable.class);
+  private CeTaskResult ceTaskResultMock = mock(CeTaskResult.class);
+  private ReportAnalysisFailureNotificationExecutionListener fullMockedUnderTest = new ReportAnalysisFailureNotificationExecutionListener(
+    notificationService, dbClientMock, serializer, system2);
+  private ReportAnalysisFailureNotificationExecutionListener underTest = new ReportAnalysisFailureNotificationExecutionListener(
+    notificationService, dbClient, serializer, system2);
+
+  @Before
+  public void setUp() throws Exception {
+
+  }
+
+  @Test
+  public void onStart_has_no_effect() {
+    CeTask mockedCeTask = mock(CeTask.class);
+
+    fullMockedUnderTest.onStart(mockedCeTask);
+
+    verifyZeroInteractions(mockedCeTask, notificationService, dbClientMock, serializer, system2);
+  }
+
+  @Test
+  public void onEnd_has_no_effect_if_status_is_SUCCESS() {
+    fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.SUCCESS, ceTaskResultMock, throwableMock);
+
+    verifyZeroInteractions(ceTaskMock, ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
+  }
+
+  @Test
+  public void onEnd_has_no_effect_if_CeTask_type_is_not_report() {
+    when(ceTaskMock.getType()).thenReturn(randomAlphanumeric(12));
+
+    fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+    verifyZeroInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
+  }
+
+  @Test
+  public void onEnd_has_no_effect_if_CeTask_has_no_component_uuid() {
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+
+    fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+    verifyZeroInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2);
+  }
+
+  @Test
+  public void onEnd_has_no_effect_if_there_is_no_subscriber_for_ReportAnalysisFailureNotification_type() {
+    String componentUuid = randomAlphanumeric(6);
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+    when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid);
+    when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE)))
+      .thenReturn(false);
+
+    fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+    verifyZeroInteractions(ceTaskResultMock, throwableMock, dbClientMock, serializer, system2);
+  }
+
+  @Test
+  public void onEnd_fails_with_RowNotFoundException_if_component_does_not_exist_in_DB() {
+    String componentUuid = randomAlphanumeric(6);
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+    when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid);
+    when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE)))
+      .thenReturn(true);
+
+    expectedException.expect(RowNotFoundException.class);
+    expectedException.expectMessage("Component with uuid '" + componentUuid + "' not found");
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+  }
+
+  @Test
+  public void onEnd_fails_with_IAE_if_component_is_not_a_project() {
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+    OrganizationDto organization = OrganizationTesting.newOrganizationDto();
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    ComponentDto module = dbTester.components().insertComponent(newModuleDto(project));
+    ComponentDto directory = dbTester.components().insertComponent(newDirectory(module, randomAlphanumeric(12)));
+    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
+    ComponentDto view = dbTester.components().insertComponent(ComponentTesting.newView(organization));
+    ComponentDto subView = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto projectCopy = dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project, subView));
+    ComponentDto application = dbTester.components().insertComponent(ComponentTesting.newApplication(organization));
+
+    Arrays.asList(module, directory, file, view, subView, projectCopy, application)
+      .forEach(component -> {
+        try {
+          when(ceTaskMock.getComponentUuid()).thenReturn(component.uuid());
+          when(notificationService.hasProjectSubscribersForTypes(component.uuid(), singleton(ReportAnalysisFailureNotification.TYPE)))
+            .thenReturn(true);
+
+          underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+          fail("An IllegalArgumentException should have been thrown for component " + component);
+        } catch (IllegalArgumentException e) {
+          assertThat(e.getMessage()).isEqualTo(String.format(
+            "Component %s must be a project (scope=%s, qualifier=%s)", component.uuid(), component.scope(), component.qualifier()));
+        }
+      });
+  }
+
+  @Test
+  public void onEnd_fails_with_RowNotFoundException_if_activity_for_task_does_not_exist_in_DB() {
+    String componentUuid = randomAlphanumeric(6);
+    String taskUuid = randomAlphanumeric(6);
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+    when(ceTaskMock.getUuid()).thenReturn(taskUuid);
+    when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid);
+    when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE)))
+      .thenReturn(true);
+    dbTester.components().insertPrivateProject(s -> s.setUuid(componentUuid));
+
+    expectedException.expect(RowNotFoundException.class);
+    expectedException.expectMessage("CeActivity with uuid '" + taskUuid + "' not found");
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+  }
+
+  @Test
+  public void onEnd_creates_notification_with_data_from_activity_and_project_and_deliver_it() {
+    String taskUuid = randomAlphanumeric(12);
+    int createdAt = random.nextInt(999_999);
+    long executedAt = random.nextInt(999_999);
+    ComponentDto project = initMocksToPassConditions(taskUuid, createdAt, executedAt);
+    Notification notificationMock = mockSerializer();
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+    ArgumentCaptor<ReportAnalysisFailureNotification> notificationCaptor = verifyAndCaptureSerializedNotification();
+    verify(notificationService).deliver(same(notificationMock));
+
+    ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue();
+
+    ReportAnalysisFailureNotification.Project notificationProject = reportAnalysisFailureNotification.getProject();
+    assertThat(notificationProject.getName()).isEqualTo(project.name());
+    assertThat(notificationProject.getKey()).isEqualTo(project.getKey());
+    assertThat(notificationProject.getUuid()).isEqualTo(project.uuid());
+    assertThat(notificationProject.getBranchName()).isEqualTo(project.getBranch());
+    ReportAnalysisFailureNotification.Task notificationTask = reportAnalysisFailureNotification.getTask();
+    assertThat(notificationTask.getUuid()).isEqualTo(taskUuid);
+    assertThat(notificationTask.getCreatedAt()).isEqualTo(createdAt);
+    assertThat(notificationTask.getFailedAt()).isEqualTo(executedAt);
+  }
+
+  @Test
+  public void onEnd_creates_notification_with_error_message_from_Throwable_argument_message() {
+    initMocksToPassConditions(randomAlphanumeric(12), random.nextInt(999_999), (long) random.nextInt(999_999));
+    String message = randomAlphanumeric(66);
+    when(throwableMock.getMessage()).thenReturn(message);
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock);
+
+    ArgumentCaptor<ReportAnalysisFailureNotification> notificationCaptor = verifyAndCaptureSerializedNotification();
+
+    ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue();
+    assertThat(reportAnalysisFailureNotification.getErrorMessage()).isEqualTo(message);
+  }
+
+  @Test
+  public void onEnd_creates_notification_with_null_error_message_if_Throwable_is_null() {
+    String taskUuid = randomAlphanumeric(12);
+    initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
+    Notification notificationMock = mockSerializer();
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null);
+
+    verify(notificationService).deliver(same(notificationMock));
+    ArgumentCaptor<ReportAnalysisFailureNotification> notificationCaptor = verifyAndCaptureSerializedNotification();
+
+    ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue();
+    assertThat(reportAnalysisFailureNotification.getErrorMessage()).isNull();
+  }
+
+  @Test
+  public void onEnd_ignores_null_CeTaskResult_argument() {
+    String taskUuid = randomAlphanumeric(12);
+    initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
+    Notification notificationMock = mockSerializer();
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, null, null);
+
+    verify(notificationService).deliver(same(notificationMock));
+  }
+
+  @Test
+  public void onEnd_ignores_CeTaskResult_argument() {
+    String taskUuid = randomAlphanumeric(12);
+    initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999));
+    Notification notificationMock = mockSerializer();
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null);
+
+    verify(notificationService).deliver(same(notificationMock));
+    verifyZeroInteractions(ceTaskResultMock);
+  }
+
+  @Test
+  public void onEnd_uses_system_data_as_failedAt_if_task_has_no_executedAt() {
+    String taskUuid = randomAlphanumeric(12);
+    initMocksToPassConditions(taskUuid, random.nextInt(999_999), null);
+    long now = random.nextInt(999_999);
+    when(system2.now()).thenReturn(now);
+    Notification notificationMock = mockSerializer();
+
+    underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null);
+
+    verify(notificationService).deliver(same(notificationMock));
+    ArgumentCaptor<ReportAnalysisFailureNotification> notificationCaptor = verifyAndCaptureSerializedNotification();
+    assertThat(notificationCaptor.getValue().getTask().getFailedAt()).isEqualTo(now);
+  }
+
+  private Notification mockSerializer() {
+    Notification notificationMock = mock(Notification.class);
+    when(serializer.toNotification(any(ReportAnalysisFailureNotification.class))).thenReturn(notificationMock);
+    return notificationMock;
+  }
+
+  private ComponentDto initMocksToPassConditions(String taskUuid, int createdAt, @Nullable Long executedAt) {
+    ComponentDto project = random.nextBoolean() ? dbTester.components().insertPrivateProject() : dbTester.components().insertPublicProject();
+    when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT);
+    when(ceTaskMock.getComponentUuid()).thenReturn(project.uuid());
+    when(ceTaskMock.getUuid()).thenReturn(taskUuid);
+    when(notificationService.hasProjectSubscribersForTypes(project.uuid(), singleton(ReportAnalysisFailureNotification.TYPE)))
+      .thenReturn(true);
+    insertActivityDto(taskUuid, createdAt, executedAt, project);
+    return project;
+  }
+
+  private void insertActivityDto(String taskUuid, int createdAt, @Nullable Long executedAt, ComponentDto project) {
+    dbClient.ceActivityDao().insert(dbTester.getSession(), new CeActivityDto(new CeQueueDto()
+      .setUuid(taskUuid)
+      .setTaskType(CeTaskTypes.REPORT)
+      .setComponentUuid(project.uuid())
+      .setCreatedAt(createdAt))
+        .setExecutedAt(executedAt)
+        .setStatus(CeActivityDto.Status.FAILED));
+    dbTester.getSession().commit();
+  }
+
+  private ArgumentCaptor<ReportAnalysisFailureNotification> verifyAndCaptureSerializedNotification() {
+    ArgumentCaptor<ReportAnalysisFailureNotification> notificationCaptor = ArgumentCaptor.forClass(ReportAnalysisFailureNotification.class);
+    verify(serializer).toNotification(notificationCaptor.capture());
+    return notificationCaptor;
+  }
+}
index ae8f1faacf8ebc27a2af3ae1fa03adcc421595da..73a65dfead7fa584b7f7c90cac9499a8df487602 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.ce.taskprocessor;
 
 import org.junit.Test;
 import org.picocontainer.ComponentAdapter;
+import org.sonar.ce.notification.ReportAnalysisFailureNotificationExecutionListener;
 import org.sonar.core.platform.ComponentContainer;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -39,4 +40,16 @@ public class CeTaskProcessorModuleTest {
       .map(ComponentAdapter::getComponentImplementation))
         .contains(CeLoggingWorkerExecutionListener.class);
   }
+
+  @Test
+  public void defines_ExecutionListener_for_report_processing_failure_notifications() {
+    ComponentContainer container = new ComponentContainer();
+
+    underTest.configure(container);
+
+    assertThat(container.getPicoContainer().getComponentAdapters(CeWorker.ExecutionListener.class)
+      .stream()
+      .map(ComponentAdapter::getComponentImplementation))
+        .contains(ReportAnalysisFailureNotificationExecutionListener.class);
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java
new file mode 100644 (file)
index 0000000..0b424db
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+public class ReportAnalysisFailureNotification {
+  public static final String TYPE = "ce-report-task-failure";
+
+  private final Project project;
+  private final Task task;
+  private final String errorMessage;
+
+  public ReportAnalysisFailureNotification(Project project, Task task, @Nullable String errorMessage) {
+    this.project = requireNonNull(project, "project can't be null");
+    this.task = requireNonNull(task, "task can't be null");
+    this.errorMessage = errorMessage;
+  }
+
+  public Project getProject() {
+    return project;
+  }
+
+  public Task getTask() {
+    return task;
+  }
+
+  @CheckForNull
+  public String getErrorMessage() {
+    return errorMessage;
+  }
+
+  public static final class Project {
+    private final String uuid;
+    private final String key;
+    private final String name;
+    private final String branchName;
+
+    public Project(String uuid, String key, String name, @Nullable String branchName) {
+      this.uuid = requireNonNull(uuid, "uuid can't be null");
+      this.key = requireNonNull(key, "key can't be null");
+      this.name = requireNonNull(name, "name can't be null");
+      this.branchName = branchName;
+    }
+
+    public String getUuid() {
+      return uuid;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    @CheckForNull
+    public String getBranchName() {
+      return branchName;
+    }
+  }
+
+  public static final class Task {
+    private final String uuid;
+    private final long createdAt;
+    private final long failedAt;
+
+    public Task(String uuid, long createdAt, long failedAt) {
+      this.uuid = requireNonNull(uuid, "uuid can't be null");
+      this.createdAt = createdAt;
+      this.failedAt = failedAt;
+    }
+
+    public String getUuid() {
+      return uuid;
+    }
+
+    public long getCreatedAt() {
+      return createdAt;
+    }
+
+    public long getFailedAt() {
+      return failedAt;
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java
new file mode 100644 (file)
index 0000000..574bb17
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.Map;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.server.notification.NotificationDispatcher;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationManager;
+
+public class ReportAnalysisFailureNotificationDispatcher extends NotificationDispatcher {
+
+  public static final String KEY = "CeReportTaskFailure";
+
+  private final NotificationManager manager;
+
+  public ReportAnalysisFailureNotificationDispatcher(NotificationManager manager) {
+    super(ReportAnalysisFailureNotification.TYPE);
+    this.manager = manager;
+  }
+
+  public static NotificationDispatcherMetadata newMetadata() {
+    return NotificationDispatcherMetadata.create(KEY)
+      .setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
+      .setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
+  }
+
+  @Override
+  public String getKey() {
+    return KEY;
+  }
+
+  @Override
+  public void dispatch(Notification notification, Context context) {
+    String projectUuid = notification.getFieldValue("project.uuid");
+    Multimap<String, NotificationChannel> subscribedRecipients = manager.findSubscribedRecipientsForDispatcher(this, projectUuid);
+
+    for (Map.Entry<String, Collection<NotificationChannel>> channelsByRecipients : subscribedRecipients.asMap().entrySet()) {
+      String userLogin = channelsByRecipients.getKey();
+      for (NotificationChannel channel : channelsByRecipients.getValue()) {
+        context.addUser(userLogin, channel);
+      }
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java
new file mode 100644 (file)
index 0000000..22e16fd
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.plugins.emailnotifications.api.EmailMessage;
+import org.sonar.plugins.emailnotifications.api.EmailTemplate;
+
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+
+public class ReportAnalysisFailureNotificationEmailTemplate extends EmailTemplate {
+  private static final char LINE_RETURN = '\n';
+  private static final char TAB = '\t';
+
+  private final ReportAnalysisFailureNotificationSerializer serializer;
+  protected final EmailSettings settings;
+
+  public ReportAnalysisFailureNotificationEmailTemplate(ReportAnalysisFailureNotificationSerializer serializer, EmailSettings settings) {
+    this.serializer = serializer;
+    this.settings = settings;
+  }
+
+  @Override
+  public EmailMessage format(Notification notification) {
+    if (!ReportAnalysisFailureNotification.TYPE.equals(notification.getType())) {
+      return null;
+    }
+
+    ReportAnalysisFailureNotification taskFailureNotification = serializer.fromNotification(notification);
+    String projectUuid = taskFailureNotification.getProject().getUuid();
+    String projectFullName = computeProjectFullName(taskFailureNotification.getProject());
+
+    return new EmailMessage()
+      .setMessageId(notification.getType() + "/" + projectUuid)
+      .setSubject(subject(projectFullName))
+      .setMessage(message(projectFullName, taskFailureNotification));
+  }
+
+  private static String computeProjectFullName(ReportAnalysisFailureNotification.Project project) {
+    String branchName = project.getBranchName();
+    if (branchName != null) {
+      return String.format("%s (%s)", project.getName(), branchName);
+    }
+    return project.getName();
+  }
+
+  private static String subject(String projectFullName) {
+    return String.format("%s: Background task in failure", projectFullName);
+  }
+
+  private String message(String projectFullName, ReportAnalysisFailureNotification taskFailureNotification) {
+    ReportAnalysisFailureNotification.Project project = taskFailureNotification.getProject();
+    ReportAnalysisFailureNotification.Task task = taskFailureNotification.getTask();
+
+    StringBuilder res = new StringBuilder();
+    res.append("Project:").append(TAB).append(projectFullName).append(LINE_RETURN);
+    res.append("Background task:").append(TAB).append(task.getUuid()).append(LINE_RETURN);
+    res.append("Submission time:").append(TAB).append(formatDateTime(task.getCreatedAt())).append(LINE_RETURN);
+    res.append("Failure time:").append(TAB).append(formatDateTime(task.getFailedAt())).append(LINE_RETURN);
+
+    String errorMessage = taskFailureNotification.getErrorMessage();
+    if (errorMessage != null) {
+      res.append(LINE_RETURN);
+      res.append("Error message:").append(TAB).append(errorMessage).append(LINE_RETURN);
+    }
+
+    res.append(LINE_RETURN);
+    res.append("More details at: ").append(String.format("%s/project/background_tasks?id=%s", settings.getServerBaseURL(), encode(project.getKey())));
+
+    return res.toString();
+  }
+
+  private static String encode(String toEncode) {
+    try {
+      return URLEncoder.encode(toEncode, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException("Encoding not supported", e);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java
new file mode 100644 (file)
index 0000000..8c86320
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import org.sonar.core.platform.Module;
+
+public class ReportAnalysisFailureNotificationModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      ReportAnalysisFailureNotificationDispatcher.class,
+      ReportAnalysisFailureNotificationDispatcher.newMetadata(),
+      ReportAnalysisFailureNotificationSerializerImpl.class,
+      ReportAnalysisFailureNotificationEmailTemplate.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java
new file mode 100644 (file)
index 0000000..9409692
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import org.sonar.api.notifications.Notification;
+
+public interface ReportAnalysisFailureNotificationSerializer {
+  Notification toNotification(ReportAnalysisFailureNotification reportAnalysisFailureNotification);
+
+  ReportAnalysisFailureNotification fromNotification(Notification notification);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java
new file mode 100644 (file)
index 0000000..022568a
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import org.sonar.api.notifications.Notification;
+
+import static java.lang.String.valueOf;
+
+public class ReportAnalysisFailureNotificationSerializerImpl implements ReportAnalysisFailureNotificationSerializer {
+  private static final String FIELD_PROJECT_UUID = "project.uuid";
+  private static final String FIELD_PROJECT_KEY = "project.key";
+  private static final String FIELD_PROJECT_NAME = "project.name";
+  private static final String FIELD_PROJECT_BRANCH = "project.branchName";
+  private static final String FIELD_TASK_UUID = "task.uuid";
+  private static final String FIELD_TASK_CREATED_AT = "task.createdAt";
+  private static final String FIELD_TASK_FAILED_AT = "task.failedAt";
+  private static final String FIELD_ERROR_MESSAGE = "error.message";
+
+  @Override
+  public Notification toNotification(ReportAnalysisFailureNotification reportAnalysisFailureNotification) {
+    return new Notification(ReportAnalysisFailureNotification.TYPE)
+      .setFieldValue(FIELD_PROJECT_UUID, reportAnalysisFailureNotification.getProject().getUuid())
+      .setFieldValue(FIELD_PROJECT_KEY, reportAnalysisFailureNotification.getProject().getKey())
+      .setFieldValue(FIELD_PROJECT_NAME, reportAnalysisFailureNotification.getProject().getName())
+      .setFieldValue(FIELD_PROJECT_BRANCH, reportAnalysisFailureNotification.getProject().getBranchName())
+      .setFieldValue(FIELD_TASK_UUID, reportAnalysisFailureNotification.getTask().getUuid())
+      .setFieldValue(FIELD_TASK_CREATED_AT, valueOf(reportAnalysisFailureNotification.getTask().getCreatedAt()))
+      .setFieldValue(FIELD_TASK_FAILED_AT, valueOf(reportAnalysisFailureNotification.getTask().getFailedAt()))
+      .setFieldValue(FIELD_ERROR_MESSAGE, reportAnalysisFailureNotification.getErrorMessage());
+  }
+
+  @Override
+  public ReportAnalysisFailureNotification fromNotification(Notification notification) {
+    return new ReportAnalysisFailureNotification(
+      new ReportAnalysisFailureNotification.Project(
+        notification.getFieldValue(FIELD_PROJECT_UUID),
+        notification.getFieldValue(FIELD_PROJECT_KEY),
+        notification.getFieldValue(FIELD_PROJECT_NAME),
+        notification.getFieldValue(FIELD_PROJECT_BRANCH)),
+      new ReportAnalysisFailureNotification.Task(
+        notification.getFieldValue(FIELD_TASK_UUID),
+        Long.valueOf(notification.getFieldValue(FIELD_TASK_CREATED_AT)),
+        Long.valueOf(notification.getFieldValue(FIELD_TASK_FAILED_AT))),
+      notification.getFieldValue(FIELD_ERROR_MESSAGE));
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java
new file mode 100644 (file)
index 0000000..da3dc0b
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.ce.notification;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 86d7c3b7610de00a53ddeeba7aeaa14fb317d47a..1e9b0f22ddd64e656ccd11ae6e0b85d69018e46b 100644 (file)
@@ -21,9 +21,7 @@ package org.sonar.server.computation.task.projectanalysis.container;
 
 import java.util.Arrays;
 import java.util.List;
-
 import javax.annotation.Nullable;
-
 import org.sonar.ce.organization.DefaultOrganizationLoader;
 import org.sonar.ce.queue.CeTask;
 import org.sonar.ce.settings.SettingsLoader;
index 0ef1562a4898bd412738fb8bebd960c2e04a6f4e..fcbf8711124c4a52d0fe2f2b0eb976c85e712c93 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.api.rules.AnnotationRuleParser;
 import org.sonar.api.rules.XMLRuleParser;
 import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
 import org.sonar.ce.CeModule;
+import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule;
 import org.sonar.ce.settings.ProjectConfigurationFactory;
 import org.sonar.core.component.DefaultResourceTypes;
 import org.sonar.core.timemachine.Periods;
@@ -520,6 +521,7 @@ public class PlatformLevel4 extends PlatformLevel {
       PrivilegedPluginsStopper.class,
 
       // Compute engine (must be after Views and Developer Cockpit)
+      ReportAnalysisFailureNotificationModule.class,
       CeModule.class,
       CeWsModule.class,
 
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java
new file mode 100644 (file)
index 0000000..f842b17
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import com.google.common.collect.HashMultimap;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.server.notification.NotificationDispatcher;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationManager;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class ReportAnalysisFailureNotificationDispatcherTest {
+  private NotificationManager notificationManager = mock(NotificationManager.class);
+  private Notification notificationMock = mock(Notification.class);
+  private NotificationDispatcher.Context contextMock = mock(NotificationDispatcher.Context.class);
+  private ReportAnalysisFailureNotificationDispatcher underTest = new ReportAnalysisFailureNotificationDispatcher(notificationManager);
+
+  @Test
+  public void dispatcher_defines_key() {
+    assertThat(underTest.getKey()).isNotEmpty();
+  }
+
+  @Test
+  public void newMetadata_indicates_enabled_global_and_project_level_notifications() {
+    NotificationDispatcherMetadata metadata = ReportAnalysisFailureNotificationDispatcher.newMetadata();
+
+    assertThat(metadata.getProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION)).isEqualTo("true");
+    assertThat(metadata.getProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION)).isEqualTo("true");
+  }
+
+  @Test
+  public void performDispatch_has_no_effect_if_type_is_empty() {
+    when(notificationMock.getType()).thenReturn("");
+
+    underTest.performDispatch(notificationMock, contextMock);
+
+    verify(notificationMock).getType();
+    verifyNoMoreInteractions(notificationMock, contextMock);
+  }
+
+  @Test
+  public void performDispatch_has_no_effect_if_type_is_not_ReportAnalysisFailureNotification_TYPE() {
+    when(notificationMock.getType()).thenReturn(randomAlphanumeric(6));
+
+    underTest.performDispatch(notificationMock, contextMock);
+
+    verify(notificationMock).getType();
+    verifyNoMoreInteractions(notificationMock, contextMock);
+  }
+
+  @Test
+  public void performDispatch_adds_user_for_each_recipient_and_channel_for_the_component_uuid_in_the_notification() {
+    when(notificationMock.getType()).thenReturn(ReportAnalysisFailureNotification.TYPE);
+    String projectUuid = randomAlphanumeric(9);
+    when(notificationMock.getFieldValue("project.uuid")).thenReturn(projectUuid);
+    HashMultimap<String, NotificationChannel> multimap = HashMultimap.create();
+    String login1 = randomAlphanumeric(3);
+    String login2 = randomAlphanumeric(3);
+    NotificationChannel channel1 = mock(NotificationChannel.class);
+    NotificationChannel channel2 = mock(NotificationChannel.class);
+    NotificationChannel channel3 = mock(NotificationChannel.class);
+    multimap.put(login1, channel1);
+    multimap.put(login1, channel2);
+    multimap.put(login2, channel2);
+    multimap.put(login2, channel3);
+    when(notificationManager.findSubscribedRecipientsForDispatcher(underTest, projectUuid))
+      .thenReturn(multimap);
+
+    underTest.performDispatch(notificationMock, contextMock);
+
+    verify(contextMock).addUser(login1, channel1);
+    verify(contextMock).addUser(login1, channel2);
+    verify(contextMock).addUser(login2, channel2);
+    verify(contextMock).addUser(login2, channel3);
+    verifyNoMoreInteractions(contextMock);
+  }
+
+  @Test
+  public void performDispatch_adds_no_user_if_notification_manager_returns_none() {
+    when(notificationMock.getType()).thenReturn(ReportAnalysisFailureNotification.TYPE);
+    String projectUuid = randomAlphanumeric(9);
+    when(notificationMock.getFieldValue("project.uuid")).thenReturn(projectUuid);
+    HashMultimap<String, NotificationChannel> multimap = HashMultimap.create();
+    when(notificationManager.findSubscribedRecipientsForDispatcher(underTest, projectUuid))
+      .thenReturn(multimap);
+
+    underTest.performDispatch(notificationMock, contextMock);
+
+    verifyNoMoreInteractions(contextMock);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java
new file mode 100644 (file)
index 0000000..6fdec2b
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import java.util.Random;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.plugins.emailnotifications.api.EmailMessage;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ReportAnalysisFailureNotificationEmailTemplateTest {
+  private String serverUrl = RandomStringUtils.randomAlphanumeric(12);
+  private Notification notification = new Notification(ReportAnalysisFailureNotification.TYPE);
+  private Random random = new Random();
+  private ReportAnalysisFailureNotification.Project projectNoBranch = new ReportAnalysisFailureNotification.Project(
+    randomAlphanumeric(2),
+    randomAlphanumeric(3),
+    randomAlphanumeric(4),
+    null);
+  private ReportAnalysisFailureNotification.Project projectWithBranch = new ReportAnalysisFailureNotification.Project(
+    randomAlphanumeric(6),
+    randomAlphanumeric(7),
+    randomAlphanumeric(8),
+    randomAlphanumeric(9));
+  private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task(
+    randomAlphanumeric(10),
+    random.nextInt(99),
+    random.nextInt(99));
+  private String errorMessage = randomAlphanumeric(11);
+
+  private ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class);
+  private EmailSettings emailSettings = mock(EmailSettings.class);
+  private ReportAnalysisFailureNotificationEmailTemplate underTest = new ReportAnalysisFailureNotificationEmailTemplate(serializer, emailSettings);
+
+  @Before
+  public void setUp() throws Exception {
+    when(emailSettings.getServerBaseURL()).thenReturn(serverUrl);
+  }
+
+  @Test
+  public void returns_null_if_notification_type_is_not_ReportAnalysisFailureNotification_TYPE() {
+    Notification notification = new Notification(RandomStringUtils.randomAlphanumeric(5));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage).isNull();
+  }
+
+  @Test
+  public void format_returns_email_with_subject_without_branch() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectNoBranch, task, errorMessage));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getSubject()).isEqualTo(projectNoBranch.getName() + ": Background task in failure");
+  }
+
+  @Test
+  public void format_returns_email_with_subject_with_branch() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectWithBranch, task, errorMessage));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getSubject()).isEqualTo(projectWithBranch.getName() + " (" + projectWithBranch.getBranchName() + "): Background task in failure");
+  }
+
+  @Test
+  public void format_returns_email_with_message_without_branch() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectNoBranch, task, errorMessage));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getMessage())
+      .contains("Project:\t" + projectNoBranch.getName() + "\n");
+  }
+
+  @Test
+  public void format_returns_email_with_message_with_branch() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectWithBranch, task, errorMessage));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getMessage())
+      .contains("Project:\t" + projectWithBranch.getName() + " (" + projectWithBranch.getBranchName() + ")\n");
+  }
+
+  @Test
+  public void format_returns_email_with_message_containing_all_information() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectNoBranch, task, errorMessage));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getMessage())
+      .isEqualTo("Project:\t" + projectNoBranch.getName() + "\n" +
+        "Background task:\t" + task.getUuid() + "\n" +
+        "Submission time:\t" + DateUtils.formatDateTime(task.getCreatedAt()) + "\n" +
+        "Failure time:\t" + DateUtils.formatDateTime(task.getFailedAt()) + "\n" +
+        "\n" +
+        "Error message:\t" + errorMessage + "\n" +
+        "\n" +
+        "More details at: " + serverUrl + "/project/background_tasks?id=" + projectNoBranch.getKey());
+  }
+
+  @Test
+  public void format_returns_email_with_message_with_error_message_when_null() {
+    when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification(
+      projectNoBranch, task, null));
+
+    EmailMessage emailMessage = underTest.format(notification);
+
+    assertThat(emailMessage.getMessage())
+      .doesNotContain("Error message:\t");
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java
new file mode 100644 (file)
index 0000000..fd4a25c
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportAnalysisFailureNotificationModuleTest {
+  private ReportAnalysisFailureNotificationModule underTest = new ReportAnalysisFailureNotificationModule();
+
+  @Test
+  public void adds_dispatcher_and_its_metadata() {
+    ComponentContainer container = new ComponentContainer();
+
+    underTest.configure(container);
+
+    assertThat(container.getPicoContainer().getComponentAdapters(NotificationDispatcherMetadata.class)).isNotNull();
+    assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationDispatcher.class)).isNotNull();
+  }
+
+  @Test
+  public void adds_template_and_serializer() {
+    ComponentContainer container = new ComponentContainer();
+
+    underTest.configure(container);
+
+    assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationEmailTemplate.class)).isNotNull();
+    assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationSerializer.class)).isNotNull();
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java
new file mode 100644 (file)
index 0000000..c1c1ee8
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import java.util.Random;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+
+import static java.lang.String.valueOf;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportAnalysisFailureNotificationSerializerImplTest {
+  private Random random = new Random();
+  private ReportAnalysisFailureNotification.Project project = new ReportAnalysisFailureNotification.Project(
+    randomAlphanumeric(2),
+    randomAlphanumeric(3),
+    randomAlphanumeric(4),
+    randomAlphanumeric(5));
+  private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task(
+    randomAlphanumeric(6),
+    random.nextInt(99),
+    random.nextInt(99));
+  private String errorMessage = randomAlphanumeric(7);
+  private ReportAnalysisFailureNotificationSerializerImpl underTest = new ReportAnalysisFailureNotificationSerializerImpl();
+
+  @Test
+  public void verify_toNotification() {
+
+    Notification notification = underTest.toNotification(new ReportAnalysisFailureNotification(project, task, errorMessage));
+
+    assertThat(notification.getFieldValue("project.uuid")).isEqualTo(project.getUuid());
+    assertThat(notification.getFieldValue("project.name")).isEqualTo(project.getName());
+    assertThat(notification.getFieldValue("project.key")).isEqualTo(project.getKey());
+    assertThat(notification.getFieldValue("project.branchName")).isEqualTo(project.getBranchName());
+    assertThat(notification.getFieldValue("task.uuid")).isEqualTo(task.getUuid());
+    assertThat(notification.getFieldValue("task.createdAt")).isEqualTo(valueOf(task.getCreatedAt()));
+    assertThat(notification.getFieldValue("task.failedAt")).isEqualTo(valueOf(task.getFailedAt()));
+    assertThat(notification.getFieldValue("error.message")).isEqualTo(errorMessage);
+  }
+
+  @Test
+  public void verify_fromNotification() {
+    Notification notification = new Notification(randomAlphanumeric(1))
+      .setFieldValue("project.uuid", project.getUuid())
+      .setFieldValue("project.name", project.getName())
+      .setFieldValue("project.key", project.getKey())
+      .setFieldValue("project.branchName", project.getBranchName())
+      .setFieldValue("task.uuid", task.getUuid())
+      .setFieldValue("task.createdAt", valueOf(task.getCreatedAt()))
+      .setFieldValue("task.failedAt", valueOf(task.getFailedAt()))
+      .setFieldValue("error.message", errorMessage);
+
+    ReportAnalysisFailureNotification reportAnalysisFailureNotification = underTest.fromNotification(notification);
+
+    assertThat(reportAnalysisFailureNotification.getProject().getUuid()).isEqualTo(project.getUuid());
+    assertThat(reportAnalysisFailureNotification.getProject().getKey()).isEqualTo(project.getKey());
+    assertThat(reportAnalysisFailureNotification.getProject().getName()).isEqualTo(project.getName());
+    assertThat(reportAnalysisFailureNotification.getProject().getBranchName()).isEqualTo(project.getBranchName());
+    assertThat(reportAnalysisFailureNotification.getTask().getUuid()).isEqualTo(task.getUuid());
+    assertThat(reportAnalysisFailureNotification.getTask().getCreatedAt()).isEqualTo(task.getCreatedAt());
+    assertThat(reportAnalysisFailureNotification.getTask().getFailedAt()).isEqualTo(task.getFailedAt());
+    assertThat(reportAnalysisFailureNotification.getErrorMessage()).isEqualTo(errorMessage);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java
new file mode 100644 (file)
index 0000000..f23e5d6
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ce.notification;
+
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportAnalysisFailureNotificationTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private Random random = new Random();
+  private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task(
+    randomAlphanumeric(2), random.nextInt(5_996), random.nextInt(9_635));
+  private ReportAnalysisFailureNotification.Project project = new ReportAnalysisFailureNotification.Project(
+    randomAlphanumeric(6), randomAlphanumeric(7), randomAlphanumeric(8), randomAlphanumeric(9));
+
+  @Test
+  public void project_constructor_fails_if_uuid_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("uuid can't be null");
+
+    new ReportAnalysisFailureNotification.Project(null, randomAlphanumeric(2), randomAlphanumeric(3), randomAlphanumeric(4));
+  }
+
+  @Test
+  public void project_constructor_fails_if_key_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("key can't be null");
+
+    new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), null, randomAlphanumeric(3), randomAlphanumeric(4));
+  }
+
+  @Test
+  public void project_constructor_fails_if_name_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("name can't be null");
+
+    new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), randomAlphanumeric(3), null, randomAlphanumeric(4));
+  }
+
+  @Test
+  public void verify_report_getters() {
+    String uuid = randomAlphanumeric(2);
+    String key = randomAlphanumeric(3);
+    String name = randomAlphanumeric(4);
+    String branchName = randomAlphanumeric(5);
+    ReportAnalysisFailureNotification.Project underTest = new ReportAnalysisFailureNotification.Project(uuid, key, name, branchName);
+
+    assertThat(underTest.getUuid()).isEqualTo(uuid);
+    assertThat(underTest.getName()).isEqualTo(name);
+    assertThat(underTest.getKey()).isEqualTo(key);
+    assertThat(underTest.getBranchName()).isEqualTo(branchName);
+  }
+
+  @Test
+  public void project_supports_null_branch() {
+    ReportAnalysisFailureNotification.Project underTest = new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), randomAlphanumeric(3), randomAlphanumeric(4), null);
+
+    assertThat(underTest.getBranchName()).isNull();
+  }
+
+  @Test
+  public void task_constructor_fails_if_uuid_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("uuid can't be null");
+
+    new ReportAnalysisFailureNotification.Task(null, random.nextInt(5_996), random.nextInt(9_635));
+  }
+
+  @Test
+  public void verify_task_getters() {
+    String uuid = randomAlphanumeric(6);
+    int createdAt = random.nextInt(5_996);
+    int failedAt = random.nextInt(9_635);
+
+    ReportAnalysisFailureNotification.Task underTest = new ReportAnalysisFailureNotification.Task(uuid, createdAt, failedAt);
+
+    assertThat(underTest.getUuid()).isEqualTo(uuid);
+    assertThat(underTest.getCreatedAt()).isEqualTo(createdAt);
+    assertThat(underTest.getFailedAt()).isEqualTo(failedAt);
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_Project_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("project can't be null");
+
+    new ReportAnalysisFailureNotification(null, task, randomAlphanumeric(99));
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_Task_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("task can't be null");
+
+    new ReportAnalysisFailureNotification(project, null, randomAlphanumeric(99));
+  }
+
+  @Test
+  public void verify_getters() {
+    String message = randomAlphanumeric(99);
+    ReportAnalysisFailureNotification underTest = new ReportAnalysisFailureNotification(project, task, message);
+
+    assertThat(underTest.getProject()).isSameAs(project);
+    assertThat(underTest.getTask()).isSameAs(task);
+    assertThat(underTest.getErrorMessage()).isSameAs(message);
+  }
+
+  @Test
+  public void null_error_message_is_supported() {
+    ReportAnalysisFailureNotification underTest = new ReportAnalysisFailureNotification(project, task, null);
+
+    assertThat(underTest.getErrorMessage()).isNull();
+  }
+}
index e16a23b24fd8e13dd8f08003cf1a39495ed496fc..469ebaa4fe6ed97ca5c75274ca5a3b65be8242b9 100644 (file)
@@ -1862,6 +1862,7 @@ notification.dispatcher.NewIssues=New issues
 notification.dispatcher.NewAlerts=New quality gate status
 notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix
 notification.dispatcher.SQ-MyNewIssues=My new issues
+notification.dispatcher.CeReportTaskFailure=Report processing failure
 
 
 #------------------------------------------------------------------------------