3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.v2.api.user.controller;
22 import com.google.gson.Gson;
23 import com.google.gson.GsonBuilder;
24 import com.google.gson.TypeAdapter;
25 import com.google.gson.stream.JsonReader;
26 import com.google.gson.stream.JsonWriter;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Optional;
30 import java.util.stream.IntStream;
31 import org.junit.Rule;
32 import org.junit.Test;
33 import org.mockito.ArgumentCaptor;
34 import org.sonar.db.user.UserDto;
35 import org.sonar.server.common.SearchResults;
36 import org.sonar.server.common.user.service.UserInformation;
37 import org.sonar.server.common.user.service.UserService;
38 import org.sonar.server.common.user.service.UsersSearchRequest;
39 import org.sonar.server.exceptions.BadRequestException;
40 import org.sonar.server.exceptions.NotFoundException;
41 import org.sonar.server.tester.UserSessionRule;
42 import org.sonar.server.user.UpdateUser;
43 import org.sonar.server.v2.api.ControllerTester;
44 import org.sonar.server.v2.api.model.RestError;
45 import org.sonar.server.v2.api.response.PageRestResponse;
46 import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
47 import org.sonar.server.v2.api.user.model.RestUser;
48 import org.sonar.server.v2.api.user.model.RestUserForAdmins;
49 import org.sonar.server.v2.api.user.request.UserCreateRestRequest;
50 import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
51 import org.springframework.http.MediaType;
52 import org.springframework.test.web.servlet.MockMvc;
53 import org.springframework.test.web.servlet.MvcResult;
55 import static org.assertj.core.api.Assertions.assertThat;
56 import static org.mockito.ArgumentMatchers.any;
57 import static org.mockito.ArgumentMatchers.eq;
58 import static org.mockito.Mockito.doThrow;
59 import static org.mockito.Mockito.mock;
60 import static org.mockito.Mockito.verify;
61 import static org.mockito.Mockito.when;
62 import static org.sonar.api.utils.DateUtils.formatDateTime;
63 import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
64 import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
65 import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_INDEX;
66 import static org.sonar.server.v2.api.model.RestPage.DEFAULT_PAGE_SIZE;
67 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
68 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
69 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
70 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
71 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
72 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
74 public class DefaultUserControllerTest {
77 public UserSessionRule userSession = UserSessionRule.standalone();
78 private final UserService userService = mock(UserService.class);
79 private final UsersSearchRestResponseGenerator responseGenerator = mock(UsersSearchRestResponseGenerator.class);
80 private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultUserController(userSession, userService, responseGenerator));
82 private static final Gson gson = new GsonBuilder().registerTypeAdapter(RestUser.class, new RestUserDeserializer()).create();
85 public void search_whenNoParameters_shouldUseDefaultAndForwardToUserService() throws Exception {
86 when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
88 mockMvc.perform(get(USER_ENDPOINT))
89 .andExpect(status().isOk());
91 ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
92 verify(userService).findUsers(requestCaptor.capture());
93 assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_SIZE));
94 assertThat(requestCaptor.getValue().getPage()).isEqualTo(Integer.valueOf(DEFAULT_PAGE_INDEX));
95 assertThat(requestCaptor.getValue().isDeactivated()).isFalse();
99 public void search_whenParametersUsed_shouldForwardWithParameters() throws Exception {
100 when(userService.findUsers(any())).thenReturn(new SearchResults<>(List.of(), 0));
101 userSession.logIn().setSystemAdministrator();
103 mockMvc.perform(get(USER_ENDPOINT)
104 .param("active", "false")
105 .param("managed", "true")
107 .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
108 .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100")
109 .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100")
110 .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100")
111 .param("pageSize", "100")
112 .param("pageIndex", "2"))
113 .andExpect(status().isOk());
115 ArgumentCaptor<UsersSearchRequest> requestCaptor = ArgumentCaptor.forClass(UsersSearchRequest.class);
116 verify(userService).findUsers(requestCaptor.capture());
117 assertThat(requestCaptor.getValue().getPageSize()).isEqualTo(100);
118 assertThat(requestCaptor.getValue().getPage()).isEqualTo(2);
119 assertThat(requestCaptor.getValue().isDeactivated()).isTrue();
123 public void search_whenAdminParametersUsedButNotAdmin_shouldFail() throws Exception {
124 mockMvc.perform(get(USER_ENDPOINT)
125 .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
127 status().isForbidden(),
128 content().string("{\"message\":\"parameter sonarQubeLastConnectionDateFrom requires Administer System permission.\"}"));
130 mockMvc.perform(get(USER_ENDPOINT)
131 .param("sonarQubeLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
133 status().isForbidden(),
134 content().string("{\"message\":\"parameter sonarQubeLastConnectionDateTo requires Administer System permission.\"}"));
136 mockMvc.perform(get(USER_ENDPOINT)
137 .param("sonarLintLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
139 status().isForbidden(),
140 content().string("{\"message\":\"parameter sonarLintLastConnectionDateFrom requires Administer System permission.\"}"));
142 mockMvc.perform(get(USER_ENDPOINT)
143 .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
145 status().isForbidden(),
146 content().string("{\"message\":\"parameter sonarLintLastConnectionDateTo requires Administer System permission.\"}"));
150 public void search_whenUserServiceReturnUsers_shouldReturnThem() throws Exception {
151 UserInformation user1 = generateUserSearchResult("user1", true, true, false, 2, 3);
152 UserInformation user2 = generateUserSearchResult("user2", true, false, false, 3, 0);
153 UserInformation user3 = generateUserSearchResult("user3", true, false, true, 1, 1);
154 UserInformation user4 = generateUserSearchResult("user4", false, true, false, 0, 0);
155 List<UserInformation> users = List.of(user1, user2, user3, user4);
156 SearchResults<UserInformation> searchResult = new SearchResults<>(users, users.size());
157 when(userService.findUsers(any())).thenReturn(searchResult);
158 List<RestUser> restUserForAdmins = List.of(toRestUser(user1), toRestUser(user2), toRestUser(user3), toRestUser(user4));
159 when(responseGenerator.toUsersForResponse(eq(searchResult.searchResults()), any())).thenReturn(new UsersSearchRestResponse(restUserForAdmins, new PageRestResponse(1, 50, 4)));
160 userSession.logIn().setSystemAdministrator();
162 MvcResult mvcResult = mockMvc.perform(get(USER_ENDPOINT))
163 .andExpect(status().isOk())
166 UsersSearchRestResponse actualUsersSearchRestResponse = gson.fromJson(mvcResult.getResponse().getContentAsString(), UsersSearchRestResponse.class);
167 assertThat(actualUsersSearchRestResponse.users())
168 .containsExactlyElementsOf(restUserForAdmins);
169 assertThat(actualUsersSearchRestResponse.page().total()).isEqualTo(users.size());
173 static class RestUserDeserializer extends TypeAdapter<RestUser> {
176 public void write(JsonWriter out, RestUser value) {
177 throw new IllegalStateException("not implemented");
181 public RestUser read(JsonReader reader) {
182 return gson.fromJson(reader, RestUserForAdmins.class);
186 private UserInformation generateUserSearchResult(String id, boolean active, boolean local, boolean managed, int groupsCount, int tokensCount) {
187 UserDto userDto = new UserDto()
188 .setLogin("login_" + id)
189 .setUuid("uuid_" + id)
190 .setName("name_" + id)
191 .setEmail(id + "@email.com")
194 .setExternalLogin("externalLogin_" + id)
195 .setExternalId("externalId_" + id)
196 .setExternalIdentityProvider("externalIdentityProvider_" + id)
197 .setLastConnectionDate(0L)
198 .setLastSonarlintConnectionDate(1L);
200 List<String> groups = new ArrayList<>();
201 IntStream.range(1, groupsCount).forEach(i -> groups.add("group" + i));
203 return new UserInformation(userDto, managed, Optional.of("avatar_" + id), groups, tokensCount);
206 private RestUserForAdmins toRestUser(UserInformation userInformation) {
207 return new RestUserForAdmins(
208 userInformation.userDto().getLogin(),
209 userInformation.userDto().getLogin(),
210 userInformation.userDto().getName(),
211 userInformation.userDto().getEmail(),
212 userInformation.userDto().isActive(),
213 userInformation.userDto().isLocal(),
214 userInformation.managed(),
215 userInformation.userDto().getExternalLogin(),
216 userInformation.userDto().getExternalIdentityProvider(),
217 userInformation.avatar().orElse(""),
218 formatDateTime(userInformation.userDto().getLastConnectionDate()),
219 formatDateTime(userInformation.userDto().getLastSonarlintConnectionDate()),
220 userInformation.userDto().getSortedScmAccounts());
224 public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
225 userSession.logIn().setNonSystemAdministrator();
227 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
229 status().isForbidden(),
230 content().json("{\"message\":\"Insufficient privileges\"}"));
234 public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
235 userSession.logIn().setSystemAdministrator();
236 doThrow(new NotFoundException("User not found.")).when(userService).deactivate("userToDelete", false);
238 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
240 status().isNotFound(),
241 content().json("{\"message\":\"User not found.\"}"));
245 public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
246 userSession.logIn().setSystemAdministrator();
247 doThrow(BadRequestException.create("Not allowed")).when(userService).deactivate("userToDelete", false);
249 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
251 status().isBadRequest(),
252 content().json("{\"message\":\"Not allowed\"}"));
256 public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
257 userSession.logIn("userToDelete").setSystemAdministrator();
259 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
261 status().isBadRequest(),
262 content().json("{\"message\":\"Self-deactivation is not possible\"}"));
266 public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
267 userSession.logIn().setSystemAdministrator();
269 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "maybe"))
271 status().isBadRequest());
275 public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
276 userSession.logIn().setSystemAdministrator();
278 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
279 .andExpect(status().isNoContent());
281 verify(userService).deactivate("userToDelete", false);
285 public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
286 userSession.logIn().setSystemAdministrator();
288 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "false"))
289 .andExpect(status().isNoContent());
291 verify(userService).deactivate("userToDelete", false);
295 public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
296 userSession.logIn().setSystemAdministrator();
298 mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete").param("anonymize", "true"))
299 .andExpect(status().isNoContent());
301 verify(userService).deactivate("userToDelete", true);
305 public void fetchUser_whenUserServiceThrowsNotFoundException_returnsNotFound() throws Exception {
306 when(userService.fetchUser("userLogin")).thenThrow(new NotFoundException("Not found"));
307 mockMvc.perform(get(USER_ENDPOINT + "/userLogin"))
309 status().isNotFound(),
310 content().json("{\"message\":\"Not found\"}")
316 public void fetchUser_whenUserExists_shouldReturnUser() throws Exception {
317 UserInformation user = generateUserSearchResult("user1", true, true, false, 2, 3);
318 RestUserForAdmins restUserForAdmins = toRestUser(user);
319 when(userService.fetchUser("userLogin")).thenReturn(user);
320 when(responseGenerator.toRestUser(user)).thenReturn(restUserForAdmins);
321 MvcResult mvcResult = mockMvc.perform(get(USER_ENDPOINT + "/userLogin"))
322 .andExpect(status().isOk())
324 RestUserForAdmins responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUserForAdmins.class);
325 assertThat(responseUser).isEqualTo(restUserForAdmins);
329 public void create_whenNotAnAdmin_shouldReturnForbidden() throws Exception {
330 userSession.logIn().setNonSystemAdministrator();
334 .contentType(MediaType.APPLICATION_JSON_VALUE)
335 .content(gson.toJson(new UserCreateRestRequest(null, null, "login", "name", null, null))))
337 status().isForbidden(),
338 content().json("{\"message\":\"Insufficient privileges\"}"));
342 public void create_whenNoLogin_shouldReturnBadRequest() throws Exception {
343 userSession.logIn().setSystemAdministrator();
347 .contentType(MediaType.APPLICATION_JSON_VALUE)
348 .content(gson.toJson(new UserCreateRestRequest(null, null, null, "name", null, null))))
350 status().isBadRequest(),
351 content().json("{\"message\":\"Value {} for field login was rejected. Error: must not be null.\"}"));
355 public void create_whenNoName_shouldReturnBadRequest() throws Exception {
356 userSession.logIn().setSystemAdministrator();
360 .contentType(MediaType.APPLICATION_JSON_VALUE)
361 .content(gson.toJson(new UserCreateRestRequest(null, null, "login", null, null, null))))
363 status().isBadRequest(),
364 content().json("{\"message\":\"Value {} for field name was rejected. Error: must not be null.\"}"));
368 public void create_whenUserServiceThrow_shouldReturnServerError() throws Exception {
369 userSession.logIn().setSystemAdministrator();
370 when(userService.createUser(any())).thenThrow(new IllegalArgumentException("IllegalArgumentException"));
374 .contentType(MediaType.APPLICATION_JSON_VALUE)
375 .content(gson.toJson(new UserCreateRestRequest("e@mail.com", true, "login", "name", "password", List.of("scm")))))
377 status().isBadRequest(),
378 content().json("{\"message\":\"IllegalArgumentException\"}"));
382 public void create_whenUserServiceReturnUser_shouldReturnIt() throws Exception {
383 userSession.logIn().setSystemAdministrator();
384 UserInformation userInformation = generateUserSearchResult("1", true, true, false, 1, 2);
385 UserDto userDto = userInformation.userDto();
386 when(userService.createUser(any())).thenReturn(userInformation);
387 when(responseGenerator.toRestUser(userInformation)).thenReturn(toRestUser(userInformation));
389 MvcResult mvcResult = mockMvc.perform(
391 .contentType(MediaType.APPLICATION_JSON_VALUE)
392 .content(gson.toJson(new UserCreateRestRequest(
393 userDto.getEmail(), userDto.isLocal(), userDto.getLogin(), userDto.getName(), "password", userDto.getSortedScmAccounts()))))
394 .andExpect(status().isOk())
396 RestUserForAdmins responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUserForAdmins.class);
397 assertThat(responseUser).isEqualTo(toRestUser(userInformation));
401 public void updateUser_whenUserDoesntExist_shouldReturnNotFound() throws Exception {
402 userSession.logIn().setSystemAdministrator();
403 when(userService.updateUser(eq("userLogin"), any(UpdateUser.class))).thenThrow(new NotFoundException("Not found"));
404 mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
405 .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
408 status().isNotFound(),
409 content().json("{\"message\":\"Not found\"}"));
413 public void updateUser_whenCallerIsNotAdmin_shouldReturnForbidden() throws Exception {
414 userSession.logIn().setNonSystemAdministrator();
417 patch(USER_ENDPOINT + "/userLogin")
418 .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
421 status().isForbidden(),
422 content().json("{\"message\":\"Insufficient privileges\"}"));
426 public void updateUser_whenEmailIsProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
427 UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"email\":\"newemail@example.com\"}");
428 assertThat(userUpdate.email()).isEqualTo("newemail@example.com");
429 assertThat(userUpdate.name()).isNull();
430 assertThat(userUpdate.scmAccounts()).isNull();
433 private UpdateUser performPatchCallAndVerifyResponse(String payload) throws Exception {
434 userSession.logIn().setSystemAdministrator();
435 UserInformation userInformation = generateUserSearchResult("1", true, true, false, 1, 2);
437 when(userService.updateUser(eq("userLogin"), any())).thenReturn(userInformation);
438 when(responseGenerator.toRestUser(userInformation)).thenReturn(toRestUser(userInformation));
440 MvcResult mvcResult = mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
441 .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
447 RestUserForAdmins responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUserForAdmins.class);
448 assertThat(responseUser).isEqualTo(toRestUser(userInformation));
450 ArgumentCaptor<UpdateUser> updateUserCaptor = ArgumentCaptor.forClass(UpdateUser.class);
451 verify(userService).updateUser(eq("userLogin"), updateUserCaptor.capture());
452 return updateUserCaptor.getValue();
456 public void updateUser_whenNameIsProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
457 UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"name\":\"new name\"}");
458 assertThat(userUpdate.email()).isNull();
459 assertThat(userUpdate.name()).isEqualTo("new name");
460 assertThat(userUpdate.scmAccounts()).isNull();
464 public void updateUser_whenScmAccountsAreProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
465 UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"scmAccounts\":[\"account1\",\"account2\"]}");
466 assertThat(userUpdate.email()).isNull();
467 assertThat(userUpdate.name()).isNull();
468 assertThat(userUpdate.scmAccounts()).containsExactly("account1", "account2");
472 public void updateUser_whenEmailIsInvalid_shouldReturnBadRequest() throws Exception {
473 performPatchCallAndExpectBadRequest("{\"email\":\"notavalidemail\"}", "Value notavalidemail for field email was rejected. Error: must be a well-formed email address.");
476 private void performPatchCallAndExpectBadRequest(String payload, String expectedMessage) throws Exception {
477 userSession.logIn().setSystemAdministrator();
479 MvcResult mvcResult = mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
480 .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
483 status().isBadRequest())
486 RestError error = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestError.class);
487 assertThat(error.message()).isEqualTo(expectedMessage);
491 public void updateUser_whenEmailIsEmpty_shouldReturnBadRequest() throws Exception {
492 performPatchCallAndExpectBadRequest("{\"email\":\"\"}", "Value for field email was rejected. Error: size must be between 1 and 100.");
496 public void updateUser_whenNameIsTooLong_shouldReturnBadRequest() throws Exception {
497 String tooLong = "toolong".repeat(30);
498 String payload = "{\"name\":\"" + tooLong + "\"}";
499 String message = "Value " + tooLong + " for field name was rejected. Error: size must be between 0 and 200.";
500 performPatchCallAndExpectBadRequest(payload, message);