From 33901a8a3f5a484bacbf9bdd7d1f9f994c4a0bfd Mon Sep 17 00:00:00 2001 From: lukasz-jarocki-sonarsource <77498856+lukasz-jarocki-sonarsource@users.noreply.github.com> Date: Wed, 2 Feb 2022 15:31:44 +0100 Subject: [PATCH] SONAR-15918 managing the connections of SonarLint push endpoint --- .../sonar/server/pushapi/HeartbeatTask.java | 38 ++++++ .../server/pushapi/ServerPushClient.java | 93 +++++++++++++ .../server/pushapi/ServerPushWsModule.java | 2 + .../pushapi/sonarlint/SonarLintClient.java | 60 +++++++++ .../sonarlint/SonarLintClientsRegistry.java | 83 ++++++++++++ .../sonarlint/SonarLintPushAction.java | 89 ++++++++++--- .../server/pushapi/HeartbeatTaskTest.java | 42 ++++++ .../server/pushapi/ServerPushClientTest.java | 124 ++++++++++++++++++ .../sonarlint/SonarLintClientTest.java | 80 +++++++++++ .../SonarLintClientsRegistryTest.java | 69 ++++++++++ .../sonarlint/SonarLintPushActionTest.java | 53 +++++++- 11 files changed, 711 insertions(+), 22 deletions(-) create mode 100644 server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/HeartbeatTask.java create mode 100644 server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java create mode 100644 server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java create mode 100644 server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java create mode 100644 server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/HeartbeatTaskTest.java create mode 100644 server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java create mode 100644 server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java create mode 100644 server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/HeartbeatTask.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/HeartbeatTask.java new file mode 100644 index 00000000000..5bfd8ada42e --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/HeartbeatTask.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi; + +class HeartbeatTask implements Runnable { + + private final ServerPushClient serverPushClient; + + public HeartbeatTask(ServerPushClient serverPushClient) { + this.serverPushClient = serverPushClient; + } + + @Override + public void run() { + synchronized (this) { + serverPushClient.writeAndFlush('\r'); + serverPushClient.writeAndFlush('\n'); + } + serverPushClient.scheduleHeartbeat(); + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java new file mode 100644 index 00000000000..5f43331f4af --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi; + +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.ServletOutputStream; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +public abstract class ServerPushClient { + + private static final Logger LOG = Loggers.get(ServerPushClient.class); + private static final int DEFAULT_HEARTBEAT_PERIOD = 60; + + protected final AsyncContext asyncContext; + + private final ScheduledExecutorService executorService; + private final HeartbeatTask heartbeatTask; + private ScheduledFuture startedHeartbeat; + + protected ServerPushClient(ScheduledExecutorService executorService, AsyncContext asyncContext) { + this.executorService = executorService; + this.asyncContext = asyncContext; + this.heartbeatTask = new HeartbeatTask(this); + } + + public void scheduleHeartbeat() { + startedHeartbeat = executorService.schedule(heartbeatTask, DEFAULT_HEARTBEAT_PERIOD, TimeUnit.SECONDS); + } + + public void writeAndFlush(char character) { + write(character); + flush(); + } + + public synchronized void write(char character) { + try { + output().write(character); + } catch (IOException e) { + handleIOException(e); + } + } + + public synchronized void flush() { + try { + output().flush(); + } catch (IOException e) { + handleIOException(e); + } + } + + private void handleIOException(IOException e) { + String remoteAddr = asyncContext.getRequest().getRemoteAddr(); + LOG.info(String.format("The server push client %s gone without notice, closing the connection (%s)", remoteAddr, e.getMessage())); + close(); + } + + private synchronized void close() { + startedHeartbeat.cancel(false); + asyncContext.complete(); + } + + private ServletOutputStream output() throws IOException { + return asyncContext.getResponse().getOutputStream(); + } + + public void addListener(AsyncListener asyncListener) { + asyncContext.addListener(asyncListener); + } + +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java index f82da6a534d..5aff7050e36 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java @@ -20,6 +20,7 @@ package org.sonar.server.pushapi; import org.sonar.core.platform.Module; +import org.sonar.server.pushapi.sonarlint.SonarLintClientsRegistry; import org.sonar.server.pushapi.sonarlint.SonarLintPushAction; public class ServerPushWsModule extends Module { @@ -29,6 +30,7 @@ public class ServerPushWsModule extends Module { add( ServerPushWs.class, + SonarLintClientsRegistry.class, SonarLintPushAction.class); } } diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java new file mode 100644 index 00000000000..62d13723272 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi.sonarlint; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import javax.servlet.AsyncContext; +import org.sonar.server.pushapi.ServerPushClient; + +public class SonarLintClient extends ServerPushClient { + + private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + + private final Set languages; + private final Set projectKeys; + + public SonarLintClient(AsyncContext asyncContext, Set projectKeys, Set languages) { + super(scheduledExecutorService, asyncContext); + this.projectKeys = projectKeys; + this.languages = languages; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SonarLintClient that = (SonarLintClient) o; + return languages.equals(that.languages) + && projectKeys.equals(that.projectKeys) + && asyncContext.equals(that.asyncContext); + } + + @Override + public int hashCode() { + return Objects.hash(languages, projectKeys); + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java new file mode 100644 index 00000000000..44d9fa4917d --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi.sonarlint; + +import com.google.common.annotations.VisibleForTesting; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +@ServerSide +public class SonarLintClientsRegistry { + + private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class); + + private final List clients = new CopyOnWriteArrayList<>(); + + public void registerClient(SonarLintClient sonarLintClient) { + clients.add(sonarLintClient); + sonarLintClient.scheduleHeartbeat(); + sonarLintClient.addListener(new SonarLintClientEventsListener(sonarLintClient)); + LOG.debug("Registering new SonarLint client"); + } + + public void unregisterClient(SonarLintClient client) { + clients.remove(client); + LOG.debug("Removing SonarLint client"); + } + + @VisibleForTesting + List getAllClients() { + return clients; + } + + class SonarLintClientEventsListener implements AsyncListener { + private final SonarLintClient client; + + public SonarLintClientEventsListener(SonarLintClient sonarLintClient) { + this.client = sonarLintClient; + } + + @Override + public void onComplete(AsyncEvent event) { + unregisterClient(client); + } + + @Override + public void onError(AsyncEvent event) { + unregisterClient(client); + } + + @Override + public void onStartAsync(AsyncEvent event) { + //nothing to do on start + } + + @Override + public void onTimeout(AsyncEvent event) { + unregisterClient(client); + } + } + +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java index eb72c1c4e81..72419be534f 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java @@ -20,24 +20,35 @@ package org.sonar.server.pushapi.sonarlint; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletResponse; 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.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.project.ProjectDto; import org.sonar.server.pushapi.ServerPushAction; +import org.sonar.server.user.UserSession; import org.sonar.server.ws.ServletRequest; import org.sonar.server.ws.ServletResponse; public class SonarLintPushAction extends ServerPushAction { - private static final Logger LOGGER = Loggers.get(SonarLintPushAction.class); - private static final String PROJECT_PARAM_KEY = "projectKeys"; private static final String LANGUAGE_PARAM_KEY = "languages"; + private final SonarLintClientsRegistry clientsRegistry; + private final UserSession userSession; + private final DbClient dbClient; + + public SonarLintPushAction(SonarLintClientsRegistry sonarLintClientRegistry, UserSession userSession, DbClient dbClient) { + this.clientsRegistry = sonarLintClientRegistry; + this.userSession = userSession; + this.dbClient = dbClient; + } @Override public void define(WebService.NewController controller) { @@ -63,18 +74,13 @@ public class SonarLintPushAction extends ServerPushAction { @Override public void handle(Request request, Response response) throws IOException { + userSession.checkLoggedIn(); + ServletRequest servletRequest = (ServletRequest) request; ServletResponse servletResponse = (ServletResponse) response; - String projectKeys = request.getParam(PROJECT_PARAM_KEY).getValue(); - String languages = request.getParam(LANGUAGE_PARAM_KEY).getValue(); - - // to remove later - LOGGER.debug(projectKeys != null ? projectKeys : ""); - LOGGER.debug(languages != null ? languages : ""); - - AsyncContext asyncContext = servletRequest.startAsync(); - asyncContext.setTimeout(0); + var params = new SonarLintPushActionParamsValidator(request); + params.validateProjectsPermissions(); if (!isServerSideEventsRequest(servletRequest)) { servletResponse.stream().setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); @@ -83,8 +89,57 @@ public class SonarLintPushAction extends ServerPushAction { setHeadersForResponse(servletResponse); - //test response to remove later - response.stream().output().write("Hello world".getBytes(StandardCharsets.UTF_8)); - response.stream().output().flush(); + AsyncContext asyncContext = servletRequest.startAsync(); + asyncContext.setTimeout(0); + + var sonarLintClient = new SonarLintClient(asyncContext, params.getProjectKeys(), params.getLanguages()); + + clientsRegistry.registerClient(sonarLintClient); } + + class SonarLintPushActionParamsValidator { + + private final Request request; + private final Set projectKeys; + private final Set languages; + + private List projectDtos; + + SonarLintPushActionParamsValidator(Request request) { + this.request = request; + this.projectKeys = parseParam(PROJECT_PARAM_KEY); + this.languages = parseParam(LANGUAGE_PARAM_KEY); + } + + Set getProjectKeys() { + return projectKeys; + } + + Set getLanguages() { + return languages; + } + + private Set parseParam(String paramKey) { + String paramProjectKeys = request.getParam(paramKey).getValue(); + if (paramProjectKeys == null) { + throw new IllegalArgumentException("Param " + paramKey + " was not provided."); + } + return Set.of(paramProjectKeys.trim().split(",")); + } + + public void validateProjectsPermissions() { + if (projectDtos == null) { + try (DbSession dbSession = dbClient.openSession(false)) { + projectDtos = dbClient.projectDao().selectProjectsByKeys(dbSession, projectKeys); + } + } + if (projectDtos.size() < projectKeys.size() || projectDtos.isEmpty()) { + throw new IllegalArgumentException("Param " + PROJECT_PARAM_KEY + " is invalid."); + } + for (ProjectDto projectDto : projectDtos) { + userSession.checkProjectPermission(UserRole.USER, projectDto); + } + } + } + } diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/HeartbeatTaskTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/HeartbeatTaskTest.java new file mode 100644 index 00000000000..05154b3d66c --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/HeartbeatTaskTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.anyChar; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class HeartbeatTaskTest { + + private final ServerPushClient serverPushClient = mock(ServerPushClient.class); + + private final HeartbeatTask underTest = new HeartbeatTask(serverPushClient); + + @Test + public void run_reSchedulesHeartBeat() { + underTest.run(); + + verify(serverPushClient, times(2)).writeAndFlush(anyChar()); + verify(serverPushClient).scheduleHeartbeat(); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java new file mode 100644 index 00000000000..ec1c4408bdc --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi; + +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ServerPushClientTest { + + private final ScheduledExecutorService executorService = mock(ScheduledExecutorService.class); + private final AsyncContext asyncContext = mock(AsyncContext.class); + + private final ServerPushClient underTest = new ServerPushClient(executorService, asyncContext) {}; + + private final ServletOutputStream outputStream = mock(ServletOutputStream.class); + private ServletResponse servletResponse; + + @Before + public void before() throws IOException { + servletResponse = mock(ServletResponse.class); + + when(servletResponse.getOutputStream()).thenReturn(outputStream); + when(asyncContext.getResponse()).thenReturn(servletResponse); + when(asyncContext.getRequest()).thenReturn(mock(ServletRequest.class)); + } + + @Test + public void scheduleHeartbeat_oneTaskIsScheduled() { + underTest.scheduleHeartbeat(); + + verify(executorService, Mockito.times(1)) + .schedule(any(HeartbeatTask.class), anyLong(), any()); + } + + @Test + public void writeAndFlush_writeIsCalledOnceAndFlushIsCalledOnce() throws IOException { + underTest.writeAndFlush('a'); + + verify(outputStream, Mockito.times(1)).flush(); + verify(outputStream, Mockito.times(1)).write('a'); + } + + @Test + public void write_writeIsCalledOnceAndDoesntFlush() throws IOException { + underTest.write('a'); + + verify(outputStream, Mockito.never()).flush(); + verify(outputStream, Mockito.times(1)).write('a'); + } + + @Test + public void flush_streamIsFlushed() throws IOException { + underTest.flush(); + + verify(outputStream, Mockito.only()).flush(); + } + + @Test + public void addListener_addsListener() { + AsyncListener mock = mock(AsyncListener.class); + + underTest.addListener(mock); + + verify(asyncContext).addListener(mock); + } + + @Test + public void write_exceptionCausesConnectionToClose() throws IOException { + when(servletResponse.getOutputStream()).thenThrow(new IOException("mock exception")); + ScheduledFuture task = mock(ScheduledFuture.class); + when(executorService.schedule(any(HeartbeatTask.class), anyLong(), any(TimeUnit.class))).thenReturn(task); + underTest.scheduleHeartbeat(); + + underTest.write('a'); + + verify(asyncContext).complete(); + } + + @Test + public void flush_exceptionCausesConnectionToClose() throws IOException { + when(servletResponse.getOutputStream()).thenThrow(new IOException("mock exception")); + ScheduledFuture task = mock(ScheduledFuture.class); + when(executorService.schedule(any(HeartbeatTask.class), anyLong(), any(TimeUnit.class))).thenReturn(task); + underTest.scheduleHeartbeat(); + + underTest.flush(); + + verify(asyncContext).complete(); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java new file mode 100644 index 00000000000..5fffbd1c218 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi.sonarlint; + +import java.util.Set; +import javax.servlet.AsyncContext; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class SonarLintClientTest { + + private final AsyncContext firstContext = mock(AsyncContext.class); + private final AsyncContext secondContext = mock(AsyncContext.class); + + @Test + public void equals_twoClientsWithSameArgumentsAreEqual() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of()); + SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of()); + + assertThat(first).isEqualTo(second); + } + + @Test + public void equals_twoClientsWithDifferentAsyncObjects() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of()); + SonarLintClient second = new SonarLintClient(secondContext, Set.of(), Set.of()); + + assertThat(first).isNotEqualTo(second); + } + + @Test + public void equals_twoClientsWithDifferentLanguages() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of("java")); + SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of("cobol")); + + assertThat(first).isNotEqualTo(second); + } + + @Test + public void equals_twoClientsWithDifferentProjectKeys() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of()); + SonarLintClient second = new SonarLintClient(firstContext, Set.of("project1"), Set.of()); + + assertThat(first).isNotEqualTo(second); + } + + @Test + public void equals_secondClientIsNull() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of()); + + assertThat(first).isNotEqualTo(null); + } + + @Test + public void hashCode_producesSameHashesForEqualObjects() { + SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of()); + SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of()); + + assertThat(first).hasSameHashCodeAs(second); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java new file mode 100644 index 00000000000..f6f0ccd68f7 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.server.pushapi.sonarlint; + +import java.util.Set; +import javax.servlet.AsyncContext; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class SonarLintClientsRegistryTest { + + private final AsyncContext defaultAsyncContext = mock(AsyncContext.class); + + private final Set exampleKeys = Set.of("project1", "project2", "project3"); + + private final Set languageKeys = Set.of("language1", "language2", "language3"); + + private SonarLintClientsRegistry underTest; + + @Before + public void before() { + underTest = new SonarLintClientsRegistry(); + } + + @Test + public void registerClientAndUnregister_changesNumberOfClients() { + SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, languageKeys); + + underTest.registerClient(sonarLintClient); + + assertThat(underTest.getAllClients()).hasSize(1); + + underTest.unregisterClient(sonarLintClient); + + assertThat(underTest.getAllClients()).isEmpty(); + } + + @Test + public void registering10Clients_10ClientsAreRegistered() { + for (int i = 0; i < 10; i++) { + AsyncContext newAsyncContext = mock(AsyncContext.class); + SonarLintClient sonarLintClient = new SonarLintClient(newAsyncContext, exampleKeys, languageKeys); + underTest.registerClient(sonarLintClient); + } + + assertThat(underTest.getAllClients()).hasSize(10); + } + +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java index f6e9f5c7187..624b4173fd9 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java @@ -19,19 +19,50 @@ */ package org.sonar.server.pushapi.sonarlint; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Before; import org.junit.Test; import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.project.ProjectDao; +import org.sonar.db.project.ProjectDto; import org.sonar.server.pushapi.TestPushRequest; import org.sonar.server.pushapi.WsPushActionTester; +import org.sonar.server.user.UserSession; import org.sonar.server.ws.TestResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class SonarLintPushActionTest { - private final WsPushActionTester ws = new WsPushActionTester(new SonarLintPushAction()); + private final SonarLintClientsRegistry registry = mock(SonarLintClientsRegistry.class); + private final UserSession userSession = mock(UserSession.class); + private final DbClient dbClient = mock(DbClient.class); + private final ProjectDao projectDao = mock(ProjectDao.class); + + private final WsPushActionTester ws = new WsPushActionTester(new SonarLintPushAction(registry, userSession, dbClient)); + + @Before + public void before() { + List projectDtos = generateProjectDtos(2); + when(projectDao.selectProjectsByKeys(any(), any())).thenReturn(projectDtos); + when(dbClient.projectDao()).thenReturn(projectDao); + } + + public List generateProjectDtos(int howMany) { + return IntStream.rangeClosed(1, howMany).mapToObj(i -> { + ProjectDto dto = new ProjectDto(); + dto.setKee("project" + i); + return dto; + }).collect(Collectors.toList()); + } @Test public void defineTest() { @@ -52,13 +83,12 @@ public class SonarLintPushActionTest { .setHeader("accept", "text/event-stream") .execute(); - assertThat(response.getInput()).isEqualTo("Hello world"); + assertThat(response.getInput()).isEmpty(); } @Test public void handle_whenAcceptHeaderNotProvided_statusCode406() { - TestResponse testResponse = ws.newPushRequest(). - setParam("projectKeys", "project1,project2") + TestResponse testResponse = ws.newPushRequest().setParam("projectKeys", "project1,project2") .setParam("languages", "java") .execute(); @@ -67,11 +97,24 @@ public class SonarLintPushActionTest { @Test public void handle_whenParamsNotProvided_throwException() { - TestPushRequest testRequest = ws.newPushRequest() + TestPushRequest testRequest = ws.newPushRequest() .setHeader("accept", "text/event-stream"); assertThatThrownBy(testRequest::execute) .isInstanceOf(IllegalArgumentException.class) .hasMessage("The 'projectKeys' parameter is missing"); } + + @Test + public void handle_whenParamProjectKeyNotValid_throwException() { + TestPushRequest testRequest = ws.newPushRequest() + .setParam("projectKeys", "not-valid-key") + .setParam("languages", "java") + .setHeader("accept", "text/event-stream"); + when(projectDao.selectProjectsByKeys(any(), any())).thenReturn(List.of()); + + assertThatThrownBy(testRequest::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Param projectKeys is invalid."); + } } -- 2.39.5