From a74d02c3444ec5b957b4c42f1f4d0227bc2dfc03 Mon Sep 17 00:00:00 2001 From: Heng Qin Date: Fri, 29 Mar 2024 14:20:06 +0800 Subject: [PATCH] [#2238] feat(server): Add the operations for the user This reverts commit d6cef11cb8966869405303a88b1ce3e8751e940c. --- .../gravitino/dto/authorization/UserDTO.java | 140 +++++++++ .../dto/requests/UserAddRequest.java | 53 ++++ .../gravitino/dto/responses/UserResponse.java | 54 ++++ .../gravitino/dto/util/DTOConverters.java | 20 ++ .../dto/responses/TestResponses.java | 16 + .../gravitino/server/GravitinoServer.java | 2 + .../server/web/rest/ExceptionHandlers.java | 38 +++ .../server/web/rest/UserOperations.java | 123 ++++++++ .../server/web/rest/TestUserOperations.java | 280 ++++++++++++++++++ 9 files changed, 726 insertions(+) create mode 100644 common/src/main/java/com/datastrato/gravitino/dto/authorization/UserDTO.java create mode 100644 common/src/main/java/com/datastrato/gravitino/dto/requests/UserAddRequest.java create mode 100644 common/src/main/java/com/datastrato/gravitino/dto/responses/UserResponse.java create mode 100644 server/src/main/java/com/datastrato/gravitino/server/web/rest/UserOperations.java create mode 100644 server/src/test/java/com/datastrato/gravitino/server/web/rest/TestUserOperations.java diff --git a/common/src/main/java/com/datastrato/gravitino/dto/authorization/UserDTO.java b/common/src/main/java/com/datastrato/gravitino/dto/authorization/UserDTO.java new file mode 100644 index 00000000000..135535c6784 --- /dev/null +++ b/common/src/main/java/com/datastrato/gravitino/dto/authorization/UserDTO.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ +package com.datastrato.gravitino.dto.authorization; + +import com.datastrato.gravitino.Audit; +import com.datastrato.gravitino.authorization.User; +import com.datastrato.gravitino.dto.AuditDTO; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; + +/** Represents a User Data Transfer Object (DTO). */ +public class UserDTO implements User { + + @JsonProperty("name") + private String name; + + + @JsonProperty("audit") + private AuditDTO audit; + + @Nullable + @JsonProperty("roles") + private List roles; + + /** Default constructor for Jackson deserialization. */ + protected UserDTO() {} + + /** + * Creates a new instance of UserDTO. + * + * @param name The name of the User DTO. + * @param audit The audit information of the User DTO. + */ + protected UserDTO(String name, AuditDTO audit) { + this.name = name; + this.audit = audit; + } + + /** @return The name of the User DTO. */ + @Override + public String name() { + return name; + } + + /** + * The roles of the user. A user can have multiple roles. Every role binds several privileges. + * + * @return The roles of the user. + */ + @Override + public List roles() { + return roles; + } + + /** @return The audit information of the User DTO. */ + @Override + public Audit auditInfo() { + return audit; + } + + /** + * Creates a new Builder for constructing an User DTO. + * + * @return A new Builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing a UserDTO instance. + * + * @param The type of the builder instance. + */ + public static class Builder { + + /** The name of the user. */ + protected String name; + + /** The roles of the user. */ + protected List roles; + + /** The audit information of the user. */ + protected AuditDTO audit; + + + /** + * Sets the name of the user. + * + * @param name The name of the user. + * @return The builder instance. + */ + public S withName(String name) { + this.name = name; + return (S) this; + } + + /** + * Sets the properties of the user. + * + * @param roles The roles of the user. + * @return The builder instance. + */ + public S withRoles(List roles) { + this.roles = roles; + return (S) this; + } + + /** + * Sets the audit information of the user. + * + * @param audit The audit information of the user. + * @return The builder instance. + */ + public S withAudit(AuditDTO audit) { + this.audit = audit; + return (S) this; + } + + /** + * Builds an instance of UserDTO using the builder's properties. + * + * @return An instance of UserDTO. + * @throws IllegalArgumentException If the name or audit are not set. + */ + public UserDTO build() { + Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be null or empty"); + Preconditions.checkArgument(audit != null, "audit cannot be null"); + return new UserDTO(name, audit); + } + } +} diff --git a/common/src/main/java/com/datastrato/gravitino/dto/requests/UserAddRequest.java b/common/src/main/java/com/datastrato/gravitino/dto/requests/UserAddRequest.java new file mode 100644 index 00000000000..173c66a324c --- /dev/null +++ b/common/src/main/java/com/datastrato/gravitino/dto/requests/UserAddRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ +package com.datastrato.gravitino.dto.requests; + +import com.datastrato.gravitino.rest.RESTRequest; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.jackson.Jacksonized; +import org.apache.commons.lang3.StringUtils; + +/** Represents a request to create a user. */ +@Getter +@EqualsAndHashCode +@ToString +@Builder +@Jacksonized +public class UserAddRequest implements RESTRequest { + + @JsonProperty("name") + private final String name; + + /** Default constructor for MetalakeCreateRequest. (Used for Jackson deserialization.) */ + public UserAddRequest() { + this(null); + } + + /** + * Creates a new UserCreateRequest. + * + * @param name The name of the user. + */ + public UserAddRequest(String name) { + super(); + this.name = name; + } + + /** + * Validates the {@link UserAddRequest} request. + * + * @throws IllegalArgumentException If the request is invalid, this exception is thrown. + */ + @Override + public void validate() throws IllegalArgumentException { + Preconditions.checkArgument( + StringUtils.isNotBlank(name), "\"name\" field is required and cannot be empty"); + } +} diff --git a/common/src/main/java/com/datastrato/gravitino/dto/responses/UserResponse.java b/common/src/main/java/com/datastrato/gravitino/dto/responses/UserResponse.java new file mode 100644 index 00000000000..277be31d6a9 --- /dev/null +++ b/common/src/main/java/com/datastrato/gravitino/dto/responses/UserResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ +package com.datastrato.gravitino.dto.responses; + +import com.datastrato.gravitino.dto.authorization.UserDTO; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.apache.commons.lang3.StringUtils; + +/** Represents a response for a user. */ +@Getter +@ToString +@EqualsAndHashCode(callSuper = true) +public class UserResponse extends BaseResponse { + + @JsonProperty("user") + private final UserDTO user; + + /** + * Constructor for UserResponse. + * + * @param user The user data transfer object. + */ + public UserResponse(UserDTO user) { + super(0); + this.user = user; + } + + /** Default constructor for UserResponse. (Used for Jackson deserialization.) */ + public UserResponse() { + super(); + this.user = null; + } + + /** + * Validates the response data. + * + * @throws IllegalArgumentException if the name or audit is not set. + */ + @Override + public void validate() throws IllegalArgumentException { + super.validate(); + + Preconditions.checkArgument(user != null, "user must not be null"); + Preconditions.checkArgument( + StringUtils.isNotBlank(user.name()), "user 'name' must not be null and empty"); + Preconditions.checkArgument(user.auditInfo() != null, "user 'auditInfo' must not be null"); + } +} diff --git a/common/src/main/java/com/datastrato/gravitino/dto/util/DTOConverters.java b/common/src/main/java/com/datastrato/gravitino/dto/util/DTOConverters.java index e06c3c17bc2..049c86bf0b1 100644 --- a/common/src/main/java/com/datastrato/gravitino/dto/util/DTOConverters.java +++ b/common/src/main/java/com/datastrato/gravitino/dto/util/DTOConverters.java @@ -9,9 +9,11 @@ import com.datastrato.gravitino.Audit; import com.datastrato.gravitino.Catalog; import com.datastrato.gravitino.Metalake; +import com.datastrato.gravitino.authorization.User; import com.datastrato.gravitino.dto.AuditDTO; import com.datastrato.gravitino.dto.CatalogDTO; import com.datastrato.gravitino.dto.MetalakeDTO; +import com.datastrato.gravitino.dto.authorization.UserDTO; import com.datastrato.gravitino.dto.file.FilesetDTO; import com.datastrato.gravitino.dto.rel.ColumnDTO; import com.datastrato.gravitino.dto.rel.DistributionDTO; @@ -327,6 +329,24 @@ public static IndexDTO toDTO(Index index) { .build(); } + /** + * Converts a user implementation to a UserDTO. + * + * @param user The user implementation. + * @return The user DTO. + */ + public static UserDTO toDTO(User user) { + if (user instanceof UserDTO) { + return (UserDTO) user; + } + + return UserDTO.builder() + .withName(user.name()) + .withRoles(user.roles()) + .withAudit(toDTO(user.auditInfo())) + .build(); + } + /** * Converts a Expression to an FunctionArg DTO. * diff --git a/common/src/test/java/com/datastrato/gravitino/dto/responses/TestResponses.java b/common/src/test/java/com/datastrato/gravitino/dto/responses/TestResponses.java index efeb57c11c7..ae2b109dd59 100644 --- a/common/src/test/java/com/datastrato/gravitino/dto/responses/TestResponses.java +++ b/common/src/test/java/com/datastrato/gravitino/dto/responses/TestResponses.java @@ -14,6 +14,7 @@ import com.datastrato.gravitino.dto.AuditDTO; import com.datastrato.gravitino.dto.CatalogDTO; import com.datastrato.gravitino.dto.MetalakeDTO; +import com.datastrato.gravitino.dto.authorization.UserDTO; import com.datastrato.gravitino.dto.rel.ColumnDTO; import com.datastrato.gravitino.dto.rel.SchemaDTO; import com.datastrato.gravitino.dto.rel.TableDTO; @@ -223,4 +224,19 @@ void testOAuthErrorException() throws IllegalArgumentException { OAuth2ErrorResponse response = new OAuth2ErrorResponse(); assertThrows(IllegalArgumentException.class, () -> response.validate()); } + + @Test + void testUserResponse() throws IllegalArgumentException { + AuditDTO audit = + new AuditDTO.Builder().withCreator("creator").withCreateTime(Instant.now()).build(); + UserDTO user = UserDTO.builder().withName("user1").withAudit(audit).build(); + UserResponse response = new UserResponse(user); + response.validate(); // No exception thrown + } + + @Test + void testUserResponseException() throws IllegalArgumentException { + UserResponse user = new UserResponse(); + assertThrows(IllegalArgumentException.class, () -> user.validate()); + } } diff --git a/server/src/main/java/com/datastrato/gravitino/server/GravitinoServer.java b/server/src/main/java/com/datastrato/gravitino/server/GravitinoServer.java index 1651f493f6a..7216fc68d6c 100644 --- a/server/src/main/java/com/datastrato/gravitino/server/GravitinoServer.java +++ b/server/src/main/java/com/datastrato/gravitino/server/GravitinoServer.java @@ -5,6 +5,7 @@ package com.datastrato.gravitino.server; import com.datastrato.gravitino.GravitinoEnv; +import com.datastrato.gravitino.authorization.AccessControlManager; import com.datastrato.gravitino.catalog.CatalogManager; import com.datastrato.gravitino.catalog.CatalogOperationDispatcher; import com.datastrato.gravitino.metalake.MetalakeManager; @@ -73,6 +74,7 @@ private void initializeRestApi() { protected void configure() { bind(gravitinoEnv.metalakesManager()).to(MetalakeManager.class).ranked(1); bind(gravitinoEnv.catalogManager()).to(CatalogManager.class).ranked(1); + bind(gravitinoEnv.accessControlManager()).to(AccessControlManager.class).ranked(1); bind(gravitinoEnv.catalogOperationDispatcher()) .to(CatalogOperationDispatcher.class) .ranked(1); diff --git a/server/src/main/java/com/datastrato/gravitino/server/web/rest/ExceptionHandlers.java b/server/src/main/java/com/datastrato/gravitino/server/web/rest/ExceptionHandlers.java index 7f4cb41c6e9..8af5693608e 100644 --- a/server/src/main/java/com/datastrato/gravitino/server/web/rest/ExceptionHandlers.java +++ b/server/src/main/java/com/datastrato/gravitino/server/web/rest/ExceptionHandlers.java @@ -13,6 +13,7 @@ import com.datastrato.gravitino.exceptions.PartitionAlreadyExistsException; import com.datastrato.gravitino.exceptions.SchemaAlreadyExistsException; import com.datastrato.gravitino.exceptions.TableAlreadyExistsException; +import com.datastrato.gravitino.exceptions.UserAlreadyExistsException; import com.datastrato.gravitino.server.web.Utils; import com.google.common.annotations.VisibleForTesting; import javax.ws.rs.core.Response; @@ -55,6 +56,11 @@ public static Response handleFilesetException( return FilesetExceptionHandler.INSTANCE.handle(op, fileset, schema, e); } + public static Response handleUserException( + OperationType op, String user, String metalake, Exception e) { + return UserExceptionHandler.INSTANCE.handle(op, user, metalake, e); + } + private static class PartitionExceptionHandler extends BaseExceptionHandler { private static final ExceptionHandler INSTANCE = new PartitionExceptionHandler(); @@ -254,6 +260,38 @@ public Response handle(OperationType op, String fileset, String schema, Exceptio } } + private static class UserExceptionHandler extends BaseExceptionHandler { + + private static final ExceptionHandler INSTANCE = new UserExceptionHandler(); + + private static String getUserErrorMsg( + String fileset, String operation, String metalake, String reason) { + return String.format( + "Failed to operate user %s operation [%s] under metalake [%s], reason [%s]", + fileset, operation, metalake, reason); + } + + @Override + public Response handle(OperationType op, String user, String metalake, Exception e) { + String formatted = StringUtil.isBlank(user) ? "" : " [" + user + "]"; + String errorMsg = getUserErrorMsg(formatted, op.name(), metalake, getErrorMsg(e)); + LOG.warn(errorMsg, e); + + if (e instanceof IllegalArgumentException) { + return Utils.illegalArguments(errorMsg, e); + + } else if (e instanceof NotFoundException) { + return Utils.notFound(errorMsg, e); + + } else if (e instanceof UserAlreadyExistsException) { + return Utils.alreadyExists(errorMsg, e); + + } else { + return super.handle(op, user, metalake, e); + } + } + } + @VisibleForTesting static class BaseExceptionHandler extends ExceptionHandler { diff --git a/server/src/main/java/com/datastrato/gravitino/server/web/rest/UserOperations.java b/server/src/main/java/com/datastrato/gravitino/server/web/rest/UserOperations.java new file mode 100644 index 00000000000..612a3b35a05 --- /dev/null +++ b/server/src/main/java/com/datastrato/gravitino/server/web/rest/UserOperations.java @@ -0,0 +1,123 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ +package com.datastrato.gravitino.server.web.rest; + +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import com.datastrato.gravitino.NameIdentifier; +import com.datastrato.gravitino.authorization.AccessControlManager; +import com.datastrato.gravitino.dto.requests.UserAddRequest; +import com.datastrato.gravitino.dto.responses.DropResponse; +import com.datastrato.gravitino.dto.responses.UserResponse; +import com.datastrato.gravitino.dto.util.DTOConverters; +import com.datastrato.gravitino.lock.LockType; +import com.datastrato.gravitino.lock.TreeLockUtils; +import com.datastrato.gravitino.meta.CatalogEntity; +import com.datastrato.gravitino.meta.UserEntity; +import com.datastrato.gravitino.metrics.MetricNames; +import com.datastrato.gravitino.server.web.Utils; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("/metalakes/{metalake}/users") +public class UserOperations { + + private static final Logger LOG = LoggerFactory.getLogger(UserOperations.class); + + private final AccessControlManager accessControlManager; + + @Context private HttpServletRequest httpRequest; + + @Inject + public UserOperations(AccessControlManager accessControlManager) { + this.accessControlManager = accessControlManager; + } + + @GET + @Path("{user}") + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "get-user." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "get-user", absolute = true) + public Response getUser(@PathParam("metalake") String metalake, @PathParam("user") String user) { + try { + NameIdentifier ident = ofUser(metalake, user); + return Utils.doAs( + httpRequest, + () -> + Utils.ok( + new UserResponse( + DTOConverters.toDTO( + TreeLockUtils.doWithTreeLock( + ident, + LockType.READ, + () -> accessControlManager.getUser(metalake, user)))))); + } catch (Exception e) { + return ExceptionHandlers.handleUserException(OperationType.LOAD, user, metalake, e); + } + } + + @POST + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "add-user." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "add-user", absolute = true) + public Response addUser(@PathParam("metalake") String metalake, UserAddRequest request) { + try { + NameIdentifier ident = ofUser(metalake, request.getName()); + return Utils.doAs( + httpRequest, + () -> + Utils.ok( + new UserResponse( + DTOConverters.toDTO( + TreeLockUtils.doWithTreeLock( + ident, + LockType.WRITE, + () -> + accessControlManager.addUser( + metalake, request.getName())))))); + } catch (Exception e) { + return ExceptionHandlers.handleUserException( + OperationType.CREATE, request.getName(), metalake, e); + } + } + + @DELETE + @Path("{user}") + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "remove-user." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "remove-user", absolute = true) + public Response removeUser(@PathParam("metalake") String metalake, @PathParam("user") String user) { + try { + return Utils.doAs( + httpRequest, + () -> { + NameIdentifier ident = ofUser(metalake, user); + boolean dropped = + TreeLockUtils.doWithTreeLock( + ident, LockType.WRITE, () -> accessControlManager.removeUser(metalake, user)); + if (!dropped) { + LOG.warn("Failed to drop table {} under metalkae {}", user, metalake); + } + return Utils.ok(new DropResponse(dropped)); + }); + } catch (Exception e) { + return ExceptionHandlers.handleUserException(OperationType.DROP, user, metalake, e); + } + } + + private NameIdentifier ofUser(String metalake, String user) { + return NameIdentifier.of(metalake, CatalogEntity.SYSTEM_CATALOG_RESERVED_NAME, UserEntity.USER_SCHEMA_NAME, user); + } +} diff --git a/server/src/test/java/com/datastrato/gravitino/server/web/rest/TestUserOperations.java b/server/src/test/java/com/datastrato/gravitino/server/web/rest/TestUserOperations.java new file mode 100644 index 00000000000..c1db8ace42c --- /dev/null +++ b/server/src/test/java/com/datastrato/gravitino/server/web/rest/TestUserOperations.java @@ -0,0 +1,280 @@ +/* + * Copyright 2024 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ +package com.datastrato.gravitino.server.web.rest; + +import static com.datastrato.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL; +import static com.datastrato.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY; +import static com.datastrato.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastrato.gravitino.Config; +import com.datastrato.gravitino.GravitinoEnv; +import com.datastrato.gravitino.authorization.AccessControlManager; +import com.datastrato.gravitino.authorization.User; +import com.datastrato.gravitino.dto.authorization.UserDTO; +import com.datastrato.gravitino.dto.requests.UserAddRequest; +import com.datastrato.gravitino.dto.responses.DropResponse; +import com.datastrato.gravitino.dto.responses.ErrorConstants; +import com.datastrato.gravitino.dto.responses.ErrorResponse; +import com.datastrato.gravitino.dto.responses.UserResponse; +import com.datastrato.gravitino.exceptions.NoSuchMetalakeException; +import com.datastrato.gravitino.exceptions.NoSuchUserException; +import com.datastrato.gravitino.exceptions.UserAlreadyExistsException; +import com.datastrato.gravitino.lock.LockManager; +import com.datastrato.gravitino.meta.AuditInfo; +import com.datastrato.gravitino.meta.UserEntity; +import com.datastrato.gravitino.rest.RESTUtils; +import java.io.IOException; +import java.time.Instant; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestUserOperations extends JerseyTest { + + private final AccessControlManager manager = mock(AccessControlManager.class); + + private static class MockServletRequestFactory extends ServletRequestFactoryBase { + @Override + public HttpServletRequest get() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteUser()).thenReturn(null); + return request; + } + } + + @BeforeAll + public static void setup() { + Config config = mock(Config.class); + Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY); + Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY); + Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL); + GravitinoEnv.getInstance().setLockManager(new LockManager(config)); + } + + @Override + protected Application configure() { + try { + forceSet( + TestProperties.CONTAINER_PORT, String.valueOf(RESTUtils.findAvailablePort(2000, 3000))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(UserOperations.class); + resourceConfig.register( + new AbstractBinder() { + @Override + protected void configure() { + bind(manager).to(AccessControlManager.class).ranked(2); + bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class); + } + }); + + return resourceConfig; + } + + @Test + public void testAddUser() { + UserAddRequest req = new UserAddRequest("user1"); + User user = buildUser("user1"); + + when(manager.addUser(any(), any())).thenReturn(user); + + Response resp = + target("/metalakes/metalake1/users") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); + + UserResponse userResponse = resp.readEntity(UserResponse.class); + Assertions.assertEquals(0, userResponse.getCode()); + + UserDTO userDTO = userResponse.getUser(); + Assertions.assertEquals("user1", userDTO.name()); + + // Test throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")) + .when(manager) + .addUser(any(), any()); + Response resp1 = + target("/metalakes/metalake1/users") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp1.getMediaType()); + + ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResponse.getCode()); + Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), errorResponse.getType()); + + // Test throw UserAlreadyExistsException + doThrow(new UserAlreadyExistsException("mock error")) + .when(manager) + .addUser(any(), any()); + Response resp2 = + target("/metalakes/metalake1/users") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), resp2.getStatus()); + + ErrorResponse errorResponse1 = resp2.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, errorResponse1.getCode()); + Assertions.assertEquals( + UserAlreadyExistsException.class.getSimpleName(), errorResponse1.getType()); + + // Test throw internal RuntimeException + doThrow(new RuntimeException("mock error")).when(manager).addUser(any(), any()); + Response resp3 = + target("/metalakes/metalake1/users") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + + ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); + } + + @Test + public void testGetUser() { + + User user = buildUser("user1"); + + when(manager.getUser(any(), any())).thenReturn(user); + + Response resp = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + + UserResponse userResponse = resp.readEntity(UserResponse.class); + Assertions.assertEquals(0, userResponse.getCode()); + UserDTO userDTO = userResponse.getUser(); + Assertions.assertEquals("user1", userDTO.name()); + + // Test throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")).when(manager).getUser(any(), any()); + Response resp1 = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + + ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResponse.getCode()); + Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), errorResponse.getType()); + + // Test throw NoSuchUserException + doThrow(new NoSuchUserException("mock error")).when(manager).getUser(any(), any()); + Response resp2 = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp2.getStatus()); + + ErrorResponse errorResponse1 = resp2.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResponse1.getCode()); + Assertions.assertEquals(NoSuchUserException.class.getSimpleName(), errorResponse1.getType()); + + // Test throw internal RuntimeException + doThrow(new RuntimeException("mock error")).when(manager).getUser(any(), any()); + Response resp3 = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + + ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); + } + + private User buildUser(String user) { + return UserEntity.builder() + .withId(1L) + .withName(user) + .withAuditInfo( + AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) + .build(); + } + + @Test + public void testDropUser() { + when(manager.removeUser(any(), any())).thenReturn(true); + + Response resp = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .delete(); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + DropResponse dropResponse = resp.readEntity(DropResponse.class); + Assertions.assertEquals(0, dropResponse.getCode()); + Assertions.assertTrue(dropResponse.dropped()); + + // Test when failed to drop user + when(manager.removeUser(any(), any())).thenReturn(false); + Response resp2 = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .delete(); + + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp2.getStatus()); + DropResponse dropResponse2 = resp2.readEntity(DropResponse.class); + Assertions.assertEquals(0, dropResponse2.getCode()); + Assertions.assertFalse(dropResponse2.dropped()); + + doThrow(new RuntimeException("mock error")).when(manager).removeUser(any(), any()); + Response resp3 = + target("/metalakes/metalake1/users/user1") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .delete(); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + + ErrorResponse errorResponse = resp3.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse.getCode()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse.getType()); + } +}