aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-webapi-v2
diff options
context:
space:
mode:
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>2023-09-04 13:55:21 +0200
committersonartech <sonartech@sonarsource.com>2023-09-04 15:48:18 +0000
commita2e9af8f2ed602ccc96eebe74a007c4b8a4e8ca2 (patch)
treead7893ae0cc7017e7511e2d33221ae60c2f5d295 /server/sonar-webserver-webapi-v2
parenta506cb857145802319545ff8ca4d4e5258fc8f67 (diff)
downloadsonarqube-a2e9af8f2ed602ccc96eebe74a007c4b8a4e8ca2.tar.gz
sonarqube-a2e9af8f2ed602ccc96eebe74a007c4b8a4e8ca2.zip
SONAR-20285 PATCH endpoint to update users
Co-authored-by: Aurelien Poscia <aurelien.poscia@sonarsource.com>
Diffstat (limited to 'server/sonar-webserver-webapi-v2')
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java1
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java22
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java20
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java20
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUserForAdmins.java7
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserCreateRestRequest.java4
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserUpdateRestRequest.java64
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/UpdateFieldValueExtractor.java34
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java2
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/UpdateField.java64
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/package-info.java23
-rw-r--r--server/sonar-webserver-webapi-v2/src/main/resources/META-INF/services/javax.validation.valueextraction.ValueExtractor1
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java211
-rw-r--r--server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java62
15 files changed, 454 insertions, 104 deletions
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
index 87ef45e672c..665f7e2eff7 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java
@@ -24,6 +24,7 @@ public class WebApiEndpoints {
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";
+ public static final String JSON_MERGE_PATCH_CONTENT_TYPE = "application/json-merge-patch+json";
private WebApiEndpoints() {
}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
index 7a100dfdb77..d29515e9047 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
@@ -24,16 +24,18 @@ import javax.annotation.Nullable;
import org.sonar.server.common.PaginationInformation;
import org.sonar.server.common.SearchResults;
import org.sonar.server.common.user.service.UserCreateRequest;
-import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserInformation;
import org.sonar.server.common.user.service.UserService;
import org.sonar.server.common.user.service.UsersSearchRequest;
import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.user.UpdateUser;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.model.RestPage;
import org.sonar.server.v2.api.model.RestSortOrder;
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.request.UserCreateRestRequest;
+import org.sonar.server.v2.api.user.request.UserUpdateRestRequest;
import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
@@ -59,7 +61,7 @@ public class DefaultUserController implements UserController {
throwIfAdminOnlyParametersAreUsed(usersSearchRestRequest);
checkRequest(!RestSortOrder.DESC.equals(order), "order parameter is present for doc-demo purpose, it will be removed.");
- SearchResults<UserSearchResult> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, page));
+ SearchResults<UserInformation> userSearchResults = userService.findUsers(toUserSearchRequest(usersSearchRestRequest, page));
PaginationInformation paging = forPageIndex(page.pageIndex()).withPageSize(page.pageSize()).andTotal(userSearchResults.total());
return usersSearchResponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging);
@@ -109,6 +111,22 @@ public class DefaultUserController implements UserController {
}
@Override
+ public RestUser updateUser(String login, UserUpdateRestRequest updateRequest) {
+ userSession.checkLoggedIn().checkIsSystemAdministrator();
+ UpdateUser update = toUpdateUser(updateRequest);
+ UserInformation updatedUser = userService.updateUser(login, update);
+ return usersSearchResponseGenerator.toRestUser(updatedUser);
+ }
+
+ private static UpdateUser toUpdateUser(UserUpdateRestRequest updateRequest) {
+ UpdateUser update = new UpdateUser();
+ updateRequest.getName().applyIfDefined(update::setName);
+ updateRequest.getEmail().applyIfDefined(update::setEmail);
+ updateRequest.getScmAccounts().applyIfDefined(update::setScmAccounts);
+ return update;
+ }
+
+ @Override
public RestUser create(UserCreateRestRequest userCreateRestRequest) {
userSession.checkLoggedIn().checkIsSystemAdministrator();
UserCreateRequest userCreateRequest = toUserCreateRequest(userCreateRestRequest);
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
index 2d027f43b85..e80151fdf8c 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
@@ -28,6 +28,7 @@ import org.sonar.server.v2.api.model.RestPage;
import org.sonar.server.v2.api.model.RestSortOrder;
import org.sonar.server.v2.api.user.model.RestUser;
import org.sonar.server.v2.api.user.request.UserCreateRestRequest;
+import org.sonar.server.v2.api.user.request.UserUpdateRestRequest;
import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
import org.springdoc.api.annotations.ParameterObject;
@@ -35,6 +36,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -43,12 +45,14 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
+import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE;
import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
@RequestMapping(USER_ENDPOINT)
@RestController
public interface UserController {
+
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Users search", description = """
@@ -72,12 +76,8 @@ public interface UserController {
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Deactivate a user", description = "Deactivates a user. Requires Administer System permission.")
void deactivate(
- @PathVariable("login")
- @Parameter(description = "The login of the user to delete.", required = true, in = ParameterIn.PATH)
- String login,
- @RequestParam(value = "anonymize", required = false, defaultValue = "false")
- @Parameter(description = "Anonymize user in addition to deactivating it.")
- Boolean anonymize);
+ @PathVariable("login") @Parameter(description = "The login of the user to delete.", required = true, in = ParameterIn.PATH) String login,
+ @RequestParam(value = "anonymize", required = false, defaultValue = "false") @Parameter(description = "Anonymize user in addition to deactivating it.") Boolean anonymize);
@GetMapping(path = "/{login}")
@ResponseStatus(HttpStatus.OK)
@@ -95,6 +95,14 @@ public interface UserController {
""")
RestUser fetchUser(@PathVariable("login") @Parameter(description = "The login of the user to fetch.", required = true, in = ParameterIn.PATH) String login);
+ @PatchMapping(path = "/{login}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.OK)
+ @Operation(summary = "Update a user", description = """
+ Update a user.
+ Allows updating user's name, email and SCM accounts.
+ """)
+ RestUser updateUser(@PathVariable("login") String login, @Valid @RequestBody UserUpdateRestRequest updateRequest);
+
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "User creation", description = """
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
index ef3c07b5702..f6ec6336fa5 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGenerator.java
@@ -27,7 +27,7 @@ import org.sonar.api.utils.DateUtils;
import org.sonar.db.user.UserDto;
import org.sonar.server.common.PaginationInformation;
import org.sonar.server.common.user.UsersSearchResponseGenerator;
-import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserInformation;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.response.PageRestResponse;
import org.sonar.server.v2.api.user.model.RestUser;
@@ -45,20 +45,20 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
}
@Override
- public UsersSearchRestResponse toUsersForResponse(List<UserSearchResult> userSearchResults, PaginationInformation paginationInformation) {
- List<RestUser> usersForResponse = toUsersForResponse(userSearchResults);
+ public UsersSearchRestResponse toUsersForResponse(List<UserInformation> userInformations, PaginationInformation paginationInformation) {
+ List<RestUser> usersForResponse = toUsersForResponse(userInformations);
PageRestResponse pageRestResponse = new PageRestResponse(paginationInformation.pageIndex(), paginationInformation.pageSize(), paginationInformation.total());
return new UsersSearchRestResponse(usersForResponse, pageRestResponse);
}
- private List<RestUser> toUsersForResponse(List<UserSearchResult> userSearchResults) {
- return userSearchResults.stream()
+ private List<RestUser> toUsersForResponse(List<UserInformation> userInformations) {
+ return userInformations.stream()
.map(this::toRestUser)
.toList();
}
- public RestUser toRestUser(UserSearchResult userSearchResult) {
- UserDto userDto = userSearchResult.userDto();
+ public RestUser toRestUser(UserInformation userInformation) {
+ UserDto userDto = userInformation.userDto();
String login = userDto.getLogin();
String name = userDto.getName();
@@ -66,17 +66,17 @@ public class UsersSearchRestResponseGenerator implements UsersSearchResponseGene
return new RestUserForAnonymousUsers(login, login, name);
}
- String avatar = userSearchResult.avatar().orElse(null);
+ String avatar = userInformation.avatar().orElse(null);
Boolean active = userDto.isActive();
Boolean local = userDto.isLocal();
String email = userDto.getEmail();
String externalIdentityProvider = userDto.getExternalIdentityProvider();
if (userSession.isSystemAdministrator() || Objects.equals(userSession.getUuid(), userDto.getUuid())) {
String externalLogin = userDto.getExternalLogin();
- Boolean managed = userSearchResult.managed();
+ Boolean managed = userInformation.managed();
String sqLastConnectionDate = toDateTime(userDto.getLastConnectionDate());
String slLastConnectionDate = toDateTime(userDto.getLastSonarlintConnectionDate());
- List<String> scmAccounts = userSearchResult.userDto().getSortedScmAccounts();
+ List<String> scmAccounts = userInformation.userDto().getSortedScmAccounts();
return new RestUserForAdmins(
login,
login,
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUserForAdmins.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUserForAdmins.java
index 5de5cc9bdbc..e7c4ea755cd 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUserForAdmins.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/model/RestUserForAdmins.java
@@ -19,20 +19,25 @@
*/
package org.sonar.server.v2.api.user.model;
+import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import javax.annotation.Nullable;
public record RestUserForAdmins(
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
String id,
String login,
String name,
@Nullable
String email,
@Nullable
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
Boolean active,
@Nullable
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
Boolean local,
@Nullable
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
Boolean managed,
@Nullable
String externalLogin,
@@ -41,8 +46,10 @@ public record RestUserForAdmins(
@Nullable
String avatar,
@Nullable
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
String sonarQubeLastConnectionDate,
@Nullable
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY)
String sonarLintLastConnectionDate,
@Nullable
List<String> scmAccounts
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserCreateRestRequest.java
index 9b28d032c22..a7e05e6a062 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserCreateRestRequest.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserCreateRestRequest.java
@@ -29,7 +29,7 @@ import javax.validation.constraints.Size;
public record UserCreateRestRequest(
@Nullable
@Email
- @Size(max = 100)
+ @Size(min = 1, max = 100)
@Schema(description = "User email")
String email,
@@ -50,7 +50,7 @@ public record UserCreateRestRequest(
String name,
@Nullable
- @Schema(description = "User password. Only mandatory when creating local user, otherwise it should not be set")
+ @Schema(description = "User password. Only mandatory when creating local user, otherwise it should not be set", accessMode = Schema.AccessMode.WRITE_ONLY)
String password,
@Nullable
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserUpdateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserUpdateRestRequest.java
new file mode 100644
index 00000000000..5418c74664e
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/request/UserUpdateRestRequest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.request;
+
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.Size;
+import org.sonar.server.v2.common.model.UpdateField;
+
+public class UserUpdateRestRequest {
+
+ private UpdateField<String> name = UpdateField.undefined();
+ private UpdateField<String> email = UpdateField.undefined();
+ private UpdateField<List<String>> scmAccounts = UpdateField.undefined();
+
+ @Size(max = 200)
+ @Schema(description = "User first name and last name", implementation = String.class)
+ public UpdateField< String> getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = UpdateField.withValue(name);
+ }
+
+ @Email
+ @Size(min = 1, max = 100)
+ @Schema(implementation = String.class, description = "Email")
+ public UpdateField<String> getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = UpdateField.withValue(email);
+ }
+
+ @ArraySchema(arraySchema = @Schema(description = "List of SCM accounts."), schema = @Schema(implementation = String.class))
+ public UpdateField<List<String>> getScmAccounts() {
+ return scmAccounts;
+ }
+
+ public void setScmAccounts(List<String> scmAccounts) {
+ this.scmAccounts = UpdateField.withValue(scmAccounts);
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/UpdateFieldValueExtractor.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/UpdateFieldValueExtractor.java
new file mode 100644
index 00000000000..1e7967a05f2
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/UpdateFieldValueExtractor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.validation;
+
+import javax.validation.valueextraction.ExtractedValue;
+import javax.validation.valueextraction.UnwrapByDefault;
+import javax.validation.valueextraction.ValueExtractor;
+import org.sonar.server.v2.common.model.UpdateField;
+
+@UnwrapByDefault
+public class UpdateFieldValueExtractor implements ValueExtractor<UpdateField<@ExtractedValue ?>> {
+
+ @Override
+ public void extractValues(UpdateField<?> originalValue, ValueReceiver receiver) {
+ receiver.value(null, originalValue.getValue());
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/package-info.java
new file mode 100644
index 00000000000..97a74ea79f0
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/validation/package-info.java
@@ -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.validation;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
index 8bd0012b75c..dd8d133e37d 100644
--- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java
@@ -63,7 +63,7 @@ public class RestResponseEntityExceptionHandler {
String fieldName = fieldError.getField();
String rejectedValueAsString = Optional.ofNullable(fieldError.getRejectedValue()).map(Object::toString).orElse("{}");
String defaultMessage = fieldError.getDefaultMessage();
- return String.format("Value %s for field %s was rejected. Error: %s", rejectedValueAsString, fieldName, defaultMessage);
+ return String.format("Value %s for field %s was rejected. Error: %s.", rejectedValueAsString, fieldName, defaultMessage);
}
@ExceptionHandler({ServerException.class, ForbiddenException.class, UnauthorizedException.class, BadRequestException.class})
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/UpdateField.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/UpdateField.java
new file mode 100644
index 00000000000..d07b6a9e7a5
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/UpdateField.java
@@ -0,0 +1,64 @@
+/*
+ * 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.model;
+
+import java.util.function.Consumer;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.validation.valueextraction.UnwrapByDefault;
+
+@UnwrapByDefault
+public class UpdateField<T> {
+ private final T value;
+ private final boolean isDefined;
+
+ private UpdateField(@Nullable T value, boolean isDefined) {
+ this.value = value;
+ this.isDefined = isDefined;
+ }
+
+ public static <T> UpdateField<T> withValue(@Nullable T value) {
+ return new UpdateField<>(value, true);
+ }
+
+ public static <T> UpdateField<T> undefined() {
+ return new UpdateField<>(null, false);
+ }
+
+ @CheckForNull
+ public T getValue() {
+ return value;
+ }
+
+ public boolean isDefined() {
+ return isDefined;
+ }
+
+ public void applyIfDefined(Consumer<T> consumer) {
+ if (isDefined) {
+ consumer.accept(value);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/package-info.java
new file mode 100644
index 00000000000..c0cc455b026
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/model/package-info.java
@@ -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.common.model;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi-v2/src/main/resources/META-INF/services/javax.validation.valueextraction.ValueExtractor b/server/sonar-webserver-webapi-v2/src/main/resources/META-INF/services/javax.validation.valueextraction.ValueExtractor
new file mode 100644
index 00000000000..8d8a32809f4
--- /dev/null
+++ b/server/sonar-webserver-webapi-v2/src/main/resources/META-INF/services/javax.validation.valueextraction.ValueExtractor
@@ -0,0 +1 @@
+org.sonar.server.v2.api.validation.UpdateFieldValueExtractor
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
index ed6a8bd2b55..3c6efc91b4e 100644
--- 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
@@ -33,13 +33,15 @@ 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.UserInformation;
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.user.UpdateUser;
import org.sonar.server.v2.api.ControllerTester;
+import org.sonar.server.v2.api.model.RestError;
import org.sonar.server.v2.api.response.PageRestResponse;
import org.sonar.server.v2.api.user.converter.UsersSearchRestResponseGenerator;
import org.sonar.server.v2.api.user.model.RestUser;
@@ -58,11 +60,13 @@ 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.JSON_MERGE_PATCH_CONTENT_TYPE;
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.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -75,7 +79,7 @@ public class DefaultUserControllerTest {
private final UsersSearchRestResponseGenerator responseGenerator = mock(UsersSearchRestResponseGenerator.class);
private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultUserController(userSession, userService, responseGenerator));
- private static final Gson gson = new GsonBuilder().registerTypeAdapter(RestUser.class, new RestUserDeserializer()).create();
+ private static final Gson gson = new GsonBuilder().registerTypeAdapter(RestUser.class, new RestUserDeserializer()).create();
@Test
public void search_whenNoParameters_shouldUseDefaultAndForwardToUserService() throws Exception {
@@ -118,25 +122,25 @@ public class DefaultUserControllerTest {
@Test
public void search_whenAdminParametersUsedButNotAdmin_shouldFail() throws Exception {
mockMvc.perform(get(USER_ENDPOINT)
- .param("sonarQubeLastConnectionDateFrom", "2020-01-01T00:00:00+0100"))
+ .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"))
+ .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"))
+ .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"))
+ .param("sonarLintLastConnectionDateTo", "2020-01-01T00:00:00+0100"))
.andExpectAll(
status().isForbidden(),
content().string("{\"message\":\"parameter sonarLintLastConnectionDateTo requires Administer System permission.\"}"));
@@ -144,12 +148,12 @@ public class DefaultUserControllerTest {
@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);
- SearchResults<UserSearchResult> searchResult = new SearchResults<>(users, users.size());
+ UserInformation user1 = generateUserSearchResult("user1", true, true, false, 2, 3);
+ UserInformation user2 = generateUserSearchResult("user2", true, false, false, 3, 0);
+ UserInformation user3 = generateUserSearchResult("user3", true, false, true, 1, 1);
+ UserInformation user4 = generateUserSearchResult("user4", false, true, false, 0, 0);
+ List<UserInformation> users = List.of(user1, user2, user3, user4);
+ SearchResults<UserInformation> searchResult = new SearchResults<>(users, users.size());
when(userService.findUsers(any())).thenReturn(searchResult);
List<RestUser> restUserForAdmins = List.of(toRestUser(user1), toRestUser(user2), toRestUser(user3), toRestUser(user4));
when(responseGenerator.toUsersForResponse(eq(searchResult.searchResults()), any())).thenReturn(new UsersSearchRestResponse(restUserForAdmins, new PageRestResponse(1, 50, 4)));
@@ -179,7 +183,7 @@ public class DefaultUserControllerTest {
}
}
- private UserSearchResult generateUserSearchResult(String id, boolean active, boolean local, boolean managed, int groupsCount, int tokensCount) {
+ private UserInformation generateUserSearchResult(String id, boolean active, boolean local, boolean managed, int groupsCount, int tokensCount) {
UserDto userDto = new UserDto()
.setLogin("login_" + id)
.setUuid("uuid_" + id)
@@ -196,24 +200,24 @@ public class DefaultUserControllerTest {
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);
+ return new UserInformation(userDto, managed, Optional.of("avatar_" + id), groups, tokensCount);
}
- private RestUserForAdmins toRestUser(UserSearchResult userSearchResult) {
+ private RestUserForAdmins toRestUser(UserInformation userInformation) {
return new RestUserForAdmins(
- 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.userDto().getSortedScmAccounts());
+ userInformation.userDto().getLogin(),
+ userInformation.userDto().getLogin(),
+ userInformation.userDto().getName(),
+ userInformation.userDto().getEmail(),
+ userInformation.userDto().isActive(),
+ userInformation.userDto().isLocal(),
+ userInformation.managed(),
+ userInformation.userDto().getExternalLogin(),
+ userInformation.userDto().getExternalIdentityProvider(),
+ userInformation.avatar().orElse(""),
+ formatDateTime(userInformation.userDto().getLastConnectionDate()),
+ formatDateTime(userInformation.userDto().getLastSonarlintConnectionDate()),
+ userInformation.userDto().getSortedScmAccounts());
}
@Test
@@ -310,7 +314,7 @@ public class DefaultUserControllerTest {
@Test
public void fetchUser_whenUserExists_shouldReturnUser() throws Exception {
- UserSearchResult user = generateUserSearchResult("user1", true, true, false, 2, 3);
+ UserInformation user = generateUserSearchResult("user1", true, true, false, 2, 3);
RestUserForAdmins restUserForAdmins = toRestUser(user);
when(userService.fetchUser("userLogin")).thenReturn(user);
when(responseGenerator.toRestUser(user)).thenReturn(restUserForAdmins);
@@ -326,9 +330,9 @@ public class DefaultUserControllerTest {
userSession.logIn().setNonSystemAdministrator();
mockMvc.perform(
- post(USER_ENDPOINT)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(gson.toJson(new UserCreateRestRequest(null, null, "login", "name", null, null))))
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, "login", "name", null, null))))
.andExpectAll(
status().isForbidden(),
content().json("{\"message\":\"Insufficient privileges\"}"));
@@ -339,12 +343,12 @@ public class DefaultUserControllerTest {
userSession.logIn().setSystemAdministrator();
mockMvc.perform(
- post(USER_ENDPOINT)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(gson.toJson(new UserCreateRestRequest(null, null, null, "name", null, null))))
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, null, "name", null, null))))
.andExpectAll(
status().isBadRequest(),
- content().json("{\"message\":\"Value {} for field login was rejected. Error: must not be null\"}"));
+ content().json("{\"message\":\"Value {} for field login was rejected. Error: must not be null.\"}"));
}
@Test
@@ -352,12 +356,12 @@ public class DefaultUserControllerTest {
userSession.logIn().setSystemAdministrator();
mockMvc.perform(
- post(USER_ENDPOINT)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(gson.toJson(new UserCreateRestRequest(null, null, "login", null, null, null))))
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(null, null, "login", null, null, null))))
.andExpectAll(
status().isBadRequest(),
- content().json("{\"message\":\"Value {} for field name was rejected. Error: must not be null\"}"));
+ content().json("{\"message\":\"Value {} for field name was rejected. Error: must not be null.\"}"));
}
@Test
@@ -366,9 +370,9 @@ public class DefaultUserControllerTest {
when(userService.createUser(any())).thenThrow(new IllegalArgumentException("IllegalArgumentException"));
mockMvc.perform(
- post(USER_ENDPOINT)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(gson.toJson(new UserCreateRestRequest("e@mail.com", true, "login", "name", "password", List.of("scm")))))
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest("e@mail.com", true, "login", "name", "password", List.of("scm")))))
.andExpectAll(
status().isBadRequest(),
content().json("{\"message\":\"IllegalArgumentException\"}"));
@@ -377,20 +381,123 @@ public class DefaultUserControllerTest {
@Test
public void create_whenUserServiceReturnUser_shouldReturnIt() throws Exception {
userSession.logIn().setSystemAdministrator();
- UserSearchResult userSearchResult = generateUserSearchResult("1", true, true, false, 1, 2);
- UserDto userDto = userSearchResult.userDto();
- when(userService.createUser(any())).thenReturn(userSearchResult);
- when(responseGenerator.toRestUser(userSearchResult)).thenReturn(toRestUser(userSearchResult));
+ UserInformation userInformation = generateUserSearchResult("1", true, true, false, 1, 2);
+ UserDto userDto = userInformation.userDto();
+ when(userService.createUser(any())).thenReturn(userInformation);
+ when(responseGenerator.toRestUser(userInformation)).thenReturn(toRestUser(userInformation));
MvcResult mvcResult = mockMvc.perform(
- post(USER_ENDPOINT)
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(gson.toJson(new UserCreateRestRequest(
- userDto.getEmail(), userDto.isLocal(), userDto.getLogin(), userDto.getName(), "password", userDto.getSortedScmAccounts()))))
+ post(USER_ENDPOINT)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(gson.toJson(new UserCreateRestRequest(
+ userDto.getEmail(), userDto.isLocal(), userDto.getLogin(), userDto.getName(), "password", userDto.getSortedScmAccounts()))))
.andExpect(status().isOk())
.andReturn();
RestUserForAdmins responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUserForAdmins.class);
- assertThat(responseUser).isEqualTo(toRestUser(userSearchResult));
+ assertThat(responseUser).isEqualTo(toRestUser(userInformation));
+ }
+
+ @Test
+ public void updateUser_whenUserDoesntExist_shouldReturnNotFound() throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ when(userService.updateUser(eq("userLogin"), any(UpdateUser.class))).thenThrow(new NotFoundException("Not found"));
+ mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content("{}"))
+ .andExpectAll(
+ status().isNotFound(),
+ content().json("{\"message\":\"Not found\"}"));
+ }
+
+ @Test
+ public void updateUser_whenCallerIsNotAdmin_shouldReturnForbidden() throws Exception {
+ userSession.logIn().setNonSystemAdministrator();
+
+ mockMvc.perform(
+ patch(USER_ENDPOINT + "/userLogin")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content("{}"))
+ .andExpectAll(
+ status().isForbidden(),
+ content().json("{\"message\":\"Insufficient privileges\"}"));
+ }
+
+ @Test
+ public void updateUser_whenEmailIsProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
+ UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"email\":\"newemail@example.com\"}");
+ assertThat(userUpdate.email()).isEqualTo("newemail@example.com");
+ assertThat(userUpdate.name()).isNull();
+ assertThat(userUpdate.scmAccounts()).isNull();
+ }
+
+ private UpdateUser performPatchCallAndVerifyResponse(String payload) throws Exception {
+ userSession.logIn().setSystemAdministrator();
+ UserInformation userInformation = generateUserSearchResult("1", true, true, false, 1, 2);
+
+ when(userService.updateUser(eq("userLogin"), any())).thenReturn(userInformation);
+ when(responseGenerator.toRestUser(userInformation)).thenReturn(toRestUser(userInformation));
+
+ MvcResult mvcResult = mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content(payload))
+ .andExpect(
+ status().isOk())
+ .andReturn();
+
+ RestUserForAdmins responseUser = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestUserForAdmins.class);
+ assertThat(responseUser).isEqualTo(toRestUser(userInformation));
+
+ ArgumentCaptor<UpdateUser> updateUserCaptor = ArgumentCaptor.forClass(UpdateUser.class);
+ verify(userService).updateUser(eq("userLogin"), updateUserCaptor.capture());
+ return updateUserCaptor.getValue();
+ }
+
+ @Test
+ public void updateUser_whenNameIsProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
+ UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"name\":\"new name\"}");
+ assertThat(userUpdate.email()).isNull();
+ assertThat(userUpdate.name()).isEqualTo("new name");
+ assertThat(userUpdate.scmAccounts()).isNull();
+ }
+
+ @Test
+ public void updateUser_whenScmAccountsAreProvided_shouldUpdateUserAndReturnUpdatedValue() throws Exception {
+ UpdateUser userUpdate = performPatchCallAndVerifyResponse("{\"scmAccounts\":[\"account1\",\"account2\"]}");
+ assertThat(userUpdate.email()).isNull();
+ assertThat(userUpdate.name()).isNull();
+ assertThat(userUpdate.scmAccounts()).containsExactly("account1", "account2");
+ }
+
+ @Test
+ public void updateUser_whenEmailIsInvalid_shouldReturnBadRequest() throws Exception {
+ performPatchCallAndExpectBadRequest("{\"email\":\"notavalidemail\"}", "Value notavalidemail for field email was rejected. Error: must be a well-formed email address.");
+ }
+
+ private void performPatchCallAndExpectBadRequest(String payload, String expectedMessage) throws Exception {
+ userSession.logIn().setSystemAdministrator();
+
+ MvcResult mvcResult = mockMvc.perform(patch(USER_ENDPOINT + "/userLogin")
+ .contentType(JSON_MERGE_PATCH_CONTENT_TYPE)
+ .content(payload))
+ .andExpect(
+ status().isBadRequest())
+ .andReturn();
+
+ RestError error = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestError.class);
+ assertThat(error.message()).isEqualTo(expectedMessage);
+ }
+
+ @Test
+ public void updateUser_whenEmailIsEmpty_shouldReturnBadRequest() throws Exception {
+ performPatchCallAndExpectBadRequest("{\"email\":\"\"}", "Value for field email was rejected. Error: size must be between 1 and 100.");
+ }
+
+ @Test
+ public void updateUser_whenNameIsTooLong_shouldReturnBadRequest() throws Exception {
+ String tooLong = "toolong".repeat(30);
+ String payload = "{\"name\":\"" + tooLong + "\"}";
+ String message = "Value " + tooLong + " for field name was rejected. Error: size must be between 0 and 200.";
+ performPatchCallAndExpectBadRequest(payload, message);
}
}
diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
index d1f40524fd9..fbbaa1d11f9 100644
--- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
+++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/user/converter/UsersSearchRestResponseGeneratorTest.java
@@ -30,7 +30,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.utils.DateUtils;
import org.sonar.db.user.UserDto;
import org.sonar.server.common.PaginationInformation;
-import org.sonar.server.common.user.service.UserSearchResult;
+import org.sonar.server.common.user.service.UserInformation;
import org.sonar.server.user.UserSession;
import org.sonar.server.v2.api.response.PageRestResponse;
import org.sonar.server.v2.api.user.model.RestUserForAdmins;
@@ -70,19 +70,19 @@ public class UsersSearchRestResponseGeneratorTest {
PaginationInformation paging = forPageIndex(1).withPageSize(2).andTotal(3);
- UserSearchResult userSearchResult1 = mockSearchResult(1, true);
- UserSearchResult userSearchResult2 = mockSearchResult(2, false);
+ UserInformation userInformation1 = mockSearchResult(1, true);
+ UserInformation userInformation2 = mockSearchResult(2, false);
- UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging);
+ UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userInformation1, userInformation2), paging);
- RestUserForAdmins expectUser1 = buildExpectedResponseForAdmin(userSearchResult1);
- RestUserForAdmins expectUser2 = buildExpectedResponseForAdmin(userSearchResult2);
+ RestUserForAdmins expectUser1 = buildExpectedResponseForAdmin(userInformation1);
+ RestUserForAdmins expectUser2 = buildExpectedResponseForAdmin(userInformation2);
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2);
assertPaginationInformationAreCorrect(paging, usersForResponse.page());
}
- private static RestUserForAdmins buildExpectedResponseForAdmin(UserSearchResult userSearchResult) {
- UserDto userDto = userSearchResult.userDto();
+ private static RestUserForAdmins buildExpectedResponseForAdmin(UserInformation userInformation) {
+ UserDto userDto = userInformation.userDto();
return new RestUserForAdmins(
userDto.getLogin(),
userDto.getLogin(),
@@ -90,13 +90,13 @@ public class UsersSearchRestResponseGeneratorTest {
userDto.getEmail(),
userDto.isActive(),
userDto.isLocal(),
- userSearchResult.managed(),
+ userInformation.managed(),
userDto.getExternalLogin(),
userDto.getExternalIdentityProvider(),
- userSearchResult.avatar().orElse(null),
+ userInformation.avatar().orElse(null),
toDateTime(userDto.getLastConnectionDate()),
toDateTime(userDto.getLastSonarlintConnectionDate()),
- userSearchResult.userDto().getSortedScmAccounts()
+ userInformation.userDto().getSortedScmAccounts()
);
}
@@ -106,19 +106,19 @@ public class UsersSearchRestResponseGeneratorTest {
PaginationInformation paging = forPageIndex(1).withPageSize(2).andTotal(3);
- UserSearchResult userSearchResult1 = mockSearchResult(1, true);
- UserSearchResult userSearchResult2 = mockSearchResult(2, false);
+ UserInformation userInformation1 = mockSearchResult(1, true);
+ UserInformation userInformation2 = mockSearchResult(2, false);
- UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging);
+ UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userInformation1, userInformation2), paging);
- RestUserForLoggedInUsers expectUser1 = buildExpectedResponseForUser(userSearchResult1);
- RestUserForLoggedInUsers expectUser2 = buildExpectedResponseForUser(userSearchResult2);
+ RestUserForLoggedInUsers expectUser1 = buildExpectedResponseForUser(userInformation1);
+ RestUserForLoggedInUsers expectUser2 = buildExpectedResponseForUser(userInformation2);
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2);
assertPaginationInformationAreCorrect(paging, usersForResponse.page());
}
- private static RestUserForLoggedInUsers buildExpectedResponseForUser(UserSearchResult userSearchResult) {
- UserDto userDto = userSearchResult.userDto();
+ private static RestUserForLoggedInUsers buildExpectedResponseForUser(UserInformation userInformation) {
+ UserDto userDto = userInformation.userDto();
return new RestUserForLoggedInUsers(
userDto.getLogin(),
userDto.getLogin(),
@@ -127,7 +127,7 @@ public class UsersSearchRestResponseGeneratorTest {
userDto.isActive(),
userDto.isLocal(),
userDto.getExternalIdentityProvider(),
- userSearchResult.avatar().orElse(null)
+ userInformation.avatar().orElse(null)
);
}
@@ -135,19 +135,19 @@ public class UsersSearchRestResponseGeneratorTest {
public void toUsersForResponse_whenAnonymous_returnsOnlyNameAndLogin() {
PaginationInformation paging = forPageIndex(1).withPageSize(2).andTotal(3);
- UserSearchResult userSearchResult1 = mockSearchResult(1, true);
- UserSearchResult userSearchResult2 = mockSearchResult(2, false);
+ UserInformation userInformation1 = mockSearchResult(1, true);
+ UserInformation userInformation2 = mockSearchResult(2, false);
- UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userSearchResult1, userSearchResult2), paging);
+ UsersSearchRestResponse usersForResponse = usersSearchRestResponseGenerator.toUsersForResponse(List.of(userInformation1, userInformation2), paging);
- RestUserForAnonymousUsers expectUser1 = buildExpectedResponseForAnonymous(userSearchResult1);
- RestUserForAnonymousUsers expectUser2 = buildExpectedResponseForAnonymous(userSearchResult2);
+ RestUserForAnonymousUsers expectUser1 = buildExpectedResponseForAnonymous(userInformation1);
+ RestUserForAnonymousUsers expectUser2 = buildExpectedResponseForAnonymous(userInformation2);
assertThat(usersForResponse.users()).containsExactly(expectUser1, expectUser2);
assertPaginationInformationAreCorrect(paging, usersForResponse.page());
}
- private static RestUserForAnonymousUsers buildExpectedResponseForAnonymous(UserSearchResult userSearchResult) {
- UserDto userDto = userSearchResult.userDto();
+ private static RestUserForAnonymousUsers buildExpectedResponseForAnonymous(UserInformation userInformation) {
+ UserDto userDto = userInformation.userDto();
return new RestUserForAnonymousUsers(
userDto.getLogin(),
userDto.getLogin(),
@@ -159,8 +159,8 @@ public class UsersSearchRestResponseGeneratorTest {
return Optional.ofNullable(dateTimeMs).map(DateUtils::formatDateTime).orElse(null);
}
- private static UserSearchResult mockSearchResult(int i, boolean booleanFlagsValue) {
- UserSearchResult userSearchResult = mock(UserSearchResult.class, RETURNS_DEEP_STUBS);
+ private static UserInformation mockSearchResult(int i, boolean booleanFlagsValue) {
+ UserInformation userInformation = mock(UserInformation.class, RETURNS_DEEP_STUBS);
UserDto user1 = new UserDto()
.setUuid("uuid_" + i)
.setLogin("login_" + i)
@@ -174,9 +174,9 @@ public class UsersSearchRestResponseGeneratorTest {
.setLocal(booleanFlagsValue)
.setActive(booleanFlagsValue);
- when(userSearchResult.userDto()).thenReturn(user1);
- when(userSearchResult.managed()).thenReturn(booleanFlagsValue);
- return userSearchResult;
+ when(userInformation.userDto()).thenReturn(user1);
+ when(userInformation.managed()).thenReturn(booleanFlagsValue);
+ return userInformation;
}
private static void assertPaginationInformationAreCorrect(PaginationInformation paginationInformation, PageRestResponse pageRestResponse) {