--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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 {
add(
ServerPushWs.class,
+ SonarLintClientsRegistry.class,
SonarLintPushAction.class);
}
}
--- /dev/null
+/*
+ * 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<String> languages;
+ private final Set<String> projectKeys;
+
+ public SonarLintClient(AsyncContext asyncContext, Set<String> projectKeys, Set<String> 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);
+ }
+}
--- /dev/null
+/*
+ * 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<SonarLintClient> 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<SonarLintClient> 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);
+ }
+ }
+
+}
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) {
@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);
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<String> projectKeys;
+ private final Set<String> languages;
+
+ private List<ProjectDto> projectDtos;
+
+ SonarLintPushActionParamsValidator(Request request) {
+ this.request = request;
+ this.projectKeys = parseParam(PROJECT_PARAM_KEY);
+ this.languages = parseParam(LANGUAGE_PARAM_KEY);
+ }
+
+ Set<String> getProjectKeys() {
+ return projectKeys;
+ }
+
+ Set<String> getLanguages() {
+ return languages;
+ }
+
+ private Set<String> 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);
+ }
+ }
+ }
+
}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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<String> exampleKeys = Set.of("project1", "project2", "project3");
+
+ private final Set<String> 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);
+ }
+
+}
*/
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<ProjectDto> projectDtos = generateProjectDtos(2);
+ when(projectDao.selectProjectsByKeys(any(), any())).thenReturn(projectDtos);
+ when(dbClient.projectDao()).thenReturn(projectDao);
+ }
+
+ public List<ProjectDto> 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() {
.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();
@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.");
+ }
}