]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19963 Add tests for API v2 users controller and refactor existing tests
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>
Mon, 24 Jul 2023 15:15:22 +0000 (17:15 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 26 Jul 2023 20:03:24 +0000 (20:03 +0000)
22 files changed:
server/sonar-webserver-webapi-v2/build.gradle
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java [deleted file]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/common/ControllerIT.java [deleted file]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java [deleted file]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java [deleted file]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java [deleted file]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/model/RestPage.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DefaultLivenessController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/HealthController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/LivenessController.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/SafeModeWebConfig.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java [deleted file]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/HealthController.java [deleted file]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java [deleted file]
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/package-info.java [deleted file]
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/ControllerTester.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DefaultLivenessControllerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/HealthControllerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java [new file with mode: 0644]

index 42a332351139298b1e3f3759df678d217571913b..04d0150bf86c88006ae5b7d0425e70fc3ec7b7ae 100644 (file)
@@ -20,9 +20,9 @@ dependencies {
     testImplementation 'org.mockito:mockito-core'
     testImplementation 'org.springframework:spring-test'
     testImplementation 'org.skyscreamer:jsonassert:1.5.1'
+    testImplementation project(':sonar-testing-harness')
 
     testImplementation testFixtures(project(':server:sonar-server-common'))
-
-    testImplementation project(':sonar-testing-harness')
+    testImplementation testFixtures(project(':server:sonar-webserver-auth'))
 }
 
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java
deleted file mode 100644 (file)
index 5298c35..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.api.user.controller;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.db.DbClient;
-import org.sonar.db.user.UserDao;
-import org.sonar.server.common.user.service.UserService;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.user.AbstractUserSession;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.v2.common.ControllerIT;
-
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-public class DefaultUserControllerIT extends ControllerIT {
-
-  @After
-  public void resetUsedMocks() {
-    Mockito.reset(webAppContext.getBean(UserService.class));
-    Mockito.reset(webAppContext.getBean(UserSession.class));
-  }
-
-  @Before
-  public void setUp() {
-    UserSession userSession = webAppContext.getBean(UserSession.class);
-    when(userSession.checkLoggedIn()).thenReturn(userSession);
-  }
-
-  @Test
-  public void deactivate_whenUserIsNotLoggedIn_shouldReturnForbidden() throws Exception {
-    when(webAppContext.getBean(UserSession.class).checkLoggedIn()).thenThrow(new UnauthorizedException("unauthorized"));
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpectAll(
-        status().isUnauthorized(),
-        content().string("{\"message\":\"unauthorized\"}"));
-  }
-
-  @Test
-  public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
-    when(webAppContext.getBean(UserSession.class).checkIsSystemAdministrator()).thenThrow(AbstractUserSession.insufficientPrivilegesException());
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpectAll(
-        status().isForbidden(),
-        content().json("{\"message\":\"Insufficient privileges\"}"));
-  }
-
-  @Test
-  public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
-    doThrow(new NotFoundException("User not found.")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpectAll(
-        status().isNotFound(),
-        content().json("{\"message\":\"User not found.\"}"));
-  }
-
-  @Test
-  public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
-    doThrow(BadRequestException.create("Not allowed")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpectAll(
-        status().isBadRequest(),
-        content().json("{\"message\":\"Not allowed\"}"));
-  }
-
-  @Test
-  public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
-    when(webAppContext.getBean(DbClient.class).userDao()).thenReturn(mock(UserDao.class));
-    when(webAppContext.getBean(UserSession.class).getLogin()).thenReturn("userToDelete");
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpectAll(
-        status().isBadRequest(),
-        content().json("{\"message\":\"Self-deactivation is not possible\"}"));
-  }
-
-  @Test
-  public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
-      .andExpect(
-        status().isBadRequest());
-  }
-
-  @Test
-  public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
-      .andExpect(status().isNoContent());
-
-    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
-  }
-
-  @Test
-  public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=false"))
-      .andExpect(status().isNoContent());
-
-    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
-  }
-
-  @Test
-  public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
-    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=true"))
-      .andExpect(status().isNoContent());
-
-    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", true);
-  }
-}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/common/ControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/common/ControllerIT.java
deleted file mode 100644 (file)
index 5985d98..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.common;
-
-import org.junit.Before;
-import org.junit.runner.RunWith;
-import org.sonar.server.v2.config.MockConfigForControllers;
-import org.sonar.server.v2.config.PlatformLevel4WebConfig;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.context.web.WebAppConfiguration;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.context.WebApplicationContext;
-
-@WebAppConfiguration
-@ContextConfiguration(
-  classes = {PlatformLevel4WebConfig.class, MockConfigForControllers.class}
-)
-@RunWith(SpringRunner.class)
-public abstract class ControllerIT {
-  @Autowired
-  protected WebApplicationContext webAppContext;
-
-  protected MockMvc mockMvc;
-
-  @Before
-  public void setup() {
-    mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
-  }
-}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java
deleted file mode 100644 (file)
index cee83db..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.config;
-
-import org.sonar.db.DbClient;
-import org.sonar.server.common.user.service.UserService;
-import org.sonar.server.health.CeStatusNodeCheck;
-import org.sonar.server.health.DbConnectionNodeCheck;
-import org.sonar.server.health.EsStatusNodeCheck;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.health.WebServerStatusNodeCheck;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.platform.ws.LivenessChecker;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.user.UserUpdater;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-import static org.mockito.Mockito.mock;
-
-@Configuration
-public class MockConfigForControllers {
-
-  @Bean
-  public DbClient dbClient() {
-    return mock(DbClient.class);
-  }
-
-  @Bean
-  public DbConnectionNodeCheck dbConnectionNodeCheck() {
-    return mock(DbConnectionNodeCheck.class);
-  }
-
-  @Bean
-  public WebServerStatusNodeCheck webServerStatusNodeCheck() {
-    return mock(WebServerStatusNodeCheck.class);
-  }
-
-  @Bean
-  public CeStatusNodeCheck ceStatusNodeCheck() {
-    return mock(CeStatusNodeCheck.class);
-  }
-
-  @Bean
-  public EsStatusNodeCheck esStatusNodeCheck() {
-    return mock(EsStatusNodeCheck.class);
-  }
-
-  @Bean
-  public LivenessChecker livenessChecker() {
-    return mock(LivenessChecker.class);
-  }
-
-  @Bean
-  public HealthChecker healthChecker() {
-    return mock(HealthChecker.class);
-  }
-
-  @Bean
-  public SystemPasscode systemPasscode() {
-    return mock(SystemPasscode.class);
-  }
-
-  @Bean
-  public NodeInformation nodeInformation() {
-    return mock(NodeInformation.class);
-  }
-
-  @Bean
-  public UserSession userSession() {
-    return mock(UserSession.class);
-  }
-
-  @Bean
-  UserUpdater userUpdater() {
-    return mock(UserUpdater.class);
-  }
-
-  @Bean
-  UserService userService() {
-    return mock(UserService.class);
-  }
-
-}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/DefaultLivenessControllerIT.java
deleted file mode 100644 (file)
index 3e77344..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.controller;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.server.platform.ws.LivenessChecker;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.v2.common.ControllerIT;
-
-import static org.mockito.Mockito.when;
-import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
-import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-public class DefaultLivenessControllerIT extends ControllerIT {
-
-  private static final String VALID_PASSCODE = "valid_passcode";
-  private static final String INVALID_PASSCODE = "invalid_passcode";
-
-  @Before
-  public void setup() {
-    super.setup();
-    when(webAppContext.getBean(LivenessChecker.class).liveness()).thenReturn(true);
-  }
-
-  @After
-  public void resetUsedMocks() {
-    Mockito.reset(webAppContext.getBean(SystemPasscode.class));
-    Mockito.reset(webAppContext.getBean(UserSession.class));
-  }
-
-  @Test
-  public void getSystemLiveness_whenValidPasscode_shouldSucceed() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
-
-    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
-      .andExpect(status().isNoContent());
-  }
-
-  @Test
-  public void getSystemLiveness_whenAdminCredential_shouldSucceed() throws Exception {
-    when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true);
-
-    mockMvc.perform(get(LIVENESS_ENDPOINT))
-      .andExpect(status().isNoContent());
-  }
-
-  @Test
-  public void getSystemLiveness_whenNoUserSessionAndNoPasscode_shouldReturnForbidden() throws Exception {
-    mockMvc.perform(get(LIVENESS_ENDPOINT))
-      .andExpectAll(
-        status().isForbidden(),
-        content().json("{\"message\":\"Insufficient privileges\"}"));
-  }
-
-  @Test
-  public void getSystemLiveness_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
-    when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(false);
-
-    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
-      .andExpectAll(
-        status().isForbidden(),
-        content().json("{\"message\":\"Insufficient privileges\"}"));
-  }
-
-  @Test
-  public void getSystemLiveness_whenLivenessCheckFails_shouldReturnServerError() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
-    when(webAppContext.getBean(LivenessChecker.class).liveness()).thenReturn(false);
-
-    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
-      .andExpectAll(
-        status().isInternalServerError(),
-        content().json("{\"message\":\"Liveness check failed\"}"));
-  }
-}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/controller/HealthControllerIT.java
deleted file mode 100644 (file)
index 9fb213f..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.controller;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.health.Health;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.sonar.server.v2.common.ControllerIT;
-
-import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
-import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-
-public class HealthControllerIT extends ControllerIT {
-
-  private static final String VALID_PASSCODE = "valid_passcode";
-  private static final String INVALID_PASSCODE = "invalid_passcode";
-  private static final Health HEALTH_RESULT = Health.builder().
-    setStatus(Health.Status.YELLOW)
-    .addCause("One cause")
-    .build();
-
-  @Before
-  public void setup() {
-    super.setup();
-    when(webAppContext.getBean(HealthChecker.class).checkNode()).thenReturn(HEALTH_RESULT);
-  }
-
-  @After
-  public void resetUsedMocks() {
-    Mockito.reset(webAppContext.getBean(SystemPasscode.class));
-    Mockito.reset(webAppContext.getBean(NodeInformation.class));
-    Mockito.reset(webAppContext.getBean(UserSession.class));
-  }
-
-  @Test
-  public void getSystemHealth_whenValidPasscodeAndStandaloneMode_shouldSucceed() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
-    when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(true);
-
-    mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
-      .andExpectAll(
-        status().isOk(),
-        content().json("""
-          {
-             "status":"YELLOW",
-             "causes":[
-                "One cause"
-             ]
-          }"""));
-  }
-
-  @Test
-  public void getSystemHealth_whenAdminCredentialAndStandaloneMode_shouldSucceed() throws Exception {
-    when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true);
-    when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(true);
-
-    mockMvc.perform(get(HEALTH_ENDPOINT))
-      .andExpectAll(
-        status().isOk(),
-        content().json("""
-          {
-             "status":"YELLOW",
-             "causes":[
-                "One cause"
-             ]
-          }"""));
-  }
-
-  @Test
-  public void getSystemHealth_whenNoCredentials_shouldReturnForbidden() throws Exception {
-    mockMvc.perform(get(HEALTH_ENDPOINT))
-      .andExpectAll(
-        status().isForbidden(),
-        content().json("{\"message\":\"Insufficient privileges\"}"));
-  }
-
-  @Test
-  public void getSystemHealth_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
-    when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(false);
-
-    mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
-      .andExpectAll(
-        status().isForbidden(),
-        content().json("{\"message\":\"Insufficient privileges\"}"));
-  }
-
-  @Test
-  public void getSystemHealth_whenUnauthorizedExceptionThrown_shouldReturnUnauthorized() throws Exception {
-    when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenThrow(new UnauthorizedException("UnauthorizedException"));
-
-    mockMvc.perform(get(HEALTH_ENDPOINT))
-      .andExpectAll(
-        status().isUnauthorized(),
-        content().json("{\"message\":\"UnauthorizedException\"}"));
-  }
-
-  @Test
-  public void getSystemHealth_whenValidPasscodeAndClusterMode_shouldReturnNotImplemented() throws Exception {
-    when(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true);
-    when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(false);
-
-    mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
-      .andExpectAll(
-        status().is(HTTP_NOT_IMPLEMENTED),
-        content().json("{\"message\":\"Unsupported in cluster mode\"}"));
-  }
-}
index 4870e06292ff1574eb76c71a14b49fd4beb68bd0..87ef45e672c5f78af641ea54dbf6ef85600fbe6e 100644 (file)
@@ -21,9 +21,7 @@ package org.sonar.server.v2;
 
 public class WebApiEndpoints {
   private static final String SYSTEM_ENDPOINTS = "/system";
-
   public static final String LIVENESS_ENDPOINT = SYSTEM_ENDPOINTS + "/liveness";
-
   public static final String HEALTH_ENDPOINT = SYSTEM_ENDPOINTS + "/health";
   public static final String USER_ENDPOINT = "/users";
 
index ec374ec5f36e4b33b6729115742c71487f1ccff9..ecd31afbc55194e878600a44b31b29d9818c0cb6 100644 (file)
@@ -42,9 +42,9 @@ public record RestPage(
 ) {
 
   @VisibleForTesting
-  static final String DEFAULT_PAGE_SIZE = "50";
+  public static final String DEFAULT_PAGE_SIZE = "50";
   @VisibleForTesting
-  static final String DEFAULT_PAGE_INDEX = "1";
+  public static final String DEFAULT_PAGE_INDEX = "1";
 
   public RestPage(@Nullable Integer pageSize, @Nullable Integer pageIndex) {
     this.pageSize = pageSize == null ? Integer.valueOf(DEFAULT_PAGE_SIZE) : pageSize;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DefaultLivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DefaultLivenessController.java
new file mode 100644 (file)
index 0000000..da32653
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.system.controller;
+
+import javax.annotation.Nullable;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+
+public class DefaultLivenessController implements LivenessController {
+
+  private final LivenessChecker livenessChecker;
+  private final UserSession userSession;
+  private final SystemPasscode systemPasscode;
+
+  public DefaultLivenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode, @Nullable UserSession userSession) {
+    this.livenessChecker = livenessChecker;
+    this.userSession = userSession;
+    this.systemPasscode = systemPasscode;
+  }
+
+  @Override
+  public void livenessCheck(String requestPassCode) {
+    if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
+      if (livenessChecker.liveness()) {
+        return;
+      }
+      throw new IllegalStateException("Liveness check failed");
+    }
+    throw new ForbiddenException("Insufficient privileges");
+  }
+
+  private boolean isSystemAdmin() {
+    if (userSession == null) {
+      return false;
+    }
+    return userSession.isSystemAdministrator();
+  }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/HealthController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/HealthController.java
new file mode 100644 (file)
index 0000000..09d4521
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.system.controller;
+
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.health.Health;
+import org.sonar.server.health.HealthChecker;
+import org.sonar.server.platform.NodeInformation;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.user.UserSession;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
+
+/*
+This controller does not support the cluster mode.
+This is not the final implementation, as we have to first define what are endpoint contracts.
+*/
+@RestController
+@RequestMapping(HEALTH_ENDPOINT)
+public class HealthController {
+
+  private final HealthChecker healthChecker;
+  private final SystemPasscode systemPasscode;
+  private final NodeInformation nodeInformation;
+  private final UserSession userSession;
+
+  public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode, NodeInformation nodeInformation,
+    UserSession userSession) {
+    this.healthChecker = healthChecker;
+    this.systemPasscode = systemPasscode;
+    this.nodeInformation = nodeInformation;
+    this.userSession = userSession;
+  }
+
+  public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode) {
+    this(healthChecker, systemPasscode, null, null);
+  }
+
+  @GetMapping
+  public Health getHealth(@RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode) {
+    if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
+      return getHealth();
+    }
+    throw new ForbiddenException("Insufficient privileges");
+  }
+
+  private Health getHealth() {
+    if (nodeInformation == null || nodeInformation.isStandalone()) {
+      return healthChecker.checkNode();
+    }
+    throw new ServerException(HTTP_NOT_IMPLEMENTED, "Unsupported in cluster mode");
+  }
+
+  private boolean isSystemAdmin() {
+    if (userSession == null) {
+      return false;
+    }
+    return userSession.isSystemAdministrator();
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/LivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/LivenessController.java
new file mode 100644 (file)
index 0000000..adce154
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.system.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
+
+@RequestMapping(LIVENESS_ENDPOINT)
+@RestController
+public interface LivenessController {
+
+  @GetMapping
+  @ResponseStatus(HttpStatus.NO_CONTENT)
+  @Operation(summary = "Provide liveness of SonarQube, meant to be used as a liveness probe on Kubernetes", description = """
+      Require 'Administer System' permission or authentication with passcode.
+
+      When SonarQube is fully started, liveness check for database connectivity, Compute Engine status, and, except for DataCenter Edition, if ElasticSearch is Green or Yellow.
+
+      When SonarQube is on Safe Mode (for example when a database migration is running), liveness check only for database connectivity
+    """)
+  @ApiResponses(value = {
+    @ApiResponse(responseCode = "204", description = "This SonarQube node is alive"),
+    @ApiResponse(description = "This SonarQube node is not alive and should be rescheduled"),
+  })
+  void livenessCheck(
+    @Parameter(description = "Passcode can be provided, see SonarQube documentation") @RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode);
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/package-info.java
new file mode 100644 (file)
index 0000000..b4ec4b3
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.v2.api.system.controller;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index fc29d481951e3c4f216592a874d971767660d1ba..74b5c8a6fdf4c07c777ab2b558d781ef628a0419 100644 (file)
@@ -31,12 +31,12 @@ import org.sonar.server.platform.ws.LivenessChecker;
 import org.sonar.server.platform.ws.LivenessCheckerImpl;
 import org.sonar.server.user.SystemPasscode;
 import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
+import org.sonar.server.v2.api.system.controller.HealthController;
+import org.sonar.server.v2.api.system.controller.LivenessController;
 import org.sonar.server.v2.api.user.controller.DefaultUserController;
 import org.sonar.server.v2.api.user.controller.UserController;
 import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
-import org.sonar.server.v2.controller.DefautLivenessController;
-import org.sonar.server.v2.controller.HealthController;
-import org.sonar.server.v2.controller.LivenessController;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
@@ -53,7 +53,7 @@ public class PlatformLevel4WebConfig {
 
   @Bean
   public LivenessController livenessController(LivenessChecker livenessChecker, UserSession userSession, SystemPasscode systemPasscode) {
-    return new DefautLivenessController(livenessChecker, systemPasscode, userSession);
+    return new DefaultLivenessController(livenessChecker, systemPasscode, userSession);
   }
 
   @Bean
index 897cc07abb9043ab23d65373c42fa4b7554417ea..5c2671d016b9442a3a94510355af3200f899ab2f 100644 (file)
@@ -24,9 +24,9 @@ import org.sonar.server.health.HealthChecker;
 import org.sonar.server.platform.ws.LivenessChecker;
 import org.sonar.server.platform.ws.SafeModeLivenessCheckerImpl;
 import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.v2.controller.DefautLivenessController;
-import org.sonar.server.v2.controller.HealthController;
-import org.sonar.server.v2.controller.LivenessController;
+import org.sonar.server.v2.api.system.controller.DefaultLivenessController;
+import org.sonar.server.v2.api.system.controller.HealthController;
+import org.sonar.server.v2.api.system.controller.LivenessController;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
@@ -44,7 +44,7 @@ public class SafeModeWebConfig {
 
   @Bean
   public LivenessController livenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode) {
-    return new DefautLivenessController(livenessChecker, systemPasscode, null);
+    return new DefaultLivenessController(livenessChecker, systemPasscode, null);
   }
 
   @Bean
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/DefautLivenessController.java
deleted file mode 100644 (file)
index d609c9e..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.controller;
-
-import javax.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.server.exceptions.ForbiddenException;
-import org.sonar.server.platform.ws.LivenessChecker;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-
-public class DefautLivenessController implements LivenessController {
-
-  private static final Logger LOGGER = LoggerFactory.getLogger(DefautLivenessController.class);
-  private final LivenessChecker livenessChecker;
-  private final UserSession userSession;
-  private final SystemPasscode systemPasscode;
-
-  public DefautLivenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode, @Nullable UserSession userSession) {
-    this.livenessChecker = livenessChecker;
-    this.userSession = userSession;
-    this.systemPasscode = systemPasscode;
-  }
-
-  @Override
-  public void livenessCheck(String requestPassCode) {
-    if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
-      if (livenessChecker.liveness()) {
-        return;
-      }
-      throw new IllegalStateException("Liveness check failed");
-    }
-    throw new ForbiddenException("Insufficient privileges");
-  }
-
-  private boolean isSystemAdmin() {
-    if (userSession == null) {
-      return false;
-    }
-    return userSession.isSystemAdministrator();
-  }
-}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/HealthController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/HealthController.java
deleted file mode 100644 (file)
index 302e79a..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.controller;
-
-import org.sonar.server.exceptions.ForbiddenException;
-import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.health.Health;
-import org.sonar.server.health.HealthChecker;
-import org.sonar.server.platform.NodeInformation;
-import org.sonar.server.user.SystemPasscode;
-import org.sonar.server.user.UserSession;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
-import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
-
-/*
-This controller does not support the cluster mode.
-This is not the final implementation, as we have to first define what are endpoint contracts.
-*/
-@RestController
-@RequestMapping(HEALTH_ENDPOINT)
-public class HealthController {
-
-  private final HealthChecker healthChecker;
-  private final SystemPasscode systemPasscode;
-  private final NodeInformation nodeInformation;
-  private final UserSession userSession;
-
-  public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode, NodeInformation nodeInformation,
-    UserSession userSession) {
-    this.healthChecker = healthChecker;
-    this.systemPasscode = systemPasscode;
-    this.nodeInformation = nodeInformation;
-    this.userSession = userSession;
-  }
-
-  public HealthController(HealthChecker healthChecker, SystemPasscode systemPasscode) {
-    this(healthChecker, systemPasscode, null, null);
-  }
-
-  @GetMapping
-  public Health getHealth(@RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode) {
-    if (systemPasscode.isValidPasscode(requestPassCode) || isSystemAdmin()) {
-      return getHealth();
-    }
-    throw new ForbiddenException("Insufficient privileges");
-  }
-
-  private Health getHealth() {
-    if (nodeInformation == null || nodeInformation.isStandalone()) {
-      return healthChecker.checkNode();
-    }
-    throw new ServerException(HTTP_NOT_IMPLEMENTED, "Unsupported in cluster mode");
-  }
-
-  private boolean isSystemAdmin() {
-    if (userSession == null) {
-      return false;
-    }
-    return userSession.isSystemAdministrator();
-  }
-}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/LivenessController.java
deleted file mode 100644 (file)
index 9433a6e..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.controller;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseStatus;
-import org.springframework.web.bind.annotation.RestController;
-
-import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
-
-@RequestMapping(LIVENESS_ENDPOINT)
-@RestController
-public interface LivenessController {
-
-  @GetMapping
-  @ResponseStatus(HttpStatus.NO_CONTENT)
-  @Operation(summary = "Provide liveness of SonarQube, meant to be used as a liveness probe on Kubernetes", description = """
-      Require 'Administer System' permission or authentication with passcode.
-
-      When SonarQube is fully started, liveness check for database connectivity, Compute Engine status, and, except for DataCenter Edition, if ElasticSearch is Green or Yellow.
-
-      When SonarQube is on Safe Mode (for example when a database migration is running), liveness check only for database connectivity
-    """)
-  @ApiResponses(value = {
-    @ApiResponse(responseCode = "204", description = "This SonarQube node is alive"),
-    @ApiResponse(description = "This SonarQube node is not alive and should be rescheduled"),
-  })
-  void livenessCheck(
-    @Parameter(description = "Passcode can be provided, see SonarQube documentation") @RequestHeader(value = "X-Sonar-Passcode", required = false) String requestPassCode);
-}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/controller/package-info.java
deleted file mode 100644 (file)
index 26d2752..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.server.v2.controller;
-
-import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/ControllerTester.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/ControllerTester.java
new file mode 100644 (file)
index 0000000..3b209c3
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api;
+
+import org.sonar.server.v2.common.RestResponseEntityExceptionHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+public class ControllerTester {
+  public static MockMvc getMockMvc(Object... controllers) {
+    return MockMvcBuilders
+      .standaloneSetup(controllers)
+      .setControllerAdvice(new RestResponseEntityExceptionHandler())
+      .build();
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DefaultLivenessControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DefaultLivenessControllerTest.java
new file mode 100644 (file)
index 0000000..6cd65d5
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.system.controller;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.platform.ws.LivenessChecker;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
+import static org.sonar.server.v2.WebApiEndpoints.LIVENESS_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultLivenessControllerTest {
+
+  private static final String VALID_PASSCODE = "valid_passcode";
+  private static final String INVALID_PASSCODE = "invalid_passcode";
+
+  private final LivenessChecker livenessChecker = mock(LivenessChecker.class);
+  private final SystemPasscode systemPasscode = mock(SystemPasscode.class);
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultLivenessController(livenessChecker, systemPasscode, userSession));
+
+  @Test
+  public void getSystemLiveness_whenValidPasscode_shouldSucceed() throws Exception {
+    when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+    when(livenessChecker.liveness()).thenReturn(true);
+
+    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+      .andExpect(status().isNoContent());
+  }
+
+  @Test
+  public void getSystemLiveness_whenAdminCredential_shouldSucceed() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(livenessChecker.liveness()).thenReturn(true);
+
+    mockMvc.perform(get(LIVENESS_ENDPOINT))
+      .andExpect(status().isNoContent());
+  }
+
+  @Test
+  public void getSystemLiveness_whenNoUserSessionAndNoPasscode_shouldReturnForbidden() throws Exception {
+    mockMvc.perform(get(LIVENESS_ENDPOINT))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void getSystemLiveness_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
+    when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
+    userSession.logIn();
+
+    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void getSystemLiveness_whenLivenessCheckFails_shouldReturnServerError() throws Exception {
+    when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+    when(livenessChecker.liveness()).thenReturn(false);
+
+    mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+      .andExpectAll(
+        status().isInternalServerError(),
+        content().json("{\"message\":\"Liveness check failed\"}"));
+  }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/HealthControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/HealthControllerTest.java
new file mode 100644 (file)
index 0000000..947187b
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.system.controller;
+
+import com.google.gson.Gson;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.server.health.Health;
+import org.sonar.server.health.HealthChecker;
+import org.sonar.server.platform.NodeInformation;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.SystemPasscode;
+import org.sonar.server.v2.api.ControllerTester;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.user.SystemPasscodeImpl.PASSCODE_HTTP_HEADER;
+import static org.sonar.server.v2.WebApiEndpoints.HEALTH_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+public class HealthControllerTest {
+
+  private static final String VALID_PASSCODE = "valid_passcode";
+  private static final String INVALID_PASSCODE = "invalid_passcode";
+  private static final Health HEALTH_RESULT = Health.builder().
+    setStatus(Health.Status.YELLOW)
+    .addCause("One cause")
+    .build();
+
+  private final HealthChecker healthChecker = mock(HealthChecker.class);
+  private final SystemPasscode systemPasscode = mock(SystemPasscode.class);
+  private final NodeInformation nodeInformation = mock(NodeInformation.class);
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  private final MockMvc mockMvc = ControllerTester.getMockMvc(new HealthController(healthChecker, systemPasscode, nodeInformation,userSession));
+
+
+  private static final Gson gson = new Gson();
+
+  @Test
+  public void getSystemHealth_whenValidPasscodeAndStandaloneMode_shouldSucceed() throws Exception {
+    when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+    when(nodeInformation.isStandalone()).thenReturn(true);
+    when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT);
+
+    MvcResult mvcResult = mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    Health actualHealth = gson.fromJson(mvcResult.getResponse().getContentAsString(), Health.class);
+    assertThat(actualHealth).isEqualTo(HEALTH_RESULT);
+  }
+
+  @Test
+  public void getSystemHealth_whenAdminCredentialAndStandaloneMode_shouldSucceed() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    when(nodeInformation.isStandalone()).thenReturn(true);
+    when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT);
+
+    MvcResult mvcResult = mockMvc.perform(get(HEALTH_ENDPOINT))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    Health actualHealth = gson.fromJson(mvcResult.getResponse().getContentAsString(), Health.class);
+    assertThat(actualHealth).isEqualTo(HEALTH_RESULT);
+  }
+
+  @Test
+  public void getSystemHealth_whenNoCredentials_shouldReturnForbidden() throws Exception {
+    mockMvc.perform(get(HEALTH_ENDPOINT))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void getSystemHealth_whenInvalidPasscodeAndNoAdminCredentials_shouldReturnForbidden() throws Exception {
+    userSession.logIn();
+    when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false);
+
+    mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void getSystemHealth_whenValidPasscodeAndClusterMode_shouldReturnNotImplemented() throws Exception {
+    when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true);
+    when(nodeInformation.isStandalone()).thenReturn(false);
+
+    mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE))
+      .andExpectAll(
+        status().is(HTTP_NOT_IMPLEMENTED),
+        content().json("{\"message\":\"Unsupported in cluster mode\"}"));
+  }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
new file mode 100644 (file)
index 0000000..a143aea
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.user.controller;
+
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.SearchResults;
+import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserService;
+import org.sonar.server.common.user.service.UsersSearchRequest;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
+import org.sonar.server.v2.api.user.model.RestUser;
+import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_INDEX;
+import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_SIZE;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultUserControllerTest {
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  private final UserService userService = mock(UserService.class);
+  private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultUserController(userSession, userService, new UsersSearchRestResponseGenerator(userSession)));
+
+  private static final Gson gson = new Gson();
+
+  @Test
+  public void search_whenNoParameters_shouldUseDefaultAndForwardToUserService() throws Exception {
+    when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
+
+    mockMvc.perform(get(USER_ENDPOINT))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
+    verify(userService).findUsers(requestCaptor.capture());
+    assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_SIZE));
+    assertThat(requestCaptor.getValue().getPage()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_INDEX));
+    assertThat(requestCaptor.getValue().isDeactivated()).isFalse();
+  }
+
+  @Test
+  public void search_whenParametersUsed_shouldForwardWithParameters() throws Exception {
+    when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(get(USER_ENDPOINT)
+        .param("active", "false")
+        .param("managed", "true")
+        .param("q", "q")
+        .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
+        .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100")
+        .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
+        .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100")
+        .param("pageSize", "100")
+        .param("pageIndex", "2"))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
+    verify(userService).findUsers(requestCaptor.capture());
+    assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(100);
+    assertThat(requestCaptor.getValue().getPage()).isEqualTo(2);
+    assertThat(requestCaptor.getValue().isDeactivated()).isTrue();
+  }
+
+  @Test
+  public void search_whenAdminParametersUsedButNotAdmin_shouldFail() throws Exception {
+    mockMvc.perform(get(USER_ENDPOINT)
+        .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().string("{\"message\":\"parameter sonarQubeLastConnectionDateFrom requires Administer System permission.\"}"));
+
+    mockMvc.perform(get(USER_ENDPOINT)
+        .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().string("{\"message\":\"parameter sonarQubeLastConnectionDateTo requires Administer System permission.\"}"));
+
+    mockMvc.perform(get(USER_ENDPOINT)
+        .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().string("{\"message\":\"parameter sonarLintLastConnectionDateFrom requires Administer System permission.\"}"));
+
+    mockMvc.perform(get(USER_ENDPOINT)
+        .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().string("{\"message\":\"parameter sonarLintLastConnectionDateTo requires Administer System permission.\"}"));
+  }
+
+  @Test
+  public void search_whenUserServiceReturnUsers_shouldReturnThem() throws Exception {
+    UserSearchResult user1 = generateUserSearchResult("user1", true, true, false, 2, 3);
+    UserSearchResult user2 = generateUserSearchResult("user2", true, false, false, 3, 0);
+    UserSearchResult user3 = generateUserSearchResult("user3", true, false, true, 1, 1);
+    UserSearchResult user4 = generateUserSearchResult("user4", false, true, false, 0, 0);
+    List<UserSearchResult> users = List.of(user1, user2, user3, user4);
+    when(userService.findUsers(any())).thenReturn(new SearchResults<>(users, users.size()));
+    userSession.logIn().setSystemAdministrator();
+
+    MvcResult mvcResult = mockMvc.perform(get(USER_ENDPOINT))
+      .andExpect(status().isOk())
+      .andReturn();
+
+    UsersSearchRestResponse actualUsersSearchRestResponse = gson.fromJson(mvcResult.getResponse().getContentAsString(), UsersSearchRestResponse.class);
+    assertThat(actualUsersSearchRestResponse.users())
+      .containsExactlyInAnyOrder(toRestUser(user1), toRestUser(user2), toRestUser(user3), toRestUser(user4));
+    assertThat(actualUsersSearchRestResponse.pageRestResponse().total()).isEqualTo(users.size());
+
+  }
+
+  private UserSearchResult generateUserSearchResult(String id, boolean active, boolean local, boolean managed, int groupsCount, int tokensCount) {
+    UserDto userDto = new UserDto()
+      .setLogin("login_" + id)
+      .setUuid("uuid_" + id)
+      .setName("name_" + id)
+      .setEmail(id + "@email.com")
+      .setActive(active)
+      .setLocal(local)
+      .setExternalLogin("externalLogin_" + id)
+      .setExternalId("externalId_" + id)
+      .setExternalIdentityProvider("externalIdentityProvider_" + id)
+      .setLastConnectionDate(0L)
+      .setLastSonarlintConnectionDate(1L);
+
+    List<String> groups = new ArrayList<>();
+    IntStream.range(1, groupsCount).forEach(i -> groups.add("group" + i));
+
+    return new UserSearchResult(userDto, managed, Optional.of("avatar_" + id), groups, tokensCount);
+  }
+
+  private RestUser toRestUser(UserSearchResult userSearchResult) {
+    return new RestUser(
+      userSearchResult.userDto().getLogin(),
+      userSearchResult.userDto().getLogin(),
+      userSearchResult.userDto().getName(),
+      userSearchResult.userDto().getEmail(),
+      userSearchResult.userDto().isActive(),
+      userSearchResult.userDto().isLocal(),
+      userSearchResult.managed(),
+      userSearchResult.userDto().getExternalLogin(),
+      userSearchResult.userDto().getExternalIdentityProvider(),
+      userSearchResult.avatar().orElse(""),
+      formatDateTime(userSearchResult.userDto().getLastConnectionDate()),
+      formatDateTime(userSearchResult.userDto().getLastSonarlintConnectionDate()),
+      userSearchResult.groups().size(),
+      userSearchResult.tokensCount()
+    );
+  }
+
+  @Test
+  public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    userSession.logIn().setNonSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    doThrow(new NotFoundException("User not found.")).when(userService).deactivate("userToDelete", false);
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isNotFound(),
+        content().json("{\"message\":\"User not found.\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+    doThrow(BadRequestException.create("Not allowed")).when(userService).deactivate("userToDelete", false);
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json("{\"message\":\"Not allowed\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
+    userSession.logIn("userToDelete").setSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json("{\"message\":\"Self-deactivation is not possible\"}"));
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
+      .andExpect(
+        status().isBadRequest());
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpect(status().isNoContent());
+
+    verify(userService).deactivate("userToDelete", false);
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "false"))
+      .andExpect(status().isNoContent());
+
+    verify(userService).deactivate("userToDelete", false);
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
+    userSession.logIn().setSystemAdministrator();
+
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "true"))
+      .andExpect(status().isNoContent());
+
+    verify(userService).deactivate("userToDelete", true);
+  }
+}