diff options
22 files changed, 1363 insertions, 54 deletions
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 2b82ec5d194..7e8488e0a49 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -24,7 +24,6 @@ import java.util.List; import javax.annotation.CheckForNull; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarQubeVersion; -import org.sonar.api.config.EmailSettings; import org.sonar.api.internal.ApiVersion; import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.profiles.AnnotationProfileParser; @@ -90,11 +89,7 @@ import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.metric.CoreCustomMetrics; import org.sonar.server.metric.DefaultMetricFinder; -import org.sonar.server.notification.DefaultNotificationManager; -import org.sonar.server.notification.NotificationCenter; -import org.sonar.server.notification.NotificationService; -import org.sonar.server.notification.email.AlertsEmailTemplate; -import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.notification.NotificationModule; import org.sonar.server.organization.DefaultOrganizationProviderImpl; import org.sonar.server.permission.GroupPermissionChanger; import org.sonar.server.permission.PermissionTemplateService; @@ -354,7 +349,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { NewIssuesEmailTemplate.class, MyNewIssuesEmailTemplate.class, IssueChangesEmailTemplate.class, - AlertsEmailTemplate.class, ChangesOnMyIssueNotificationDispatcher.class, ChangesOnMyIssueNotificationDispatcher.newMetadata(), NewIssuesNotificationDispatcher.class, @@ -364,17 +358,13 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { DoNotFixNotificationDispatcher.class, DoNotFixNotificationDispatcher.newMetadata(), NewIssuesNotificationFactory.class, // used by SendIssueNotificationsStep - EmailNotificationChannel.class, // technical debt DebtModelPluginRepository.class, DebtRulesXMLImporter.class, // Notifications - EmailSettings.class, - NotificationService.class, - NotificationCenter.class, - DefaultNotificationManager.class, + NotificationModule.class, // Tests TestIndexer.class, diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 87e2eb8a969..0a6c45790e8 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -88,7 +88,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getComponentAdapters()) .hasSize( CONTAINER_ITSELF - + 77 // level 4 + + 83 // level 4 + 4 // content of CeConfigurationModule + 3 // content of CeHttpModule + 5 // content of CeQueueModule diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java index 4a16f197e54..ee15c127b01 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationCenter.java @@ -19,7 +19,7 @@ */ package org.sonar.server.notification; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; @@ -69,7 +69,7 @@ public class NotificationCenter { * If "propertyValue" is null, the verification is done on the existence of such a property (whatever the value). */ public List<String> getDispatcherKeysForProperty(String propertyKey, @Nullable String propertyValue) { - List<String> keys = Lists.newArrayList(); + ImmutableList.Builder<String> keys = ImmutableList.builder(); for (NotificationDispatcherMetadata metadata : dispatchersMetadata) { String dispatcherKey = metadata.getDispatcherKey(); String value = metadata.getProperty(propertyKey); @@ -77,7 +77,7 @@ public class NotificationCenter { keys.add(dispatcherKey); } } - return keys; + return keys.build(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java new file mode 100644 index 00000000000..7c2cc011a66 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationModule.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification; + +import org.sonar.api.config.EmailSettings; +import org.sonar.core.platform.Module; +import org.sonar.server.email.ws.EmailsWsModule; +import org.sonar.server.notification.email.AlertsEmailTemplate; +import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.notification.ws.AddAction; +import org.sonar.server.notification.ws.NotificationsWs; + +public class NotificationModule extends Module { + @Override + protected void configureModule() { + add( + EmailSettings.class, + NotificationService.class, + NotificationCenter.class, + NotificationUpdater.class, + DefaultNotificationManager.class, + EmailsWsModule.class, + NotificationDaemon.class, + AlertsEmailTemplate.class, + EmailNotificationChannel.class, + // WS + NotificationsWs.class, + AddAction.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java new file mode 100644 index 00000000000..417cadaf65b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationUpdater.java @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification; + +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; +import org.sonar.db.property.PropertyQuery; +import org.sonar.server.user.UserSession; + +import static com.google.common.base.Preconditions.checkArgument; + +public class NotificationUpdater { + private static final String PROP_NOTIFICATION_PREFIX = "notification"; + private static final String PROP_NOTIFICATION_VALUE = "true"; + + private final UserSession userSession; + private final DbClient dbClient; + + public NotificationUpdater(UserSession userSession, DbClient dbClient) { + this.userSession = userSession; + this.dbClient = dbClient; + } + + /** + * Add a notification to the current authenticated user. + * Does nothing if the user is not authenticated. + */ + public void add(DbSession dbSession, String channel, String dispatcher, @Nullable ComponentDto project) { + if (!userSession.isLoggedIn()) { + return; + } + + String key = String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel); + Long projectId = project == null ? null : project.getId(); + + List<PropertyDto> existingNotification = dbClient.propertiesDao().selectByQuery( + PropertyQuery.builder() + .setKey(key) + .setComponentId(projectId) + .setUserId(userSession.getUserId()) + .build(), + dbSession); + checkArgument(existingNotification.isEmpty() + || !PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification already added"); + + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto() + .setKey(key) + .setUserId(Long.valueOf(userSession.getUserId())) + .setValue(PROP_NOTIFICATION_VALUE) + .setResourceId(projectId)); + } + + /** + * Remove a notification to the current authenticated user. + * Does nothing if the user is not authenticated. + */ + public void remove(DbSession dbSession, String channel, String dispatcher, @Nullable ComponentDto project) { + if (!userSession.isLoggedIn()) { + return; + } + + String key = String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel); + Long projectId = project == null ? null : project.getId(); + + List<PropertyDto> existingNotification = dbClient.propertiesDao().selectByQuery( + PropertyQuery.builder() + .setKey(key) + .setComponentId(projectId) + .setUserId(userSession.getUserId()) + .build(), + dbSession); + checkArgument(!existingNotification.isEmpty() && PROP_NOTIFICATION_VALUE.equals(existingNotification.get(0).getValue()), "Notification doesn't exist"); + + dbClient.propertiesDao().delete(dbSession, new PropertyDto() + .setKey(key) + .setUserId(Long.valueOf(userSession.getUserId())) + .setValue(PROP_NOTIFICATION_VALUE) + .setResourceId(projectId)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java new file mode 100644 index 00000000000..bef211f4f58 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java @@ -0,0 +1,162 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher; +import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationUpdater; +import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.KeyExamples; +import org.sonarqube.ws.client.notification.AddRequest; + +import static java.util.Optional.empty; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION; +import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.ACTION_ADD; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT; + +public class AddAction implements NotificationsWsAction { + private final NotificationCenter notificationCenter; + private final NotificationUpdater notificationUpdater; + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + private final List<String> globalDispatchers; + private final List<String> projectDispatchers; + + public AddAction(NotificationCenter notificationCenter, NotificationUpdater notificationUpdater, DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.notificationCenter = notificationCenter; + this.notificationUpdater = notificationUpdater; + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true"); + this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true"); + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_ADD) + .setDescription("Add a notification for the authenticated user.<br>" + + "Requires authentication. If a project is provided, requires the 'Browse' permission on the specified project.") + .setSince("6.3") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001); + + List<NotificationChannel> channels = notificationCenter.getChannels(); + action.createParam(PARAM_CHANNEL) + .setDescription("Channel through which the notification is sent. For example, notifications can be sent by email.") + .setPossibleValues(channels) + .setDefaultValue(EmailNotificationChannel.class.getSimpleName()); + + action.createParam(PARAM_NOTIFICATION) + .setDescription("Notification. Possible values are for:" + + "<ul>" + + " <li>Overall notifications: %s</li>" + + " <li>Per project notifications: %s</li>" + + "</ul>", + globalDispatchers.stream().sorted().collect(Collectors.joining(", ")), + projectDispatchers.stream().sorted().collect(Collectors.joining(", "))) + .setRequired(true) + .setExampleValue(MyNewIssuesNotificationDispatcher.KEY); + } + + @Override + public void handle(Request request, Response response) throws Exception { + Stream.of(request) + .map(toWsRequest()) + .peek(checkPermissions()) + .forEach(add()); + + response.noContent(); + } + + private Consumer<AddRequest> add() { + return request -> { + try (DbSession dbSession = dbClient.openSession(false)) { + Optional<ComponentDto> project = searchProject(dbSession, request); + notificationUpdater.add(dbSession, request.getChannel(), request.getNotification(), project.orElse(null)); + dbSession.commit(); + } + }; + } + + private Optional<ComponentDto> searchProject(DbSession dbSession, AddRequest request) { + Optional<ComponentDto> project = request.getProject() == null ? empty() : Optional.of(componentFinder.getByKey(dbSession, request.getProject())); + project.ifPresent(p -> checkRequest(Qualifiers.PROJECT.equals(p.qualifier()) && Scopes.PROJECT.equals(p.scope()), + "Component '%s' must be a project", request.getProject())); + return project; + } + + private Consumer<AddRequest> checkPermissions() { + return request -> userSession.checkLoggedIn(); + } + + private Function<Request, AddRequest> toWsRequest() { + return request -> { + AddRequest.Builder requestBuilder = AddRequest.builder() + .setNotification(request.mandatoryParam(PARAM_NOTIFICATION)) + .setChannel(request.mandatoryParam(PARAM_CHANNEL)); + String project = request.param(PARAM_PROJECT); + setNullable(project, requestBuilder::setProject); + AddRequest wsRequest = requestBuilder.build(); + + if (wsRequest.getProject() == null) { + checkRequest(globalDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", + PARAM_NOTIFICATION, + wsRequest.getNotification(), + globalDispatchers); + } else { + checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", + PARAM_NOTIFICATION, + wsRequest.getNotification(), + projectDispatchers); + } + + return wsRequest; + }; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java new file mode 100644 index 00000000000..eef7c3bfb29 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWs.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import org.sonar.api.server.ws.WebService; + +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.CONTROLLER; + +public class NotificationsWs implements WebService { + private final NotificationsWsAction[] actions; + + public NotificationsWs(NotificationsWsAction[] actions) { + this.actions = actions; + } + + @Override + public void define(Context context) { + NewController controller = context.createController(CONTROLLER) + .setDescription("Manage notifications of the authenticated user") + .setSince("6.3"); + + for (NotificationsWsAction action : actions) { + action.define(controller); + } + + controller.done(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java new file mode 100644 index 00000000000..eb82a25c162 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/NotificationsWsAction.java @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import org.sonar.server.ws.WsAction; + +public interface NotificationsWsAction extends WsAction { + // marker interface +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java new file mode 100644 index 00000000000..0424f2df9be --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java @@ -0,0 +1,162 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher; +import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationUpdater; +import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.user.UserSession; +import org.sonar.server.ws.KeyExamples; +import org.sonarqube.ws.client.notification.AddRequest; + +import static java.util.Optional.empty; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION; +import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.ACTION_ADD; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT; + +public class RemoveAction implements NotificationsWsAction { + private final NotificationCenter notificationCenter; + private final NotificationUpdater notificationUpdater; + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + private final List<String> globalDispatchers; + private final List<String> projectDispatchers; + + public RemoveAction(NotificationCenter notificationCenter, NotificationUpdater notificationUpdater, DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.notificationCenter = notificationCenter; + this.notificationUpdater = notificationUpdater; + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + this.globalDispatchers = notificationCenter.getDispatcherKeysForProperty(GLOBAL_NOTIFICATION, "true"); + this.projectDispatchers = notificationCenter.getDispatcherKeysForProperty(PER_PROJECT_NOTIFICATION, "true"); + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_ADD) + .setDescription("Remove a notification for the authenticated user.<br>" + + "Requires authentication. If a project is provided, requires the 'Browse' permission on the specified project.") + .setSince("6.3") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001); + + List<NotificationChannel> channels = notificationCenter.getChannels(); + action.createParam(PARAM_CHANNEL) + .setDescription("Channel through which the notification is sent. For example, notifications can be sent by email.") + .setPossibleValues(channels) + .setDefaultValue(EmailNotificationChannel.class.getSimpleName()); + + action.createParam(PARAM_NOTIFICATION) + .setDescription("Notification. Possible values are for:" + + "<ul>" + + " <li>Overall notifications: %s</li>" + + " <li>Per project notifications: %s</li>" + + "</ul>", + globalDispatchers.stream().sorted().collect(Collectors.joining(", ")), + projectDispatchers.stream().sorted().collect(Collectors.joining(", "))) + .setRequired(true) + .setExampleValue(MyNewIssuesNotificationDispatcher.KEY); + } + + @Override + public void handle(Request request, Response response) throws Exception { + Stream.of(request) + .map(toWsRequest()) + .peek(checkPermissions()) + .forEach(remove()); + + response.noContent(); + } + + private Consumer<AddRequest> remove() { + return request -> { + try (DbSession dbSession = dbClient.openSession(false)) { + Optional<ComponentDto> project = searchProject(dbSession, request); + notificationUpdater.remove(dbSession, request.getChannel(), request.getNotification(), project.orElse(null)); + dbSession.commit(); + } + }; + } + + private Optional<ComponentDto> searchProject(DbSession dbSession, AddRequest request) { + Optional<ComponentDto> project = request.getProject() == null ? empty() : Optional.of(componentFinder.getByKey(dbSession, request.getProject())); + project.ifPresent(p -> checkRequest(Qualifiers.PROJECT.equals(p.qualifier()) && Scopes.PROJECT.equals(p.scope()), + "Component '%s' must be a project", request.getProject())); + return project; + } + + private Consumer<AddRequest> checkPermissions() { + return request -> userSession.checkLoggedIn(); + } + + private Function<Request, AddRequest> toWsRequest() { + return request -> { + AddRequest.Builder requestBuilder = AddRequest.builder() + .setNotification(request.mandatoryParam(PARAM_NOTIFICATION)) + .setChannel(request.mandatoryParam(PARAM_CHANNEL)); + String project = request.param(PARAM_PROJECT); + setNullable(project, requestBuilder::setProject); + AddRequest wsRequest = requestBuilder.build(); + + if (wsRequest.getProject() == null) { + checkRequest(globalDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", + PARAM_NOTIFICATION, + wsRequest.getNotification(), + globalDispatchers); + } else { + checkRequest(projectDispatchers.contains(wsRequest.getNotification()), "Value of parameter '%s' (%s) must be one of: %s", + PARAM_NOTIFICATION, + wsRequest.getNotification(), + projectDispatchers); + } + + return wsRequest; + }; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java new file mode 100644 index 00000000000..e1a0ac3109f --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/ws/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 9d0ae80c37d..c2dfc70e9d1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -20,7 +20,6 @@ package org.sonar.server.platform.platformlevel; import java.util.List; -import org.sonar.api.config.EmailSettings; import org.sonar.api.profiles.AnnotationProfileParser; import org.sonar.api.profiles.XMLProfileParser; import org.sonar.api.profiles.XMLProfileSerializer; @@ -52,7 +51,6 @@ import org.sonar.server.debt.DebtRulesXMLImporter; import org.sonar.server.duplication.ws.DuplicationsJsonWriter; import org.sonar.server.duplication.ws.DuplicationsParser; import org.sonar.server.duplication.ws.DuplicationsWs; -import org.sonar.server.email.ws.EmailsWsModule; import org.sonar.server.es.IndexCreator; import org.sonar.server.es.IndexDefinitions; import org.sonar.server.event.NewAlerts; @@ -85,12 +83,7 @@ import org.sonar.server.measure.ws.TimeMachineWs; import org.sonar.server.metric.CoreCustomMetrics; import org.sonar.server.metric.DefaultMetricFinder; import org.sonar.server.metric.ws.MetricsWsModule; -import org.sonar.server.notification.DefaultNotificationManager; -import org.sonar.server.notification.NotificationCenter; -import org.sonar.server.notification.NotificationDaemon; -import org.sonar.server.notification.NotificationService; -import org.sonar.server.notification.email.AlertsEmailTemplate; -import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.notification.NotificationModule; import org.sonar.server.organization.ws.OrganizationsWsModule; import org.sonar.server.permission.GroupPermissionChanger; import org.sonar.server.permission.PermissionTemplateService; @@ -396,8 +389,6 @@ public class PlatformLevel4 extends PlatformLevel { DoNotFixNotificationDispatcher.class, DoNotFixNotificationDispatcher.newMetadata(), NewIssuesNotificationFactory.class, - EmailNotificationChannel.class, - AlertsEmailTemplate.class, // issues actions AssignAction.class, @@ -437,12 +428,7 @@ public class PlatformLevel4 extends PlatformLevel { RubyTextService.class, // Notifications - EmailSettings.class, - NotificationService.class, - NotificationCenter.class, - DefaultNotificationManager.class, - EmailsWsModule.class, - NotificationDaemon.class, + NotificationModule.class, // Tests TestsWs.class, diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java index 0440867c6bb..cd4f3c3ab74 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationCenterTest.java @@ -21,31 +21,25 @@ package org.sonar.server.notification; import org.junit.Before; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.sonar.api.notifications.NotificationChannel; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; public class NotificationCenterTest { - @Mock - private NotificationChannel emailChannel; + private NotificationChannel emailChannel = mock(NotificationChannel.class); + private NotificationChannel gtalkChannel = mock(NotificationChannel.class); - @Mock - private NotificationChannel gtalkChannel; - - private NotificationCenter notificationCenter; + private NotificationCenter underTest; @Before public void init() { - MockitoAnnotations.initMocks(this); - NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true").setProperty("on-project", "true"); NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create("Dispatcher2").setProperty("global", "true"); NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create("Dispatcher3").setProperty("global", "FOO").setProperty("on-project", "BAR"); - notificationCenter = new NotificationCenter( + underTest = new NotificationCenter( new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3}, new NotificationChannel[] {emailChannel, gtalkChannel} ); @@ -53,30 +47,30 @@ public class NotificationCenterTest { @Test public void shouldReturnChannels() { - assertThat(notificationCenter.getChannels()).containsOnly(emailChannel, gtalkChannel); + assertThat(underTest.getChannels()).containsOnly(emailChannel, gtalkChannel); } @Test public void shouldReturnDispatcherKeysForSpecificPropertyValue() { - assertThat(notificationCenter.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2"); + assertThat(underTest.getDispatcherKeysForProperty("global", "true")).containsOnly("Dispatcher1", "Dispatcher2"); } @Test public void shouldReturnDispatcherKeysForExistenceOfProperty() { - assertThat(notificationCenter.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3"); + assertThat(underTest.getDispatcherKeysForProperty("on-project", null)).containsOnly("Dispatcher1", "Dispatcher3"); } @Test public void testDefaultConstructors() { - notificationCenter = new NotificationCenter(new NotificationChannel[] {emailChannel}); - assertThat(notificationCenter.getChannels()).hasSize(1); + underTest = new NotificationCenter(new NotificationChannel[] {emailChannel}); + assertThat(underTest.getChannels()).hasSize(1); - notificationCenter = new NotificationCenter(); - assertThat(notificationCenter.getChannels()).hasSize(0); + underTest = new NotificationCenter(); + assertThat(underTest.getChannels()).hasSize(0); - notificationCenter = new NotificationCenter(new NotificationDispatcherMetadata[] {NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true")}); - assertThat(notificationCenter.getChannels()).hasSize(0); - assertThat(notificationCenter.getDispatcherKeysForProperty("global", null)).hasSize(1); + underTest = new NotificationCenter(new NotificationDispatcherMetadata[] {NotificationDispatcherMetadata.create("Dispatcher1").setProperty("global", "true")}); + assertThat(underTest.getChannels()).hasSize(0); + assertThat(underTest.getDispatcherKeysForProperty("global", null)).hasSize(1); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java new file mode 100644 index 00000000000..f879ed4542c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationModuleTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new NotificationModule().configure(container); + assertThat(container.size()).isEqualTo(11 + 2); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java new file mode 100644 index 00000000000..f62f552b95f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/AddActionTest.java @@ -0,0 +1,223 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationUpdater; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.client.notification.AddRequest; + +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.ComponentTesting.newView; +import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION; +import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT; + +public class AddActionTest { + private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1"; + private static final String NOTIF_NEW_ISSUES = "Dispatcher2"; + private static final String NOTIF_NEW_QUALITY_GATE_STATUS = "Dispatcher3"; + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().login().setUserId(123); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private NotificationChannel emailChannel = new FakeNotificationChannel("EmailChannel"); + private NotificationChannel twitterChannel = new FakeNotificationChannel("TwitterChannel"); + // default channel, based on class simple name + private NotificationChannel defaultChannel = new FakeNotificationChannel("EmailNotificationChannel"); + + private NotificationCenter notificationCenter; + private AddAction underTest; + private WsActionTester ws; + + private AddRequest.Builder request = AddRequest.builder() + .setNotification(NOTIF_MY_NEW_ISSUES); + + @Before + public void setUp() { + NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create(NOTIF_MY_NEW_ISSUES) + .setProperty(GLOBAL_NOTIFICATION, "true") + .setProperty(PER_PROJECT_NOTIFICATION, "true"); + NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create(NOTIF_NEW_ISSUES) + .setProperty(GLOBAL_NOTIFICATION, "true"); + NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create(NOTIF_NEW_QUALITY_GATE_STATUS) + .setProperty(GLOBAL_NOTIFICATION, "true") + .setProperty(PER_PROJECT_NOTIFICATION, "true"); + + notificationCenter = new NotificationCenter( + new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3}, + new NotificationChannel[] {emailChannel, twitterChannel, defaultChannel}); + underTest = new AddAction(notificationCenter, new NotificationUpdater(userSession, dbClient), dbClient, new ComponentFinder(dbClient), userSession); + ws = new WsActionTester(underTest); + } + + @Test + public void add_to_email_channel_by_default() { + call(request); + + db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null); + } + + @Test + public void add_to_a_specific_channel() { + call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey())); + + db.notifications().assertExists(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null); + } + + @Test + public void add_a_project_notification() { + ComponentDto project = db.components().insertProject(); + + call(request.setProject(project.getKey())); + + db.notifications().assertExists(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project); + } + + @Test + public void http_no_content() { + TestResponse result = call(request); + + assertThat(result.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + + @Test + public void fail_when_notification_already_exists() { + call(request); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Notification already added"); + + call(request); + } + + @Test + public void fail_when_unknown_channel() { + expectedException.expect(IllegalArgumentException.class); + + call(request.setChannel("Channel42")); + } + + @Test + public void fail_when_unknown_global_dispatcher() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]"); + + call(request.setNotification("Dispatcher42")); + } + + @Test + public void fail_when_unknown_project_dispatcher() { + ComponentDto project = db.components().insertProject(); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]"); + + call(request.setNotification("Dispatcher42").setProject(project.key())); + } + + @Test + public void fail_when_no_dispatcher() { + expectedException.expect(IllegalArgumentException.class); + + ws.newRequest().execute(); + } + + @Test + public void fail_when_project_is_unknown() { + expectedException.expect(NotFoundException.class); + + call(request.setProject("Project-42")); + } + + @Test + public void fail_when_component_is_not_a_project() { + db.components().insertViewAndSnapshot(newView().setKey("VIEW_1")); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Component 'VIEW_1' must be a project"); + + call(request.setProject("VIEW_1")); + } + + @Test + public void fail_when_not_authenticated() { + userSession.anonymous(); + + expectedException.expect(UnauthorizedException.class); + + call(request); + } + + private TestResponse call(AddRequest.Builder wsRequestBuilder) { + AddRequest wsRequest = wsRequestBuilder.build(); + TestRequest request = ws.newRequest(); + request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification()); + setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel)); + setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project)); + return request.execute(); + } + + private static class FakeNotificationChannel extends NotificationChannel { + private final String key; + + private FakeNotificationChannel(String key) { + this.key = key; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public void deliver(Notification notification, String username) { + // do nothing + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java new file mode 100644 index 00000000000..a4634657b98 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/NotificationsWsTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import org.junit.Test; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Controller; +import org.sonar.server.ws.WsTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationsWsTest { + private NotificationsWsAction action = new FakeNotificationAction(); + private NotificationsWsAction[] actions = {action}; + private WsTester ws = new WsTester(new NotificationsWs(actions)); + + private Controller underTest = ws.controller("api/notifications"); + + @Test + public void definition() { + assertThat(underTest.path()).isEqualTo("api/notifications"); + } + + private static class FakeNotificationAction implements NotificationsWsAction { + @Override + public void define(WebService.NewController context) { + context.createAction("fake") + .setHandler(this); + } + + @Override + public void handle(Request request, Response response) throws Exception { + // do nothing + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java new file mode 100644 index 00000000000..fd371d3e77b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/ws/RemoveActionTest.java @@ -0,0 +1,234 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.notification.ws; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationUpdater; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.client.notification.AddRequest; + +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.db.component.ComponentTesting.newView; +import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION; +import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_CHANNEL; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_NOTIFICATION; +import static org.sonarqube.ws.client.notification.NotificationsWsParameters.PARAM_PROJECT; + +public class RemoveActionTest { + private static final String NOTIF_MY_NEW_ISSUES = "Dispatcher1"; + private static final String NOTIF_NEW_ISSUES = "Dispatcher2"; + private static final String NOTIF_NEW_QUALITY_GATE_STATUS = "Dispatcher3"; + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().login().setUserId(123); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + private DbSession dbSession = db.getSession(); + + private NotificationChannel emailChannel = new FakeNotificationChannel("EmailChannel"); + private NotificationChannel twitterChannel = new FakeNotificationChannel("TwitterChannel"); + // default channel, based on class simple name + private NotificationChannel defaultChannel = new FakeNotificationChannel("EmailNotificationChannel"); + + private NotificationCenter notificationCenter; + private NotificationUpdater notificationUpdater; + private RemoveAction underTest; + + private WsActionTester ws; + private AddRequest.Builder request = AddRequest.builder() + .setNotification(NOTIF_MY_NEW_ISSUES); + + @Before + public void setUp() { + NotificationDispatcherMetadata metadata1 = NotificationDispatcherMetadata.create(NOTIF_MY_NEW_ISSUES) + .setProperty(GLOBAL_NOTIFICATION, "true") + .setProperty(PER_PROJECT_NOTIFICATION, "true"); + NotificationDispatcherMetadata metadata2 = NotificationDispatcherMetadata.create(NOTIF_NEW_ISSUES) + .setProperty(GLOBAL_NOTIFICATION, "true"); + NotificationDispatcherMetadata metadata3 = NotificationDispatcherMetadata.create(NOTIF_NEW_QUALITY_GATE_STATUS) + .setProperty(GLOBAL_NOTIFICATION, "true") + .setProperty(PER_PROJECT_NOTIFICATION, "true"); + + notificationCenter = new NotificationCenter( + new NotificationDispatcherMetadata[] {metadata1, metadata2, metadata3}, + new NotificationChannel[] {emailChannel, twitterChannel, defaultChannel}); + notificationUpdater = new NotificationUpdater(userSession, dbClient); + underTest = new RemoveAction(notificationCenter, notificationUpdater, dbClient, new ComponentFinder(dbClient), userSession); + ws = new WsActionTester(underTest); + } + + @Test + public void remove_to_email_channel_by_default() { + notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null); + dbSession.commit(); + + call(request); + + db.notifications().assertDoesNotExist(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), null); + } + + @Test + public void remove_from_a_specific_channel() { + notificationUpdater.add(dbSession, twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, null); + dbSession.commit(); + + call(request.setNotification(NOTIF_NEW_QUALITY_GATE_STATUS).setChannel(twitterChannel.getKey())); + + db.notifications().assertDoesNotExist(twitterChannel.getKey(), NOTIF_NEW_QUALITY_GATE_STATUS, userSession.getUserId(), null); + } + + @Test + public void remove_a_project_notification() { + ComponentDto project = db.components().insertProject(); + notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, project); + dbSession.commit(); + + call(request.setProject(project.getKey())); + + db.notifications().assertDoesNotExist(defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, userSession.getUserId(), project); + } + + @Test + public void http_no_content() { + notificationUpdater.add(dbSession, defaultChannel.getKey(), NOTIF_MY_NEW_ISSUES, null); + dbSession.commit(); + + TestResponse result = call(request); + + assertThat(result.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + + @Test + public void fail_when_notification_does_not_exist() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Notification doesn't exist"); + + call(request); + } + + @Test + public void fail_when_unknown_channel() { + expectedException.expect(IllegalArgumentException.class); + + call(request.setChannel("Channel42")); + } + + @Test + public void fail_when_unknown_global_dispatcher() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher2, Dispatcher3]"); + + call(request.setNotification("Dispatcher42")); + } + + @Test + public void fail_when_unknown_project_dispatcher() { + ComponentDto project = db.components().insertProject(); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Value of parameter 'notification' (Dispatcher42) must be one of: [Dispatcher1, Dispatcher3]"); + + call(request.setNotification("Dispatcher42").setProject(project.key())); + } + + @Test + public void fail_when_no_dispatcher() { + expectedException.expect(IllegalArgumentException.class); + + ws.newRequest().execute(); + } + + @Test + public void fail_when_project_is_unknown() { + expectedException.expect(NotFoundException.class); + + call(request.setProject("Project-42")); + } + + @Test + public void fail_when_component_is_not_a_project() { + db.components().insertViewAndSnapshot(newView().setKey("VIEW_1")); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Component 'VIEW_1' must be a project"); + + call(request.setProject("VIEW_1")); + } + + @Test + public void fail_when_not_authenticated() { + userSession.anonymous(); + + expectedException.expect(UnauthorizedException.class); + + call(request); + } + + private TestResponse call(AddRequest.Builder wsRequestBuilder) { + AddRequest wsRequest = wsRequestBuilder.build(); + TestRequest request = ws.newRequest(); + request.setParam(PARAM_NOTIFICATION, wsRequest.getNotification()); + setNullable(wsRequest.getChannel(), channel -> request.setParam(PARAM_CHANNEL, channel)); + setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project)); + return request.execute(); + } + + private static class FakeNotificationChannel extends NotificationChannel { + private final String key; + + private FakeNotificationChannel(String key) { + this.key = key; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public void deliver(Notification notification, String username) { + // do nothing + } + } +} diff --git a/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java b/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java index 34a8bb5cfe1..45befce078f 100644 --- a/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java +++ b/sonar-db/src/main/java/org/sonar/db/property/PropertyQuery.java @@ -19,6 +19,8 @@ */ package org.sonar.db.property; +import javax.annotation.Nullable; + public class PropertyQuery { private final String key; @@ -57,7 +59,7 @@ public class PropertyQuery { return this; } - public Builder setComponentId(Long componentId) { + public Builder setComponentId(@Nullable Long componentId) { this.componentId = componentId; return this; } diff --git a/sonar-db/src/test/java/org/sonar/db/DbTester.java b/sonar-db/src/test/java/org/sonar/db/DbTester.java index 48fbd8494c3..970e8756171 100644 --- a/sonar-db/src/test/java/org/sonar/db/DbTester.java +++ b/sonar-db/src/test/java/org/sonar/db/DbTester.java @@ -67,6 +67,7 @@ import org.sonar.db.component.ComponentDbTester; import org.sonar.db.event.EventDbTester; import org.sonar.db.favorite.FavoriteDbTester; import org.sonar.db.issue.IssueDbTester; +import org.sonar.db.notification.NotificationDbTester; import org.sonar.db.organization.OrganizationDbTester; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.organization.OrganizationTesting; @@ -110,6 +111,7 @@ public class DbTester extends ExternalResource { private final QualityGateDbTester qualityGateDbTester; private final IssueDbTester issueDbTester; private final RuleDbTester ruleDbTester; + private final NotificationDbTester notificationDbTester; private final RootFlagAssertions rootFlagAssertions; private DbTester(System2 system2, @Nullable String schemaPath) { @@ -125,6 +127,7 @@ public class DbTester extends ExternalResource { this.qualityGateDbTester = new QualityGateDbTester(this); this.issueDbTester = new IssueDbTester(this); this.ruleDbTester = new RuleDbTester(this); + this.notificationDbTester = new NotificationDbTester(this); this.rootFlagAssertions = new RootFlagAssertions(this); } @@ -225,6 +228,10 @@ public class DbTester extends ExternalResource { return ruleDbTester; } + public NotificationDbTester notifications() { + return notificationDbTester; + } + @Override protected void after() { if (session != null) { diff --git a/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java b/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java new file mode 100644 index 00000000000..4ad7893ee73 --- /dev/null +++ b/sonar-db/src/test/java/org/sonar/db/notification/NotificationDbTester.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.db.notification; + +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; +import org.sonar.db.property.PropertyQuery; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationDbTester { + private static final String PROP_NOTIFICATION_PREFIX = "notification"; + + private final DbTester db; + private final DbClient dbClient; + private final DbSession dbSession; + + public NotificationDbTester(DbTester db) { + this.db = db; + this.dbClient = db.getDbClient(); + this.dbSession = db.getSession(); + } + + public void assertExists(String channel, String dispatcher, long userId, @Nullable ComponentDto component) { + List<PropertyDto> result = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder() + .setKey(String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel)) + .setComponentId(component == null ? null : component.getId()) + .setUserId((int) userId) + .build(), dbSession); + assertThat(result).hasSize(1); + assertThat(result.get(0).getValue()).isEqualTo("true"); + } + + public void assertDoesNotExist(String channel, String dispatcher, long userId, @Nullable ComponentDto component) { + List<PropertyDto> result = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder() + .setKey(String.join(".", PROP_NOTIFICATION_PREFIX, dispatcher, channel)) + .setComponentId(component == null ? null : component.getId()) + .setUserId((int) userId) + .build(), dbSession); + assertThat(result).isEmpty(); + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java new file mode 100644 index 00000000000..c0e97ea9fd7 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/AddRequest.java @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.sonarqube.ws.client.notification; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +public class AddRequest { + private final String notification; + private final String channel; + private final String project; + + private AddRequest(Builder builder) { + this.channel = builder.channel; + this.notification = builder.notification; + this.project = builder.project; + } + + public String getNotification() { + return notification; + } + + @CheckForNull + public String getChannel() { + return channel; + } + + @CheckForNull + public String getProject() { + return project; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String notification; + private String channel; + private String project; + + private Builder() { + // enforce factory method + } + + public Builder setNotification(String notification) { + this.notification = notification; + return this; + } + + public Builder setChannel(@Nullable String channel) { + this.channel = channel; + return this; + } + + public Builder setProject(@Nullable String project) { + this.project = project; + return this; + } + + public AddRequest build() { + requireNonNull(notification, "Notification is required"); + return new AddRequest(this); + } + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java new file mode 100644 index 00000000000..a065b831ac9 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/NotificationsWsParameters.java @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.sonarqube.ws.client.notification; + +public class NotificationsWsParameters { + public static final String CONTROLLER = "api/notifications"; + public static final String ACTION_ADD = "add"; + + public static final String PARAM_PROJECT = "project"; + public static final String PARAM_CHANNEL = "channel"; + public static final String PARAM_NOTIFICATION = "notification"; + + private NotificationsWsParameters() { + // prevent instantiation + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java new file mode 100644 index 00000000000..fdfcd790e5e --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/notification/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.sonarqube.ws.client.notification; + +import javax.annotation.ParametersAreNonnullByDefault; + |