diff --git a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java index c708a94..0ed2cb9 100644 --- a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java @@ -19,4 +19,6 @@ public class ApiPathConstant { DEVELOPMENT_API_PREFIX + "/error"; public static final String USER_API_PREFIX = ALL_API_PREFIX + "/user"; + + public static final String USER_GET_BY_NAME_API_PATH = USER_API_PREFIX + "/{username}"; } diff --git a/src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java b/src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java index 9fcdc73..030a88c 100644 --- a/src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java +++ b/src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java @@ -2,4 +2,6 @@ public class HeaderParameter { public static final String TOKEN = "Token"; + public static final String ACCESS_TOKEN = "Access-Token"; + public static final String REFRESH_TOKEN = "Refresh-Token"; } diff --git a/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java b/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java index b6505c3..64e6d19 100644 --- a/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java +++ b/src/main/java/edu/cmipt/gcs/controller/AuthenticationController.java @@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -96,14 +97,20 @@ public void signUp(@Validated(CreateGroup.class) @RequestBody UserDTO user) { content = @Content(schema = @Schema(implementation = ErrorVO.class))), @ApiResponse(responseCode = "500", description = "Internal server error") }) - public ResponseEntity signIn(@Validated @RequestBody UserSignInDTO user) { + public ResponseEntity signIn(@Validated @RequestBody UserSignInDTO user) { QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("username", user.username()); wrapper.eq("user_password", MD5Converter.convertToMD5(user.userPassword())); if (!userService.exists(wrapper)) { throw new GenericException(ErrorCodeEnum.WRONG_SIGN_IN_INFORMATION); } - return ResponseEntity.ok(new UserVO(userService.getOne(wrapper))); + UserVO userVO = new UserVO(userService.getOne(wrapper)); + HttpHeaders headers = new HttpHeaders(); + headers.add(HeaderParameter.ACCESS_TOKEN, + JwtUtil.generateToken(userVO.id(), TokenTypeEnum.ACCESS_TOKEN)); + headers.add(HeaderParameter.REFRESH_TOKEN, + JwtUtil.generateToken(userVO.id(), TokenTypeEnum.REFRESH_TOKEN)); + return ResponseEntity.ok().headers(headers).body(userVO); } @DeleteMapping(ApiPathConstant.AUTHENTICATION_SIGN_OUT_API_PATH) @@ -134,7 +141,9 @@ public void signOut(@RequestBody List tokenList) { content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse(responseCode = "500", description = "Internal server error") }) - public String refreshToken(@RequestHeader(HeaderParameter.TOKEN) String token) { - return JwtUtil.generateToken(JwtUtil.getID(token), TokenTypeEnum.ACCESS_TOKEN); + public ResponseEntity refreshToken(@RequestHeader(HeaderParameter.TOKEN) String token) { + HttpHeaders headers = new HttpHeaders(); + headers.add(HeaderParameter.ACCESS_TOKEN, JwtUtil.generateToken(JwtUtil.getID(token), TokenTypeEnum.ACCESS_TOKEN)); + return ResponseEntity.ok().headers(headers).build(); } } diff --git a/src/main/java/edu/cmipt/gcs/controller/UserController.java b/src/main/java/edu/cmipt/gcs/controller/UserController.java index c8bd07e..1939c00 100644 --- a/src/main/java/edu/cmipt/gcs/controller/UserController.java +++ b/src/main/java/edu/cmipt/gcs/controller/UserController.java @@ -1,14 +1,49 @@ package edu.cmipt.gcs.controller; +import edu.cmipt.gcs.constant.ApiPathConstant; +import edu.cmipt.gcs.enumeration.ErrorCodeEnum; +import edu.cmipt.gcs.exception.GenericException; +import edu.cmipt.gcs.pojo.error.ErrorVO; +import edu.cmipt.gcs.pojo.user.UserPO; +import edu.cmipt.gcs.pojo.user.UserVO; import edu.cmipt.gcs.service.UserService; - +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + @RestController @Tag(name = "User", description = "User Related APIs") public class UserController { @Autowired private UserService userService; + + @GetMapping(ApiPathConstant.USER_GET_BY_NAME_API_PATH) + @Operation( + summary = "Get user by name", + description = "Get user information by user name", + tags = {"User", "Get Method"}) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "User information returned successfully"), + @ApiResponse( + responseCode = "404", + description = "User not found", + content = @Content(schema = @Schema(implementation = ErrorVO.class))) + }) + public UserVO getUserByName(@PathVariable("username") String username) { + QueryWrapper wrapper = new QueryWrapper(); + wrapper.eq("username", username); + if (!userService.exists(wrapper)) { + throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, username); + } + return new UserVO(userService.getOne(wrapper)); + } } diff --git a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java index 986e121..7e0c49d 100644 --- a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java +++ b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java @@ -23,7 +23,9 @@ public enum ErrorCodeEnum { INVALID_TOKEN("INVALID_TOKEN"), ACCESS_DENIED("ACCESS_DENIED"), - MESSAGE_CONVERSION_ERROR("MESSAGE_CONVERSION_ERROR"); + MESSAGE_CONVERSION_ERROR("MESSAGE_CONVERSION_ERROR"), + + USER_NOT_FOUND("USER_NOT_FOUND"); // code means the error code in the message.properties private String code; diff --git a/src/main/java/edu/cmipt/gcs/exception/GlobalExceptionHandler.java b/src/main/java/edu/cmipt/gcs/exception/GlobalExceptionHandler.java index f312107..8307e65 100644 --- a/src/main/java/edu/cmipt/gcs/exception/GlobalExceptionHandler.java +++ b/src/main/java/edu/cmipt/gcs/exception/GlobalExceptionHandler.java @@ -64,6 +64,9 @@ public ResponseEntity handleGenericException( case ACCESS_DENIED: return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(new ErrorVO(e.getCode(), e.getMessage())); + case USER_NOT_FOUND: + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorVO(e.getCode(), e.getMessage())); default: return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ErrorVO(e.getCode(), e.getMessage())); diff --git a/src/main/java/edu/cmipt/gcs/pojo/user/UserVO.java b/src/main/java/edu/cmipt/gcs/pojo/user/UserVO.java index 28502e5..02cf4da 100644 --- a/src/main/java/edu/cmipt/gcs/pojo/user/UserVO.java +++ b/src/main/java/edu/cmipt/gcs/pojo/user/UserVO.java @@ -1,23 +1,16 @@ package edu.cmipt.gcs.pojo.user; -import edu.cmipt.gcs.enumeration.TokenTypeEnum; -import edu.cmipt.gcs.util.JwtUtil; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "User Value Object") public record UserVO( @Schema(description = "User ID") Long id, @Schema(description = "Username", example = "admin") String username, - @Schema(description = "Email", example = "admin@cmipt.edu") String email, - @Schema(description = "Access Token") String accessToken, - @Schema(description = "Refresh Token") String refreshToken) { + @Schema(description = "Email", example = "admin@cmipt.edu") String email) { public UserVO(UserPO userPO) { this( userPO.getId(), userPO.getUsername(), - userPO.getEmail(), - JwtUtil.generateToken(userPO.getId(), TokenTypeEnum.ACCESS_TOKEN), - JwtUtil.generateToken(userPO.getId(), TokenTypeEnum.REFRESH_TOKEN)); + userPO.getEmail()); } } diff --git a/src/main/resources/message/message.properties b/src/main/resources/message/message.properties index 2f8bc0b..59f253d 100644 --- a/src/main/resources/message/message.properties +++ b/src/main/resources/message/message.properties @@ -20,3 +20,5 @@ INVALID_TOKEN=Invalid token: {} ACCESS_DENIED=Operation without privileges MESSAGE_CONVERSION_ERROR=Error occurs while converting message + +USER_NOT_FOUND=User not found: {} diff --git a/src/test/java/edu/cmipt/gcs/constant/TestConstant.java b/src/test/java/edu/cmipt/gcs/constant/TestConstant.java new file mode 100644 index 0000000..e0d8811 --- /dev/null +++ b/src/test/java/edu/cmipt/gcs/constant/TestConstant.java @@ -0,0 +1,11 @@ +package edu.cmipt.gcs.constant; + +import java.util.Date; + +public class TestConstant { + public static String USERNAME = new Date().getTime() + ""; + public static String USER_PASSWORD = "123456"; + public static String EMAIL = USERNAME + "@cmipt.edu"; + public static String ACCESS_TOKEN; + public static String REFRESH_TOKEN; +} diff --git a/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java index 99c1e6d..a7f20ca 100644 --- a/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java +++ b/src/test/java/edu/cmipt/gcs/controller/AuthenticationControllerTest.java @@ -2,12 +2,14 @@ import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.jayway.jsonpath.JsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import edu.cmipt.gcs.constant.ApiPathConstant; +import edu.cmipt.gcs.constant.TestConstant; import edu.cmipt.gcs.constant.HeaderParameter; import edu.cmipt.gcs.enumeration.ErrorCodeEnum; import edu.cmipt.gcs.util.MessageSourceUtil; @@ -22,9 +24,6 @@ import org.springframework.core.Ordered; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -import java.util.Date; /** * Tests for AuthenticationController @@ -34,12 +33,10 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Order(Ordered.HIGHEST_PRECEDENCE) public class AuthenticationControllerTest { @Autowired private MockMvc mvc; - private static String username = new Date().getTime() + ""; - private static String userPassword = "123456"; - private static String email = username + "@cmipt.edu"; private static String userDTO = """ { @@ -48,7 +45,7 @@ public class AuthenticationControllerTest { "userPassword": "%s" } """ - .formatted(username, email, userPassword); + .formatted(TestConstant.USERNAME, TestConstant.EMAIL, TestConstant.USER_PASSWORD); private static String userSignInDTO = """ { @@ -56,9 +53,7 @@ public class AuthenticationControllerTest { "userPassword": "%s" } """ - .formatted(username, userPassword); - private static String accessToken; - private static String refreshToken; + .formatted(TestConstant.USERNAME, TestConstant.USER_PASSWORD); private static String invalidUserDTO = """ @@ -75,7 +70,7 @@ public class AuthenticationControllerTest { "userPassword": "%s" } """ - .formatted(username, userPassword + "wrong"); + .formatted(TestConstant.USERNAME, TestConstant.USER_PASSWORD + "wrong"); /** * Test sign in with invalid user information @@ -88,7 +83,7 @@ public class AuthenticationControllerTest { @Order(Ordered.HIGHEST_PRECEDENCE) public void testSignUpValid() throws Exception { mvc.perform( - MockMvcRequestBuilders.post(ApiPathConstant.AUTHENTICATION_SIGN_UP_API_PATH) + post(ApiPathConstant.AUTHENTICATION_SIGN_UP_API_PATH) .contentType(MediaType.APPLICATION_JSON) .content(userDTO)) .andExpect(status().isOk()); @@ -105,24 +100,23 @@ public void testSignUpValid() throws Exception { @Test @Order(Ordered.HIGHEST_PRECEDENCE + 1) public void testSignInValid() throws Exception { - String jsonResponse = + var response = mvc.perform( - MockMvcRequestBuilders.post( + post( ApiPathConstant.AUTHENTICATION_SIGN_IN_API_PATH) .contentType(MediaType.APPLICATION_JSON) .content(userSignInDTO)) .andExpectAll( status().isOk(), - jsonPath("$.username", is(username)), - jsonPath("$.email", is(email)), + jsonPath("$.username", is(TestConstant.USERNAME)), + jsonPath("$.email", is(TestConstant.EMAIL)), jsonPath("$.id").isNumber(), - jsonPath("$.accessToken").isString(), - jsonPath("$.refreshToken").isString()) + header().exists(HeaderParameter.ACCESS_TOKEN), + header().exists(HeaderParameter.REFRESH_TOKEN)) .andReturn() - .getResponse() - .getContentAsString(); - accessToken = JsonPath.read(jsonResponse, "$.accessToken"); - refreshToken = JsonPath.read(jsonResponse, "$.refreshToken"); + .getResponse(); + TestConstant.ACCESS_TOKEN = response.getHeader(HeaderParameter.ACCESS_TOKEN); + TestConstant.REFRESH_TOKEN = response.getHeader(HeaderParameter.REFRESH_TOKEN); } /** @@ -136,15 +130,15 @@ public void testSignInValid() throws Exception { @Order(Ordered.HIGHEST_PRECEDENCE + 2) public void testRefreshValid() throws Exception { mvc.perform( - MockMvcRequestBuilders.get(ApiPathConstant.AUTHENTICATION_REFRESH_API_PATH) - .header(HeaderParameter.TOKEN, refreshToken)) - .andExpect(status().isOk()); + get(ApiPathConstant.AUTHENTICATION_REFRESH_API_PATH) + .header(HeaderParameter.TOKEN, TestConstant.REFRESH_TOKEN)) + .andExpectAll(status().isOk(), header().exists(HeaderParameter.ACCESS_TOKEN)); } @Test public void testSignInInvalid() throws Exception { mvc.perform( - MockMvcRequestBuilders.post(ApiPathConstant.AUTHENTICATION_SIGN_IN_API_PATH) + post(ApiPathConstant.AUTHENTICATION_SIGN_IN_API_PATH) .contentType(MediaType.APPLICATION_JSON) .content(invalidUserSignInDTO)) .andExpectAll( @@ -168,7 +162,7 @@ public void testSignInInvalid() throws Exception { @Test public void testSignUpInvalid() throws Exception { mvc.perform( - MockMvcRequestBuilders.post(ApiPathConstant.AUTHENTICATION_SIGN_UP_API_PATH) + post(ApiPathConstant.AUTHENTICATION_SIGN_UP_API_PATH) .contentType(MediaType.APPLICATION_JSON) .content(invalidUserDTO)) .andExpectAll( @@ -192,7 +186,7 @@ public void testSignUpInvalid() throws Exception { public void testRefreshInvalid() throws Exception { String invalidToken = "This is a invalid token"; mvc.perform( - MockMvcRequestBuilders.get(ApiPathConstant.AUTHENTICATION_REFRESH_API_PATH) + get(ApiPathConstant.AUTHENTICATION_REFRESH_API_PATH) .header(HeaderParameter.TOKEN, invalidToken)) .andExpectAll( status().isUnauthorized(), diff --git a/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java index cb89a46..1ba8f70 100644 --- a/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java +++ b/src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java @@ -1,10 +1,23 @@ package edu.cmipt.gcs.controller; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; +import edu.cmipt.gcs.constant.ApiPathConstant; +import edu.cmipt.gcs.constant.TestConstant; +import edu.cmipt.gcs.constant.HeaderParameter; +import edu.cmipt.gcs.enumeration.ErrorCodeEnum; +import edu.cmipt.gcs.util.MessageSourceUtil; + /** * Tests for UserController * @@ -13,5 +26,33 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class UserControllerTest { - @Autowired private MockMvc mvc; + @Autowired + private MockMvc mvc; + + @Test + public void testGetUserByNameValid() throws Exception { + mvc.perform(get(ApiPathConstant.USER_API_PREFIX + "/" + TestConstant.USERNAME).header(HeaderParameter.TOKEN, TestConstant.ACCESS_TOKEN)).andExpectAll( + status().isOk(), + jsonPath("$.username", is(TestConstant.USERNAME)), + jsonPath("$.email", is(TestConstant.EMAIL)), + jsonPath("$.id").isNumber()); + } + + @Test + public void testGetUserByNameInvalid() throws Exception { + String invalidUsername = TestConstant.USERNAME + "invalid"; + mvc.perform(get(ApiPathConstant.USER_API_PREFIX + "/" + invalidUsername).header(HeaderParameter.TOKEN, TestConstant.ACCESS_TOKEN)) + .andExpectAll( + status().isNotFound(), + content().json( + """ + { + "code": %d, + "message": "%s" + } + """.formatted(ErrorCodeEnum.USER_NOT_FOUND.ordinal(), + MessageSourceUtil.getMessage(ErrorCodeEnum.USER_NOT_FOUND, invalidUsername)) + ) + ); + } }