@@ -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')) | |||
} | |||
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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"; | |||
@@ -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; |
@@ -17,24 +17,21 @@ | |||
* 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; | |||
package org.sonar.server.v2.api.system.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 { | |||
public class DefaultLivenessController 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) { | |||
public DefaultLivenessController(LivenessChecker livenessChecker, SystemPasscode systemPasscode, @Nullable UserSession userSession) { | |||
this.livenessChecker = livenessChecker; | |||
this.userSession = userSession; | |||
this.systemPasscode = systemPasscode; | |||
@@ -57,4 +54,5 @@ public class DefautLivenessController implements LivenessController { | |||
} | |||
return userSession.isSystemAdministrator(); | |||
} | |||
} |
@@ -17,7 +17,7 @@ | |||
* 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; | |||
package org.sonar.server.v2.api.system.controller; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.ServerException; |
@@ -17,7 +17,7 @@ | |||
* 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; | |||
package org.sonar.server.v2.api.system.controller; | |||
import io.swagger.v3.oas.annotations.Operation; | |||
import io.swagger.v3.oas.annotations.Parameter; |
@@ -18,6 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.v2.controller; | |||
package org.sonar.server.v2.api.system.controller; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -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 |
@@ -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 |
@@ -17,33 +17,17 @@ | |||
* 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; | |||
package org.sonar.server.v2.api; | |||
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.sonar.server.v2.common.RestResponseEntityExceptionHandler; | |||
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(); | |||
public class ControllerTester { | |||
public static MockMvc getMockMvc(Object... controllers) { | |||
return MockMvcBuilders | |||
.standaloneSetup(controllers) | |||
.setControllerAdvice(new RestResponseEntityExceptionHandler()) | |||
.build(); | |||
} | |||
} |
@@ -17,17 +17,17 @@ | |||
* 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; | |||
package org.sonar.server.v2.api.system.controller; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.sonar.server.platform.ws.LivenessChecker; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.user.SystemPasscode; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.common.ControllerIT; | |||
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; | |||
@@ -35,26 +35,21 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder | |||
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 { | |||
public class DefaultLivenessControllerTest { | |||
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)); | |||
} | |||
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(webAppContext.getBean(SystemPasscode.class).isValidPasscode(VALID_PASSCODE)).thenReturn(true); | |||
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()); | |||
@@ -62,7 +57,8 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
@Test | |||
public void getSystemLiveness_whenAdminCredential_shouldSucceed() throws Exception { | |||
when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true); | |||
userSession.logIn().setSystemAdministrator(); | |||
when(livenessChecker.liveness()).thenReturn(true); | |||
mockMvc.perform(get(LIVENESS_ENDPOINT)) | |||
.andExpect(status().isNoContent()); | |||
@@ -78,8 +74,8 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
@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); | |||
when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false); | |||
userSession.logIn(); | |||
mockMvc.perform(get(LIVENESS_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) | |||
.andExpectAll( | |||
@@ -89,12 +85,13 @@ public class DefaultLivenessControllerIT extends ControllerIT { | |||
@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); | |||
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\"}")); | |||
} | |||
} |
@@ -17,21 +17,23 @@ | |||
* 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; | |||
package org.sonar.server.v2.api.system.controller; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import com.google.gson.Gson; | |||
import org.junit.Rule; | |||
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.tester.UserSessionRule; | |||
import org.sonar.server.user.SystemPasscode; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.v2.common.ControllerIT; | |||
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; | |||
@@ -40,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. | |||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | |||
public class HealthControllerIT extends ControllerIT { | |||
public class HealthControllerTest { | |||
private static final String VALID_PASSCODE = "valid_passcode"; | |||
private static final String INVALID_PASSCODE = "invalid_passcode"; | |||
@@ -49,51 +51,42 @@ public class HealthControllerIT extends ControllerIT { | |||
.addCause("One cause") | |||
.build(); | |||
@Before | |||
public void setup() { | |||
super.setup(); | |||
when(webAppContext.getBean(HealthChecker.class).checkNode()).thenReturn(HEALTH_RESULT); | |||
} | |||
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)); | |||
@After | |||
public void resetUsedMocks() { | |||
Mockito.reset(webAppContext.getBean(SystemPasscode.class)); | |||
Mockito.reset(webAppContext.getBean(NodeInformation.class)); | |||
Mockito.reset(webAppContext.getBean(UserSession.class)); | |||
} | |||
private static final Gson gson = new Gson(); | |||
@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); | |||
when(systemPasscode.isValidPasscode(VALID_PASSCODE)).thenReturn(true); | |||
when(nodeInformation.isStandalone()).thenReturn(true); | |||
when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT); | |||
mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, VALID_PASSCODE)) | |||
.andExpectAll( | |||
status().isOk(), | |||
content().json(""" | |||
{ | |||
"status":"YELLOW", | |||
"causes":[ | |||
"One cause" | |||
] | |||
}""")); | |||
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 { | |||
when(webAppContext.getBean(UserSession.class).isSystemAdministrator()).thenReturn(true); | |||
when(webAppContext.getBean(NodeInformation.class).isStandalone()).thenReturn(true); | |||
userSession.logIn().setSystemAdministrator(); | |||
when(nodeInformation.isStandalone()).thenReturn(true); | |||
when(healthChecker.checkNode()).thenReturn(HEALTH_RESULT); | |||
mockMvc.perform(get(HEALTH_ENDPOINT)) | |||
.andExpectAll( | |||
status().isOk(), | |||
content().json(""" | |||
{ | |||
"status":"YELLOW", | |||
"causes":[ | |||
"One cause" | |||
] | |||
}""")); | |||
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 | |||
@@ -106,8 +99,8 @@ public class HealthControllerIT extends ControllerIT { | |||
@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); | |||
userSession.logIn(); | |||
when(systemPasscode.isValidPasscode(INVALID_PASSCODE)).thenReturn(false); | |||
mockMvc.perform(get(HEALTH_ENDPOINT).header(PASSCODE_HTTP_HEADER, INVALID_PASSCODE)) | |||
.andExpectAll( | |||
@@ -115,24 +108,15 @@ public class HealthControllerIT extends ControllerIT { | |||
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); | |||
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\"}")); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |