Skip to content

Commit

Permalink
Merge pull request #41 from CMIPT/kaiser-crud
Browse files Browse the repository at this point in the history
Add function to get user info by name.
  • Loading branch information
Kaiser-Yang authored Sep 12, 2024
2 parents fa2e1e4 + 1826570 commit 1a8f6c7
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
2 changes: 2 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,14 +97,22 @@ 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<UserVO> signIn(@Validated @RequestBody UserSignInDTO user) {
QueryWrapper<UserPO> wrapper = new QueryWrapper<UserPO>();
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)
Expand Down Expand Up @@ -134,7 +143,11 @@ public void signOut(@RequestBody List<String> 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<Void> 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();
}
}
36 changes: 36 additions & 0 deletions src/main/java/edu/cmipt/gcs/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,50 @@
package edu.cmipt.gcs.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

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.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
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 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;

@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<UserPO> wrapper = new QueryWrapper<UserPO>();
wrapper.eq("username", username);
if (!userService.exists(wrapper)) {
throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, username);
}
return new UserVO(userService.getOne(wrapper));
}
}
4 changes: 3 additions & 1 deletion src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public ResponseEntity<ErrorVO> 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()));
Expand Down
14 changes: 2 additions & 12 deletions src/main/java/edu/cmipt/gcs/pojo/user/UserVO.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
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 = "[email protected]") String email,
@Schema(description = "Access Token") String accessToken,
@Schema(description = "Refresh Token") String refreshToken) {
@Schema(description = "Email", example = "[email protected]") 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));
this(userPO.getId(), userPO.getUsername(), userPO.getEmail());
}
}
2 changes: 2 additions & 0 deletions src/main/resources/message/message.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
11 changes: 11 additions & 0 deletions src/test/java/edu/cmipt/gcs/constant/TestConstant.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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.request.MockMvcRequestBuilders.post;
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 edu.cmipt.gcs.constant.ApiPathConstant;
import edu.cmipt.gcs.constant.HeaderParameter;
import edu.cmipt.gcs.constant.TestConstant;
import edu.cmipt.gcs.enumeration.ErrorCodeEnum;
import edu.cmipt.gcs.util.MessageSourceUtil;

Expand All @@ -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
Expand All @@ -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 =
"""
{
Expand All @@ -48,17 +45,16 @@ public class AuthenticationControllerTest {
"userPassword": "%s"
}
"""
.formatted(username, email, userPassword);
.formatted(
TestConstant.USERNAME, TestConstant.EMAIL, TestConstant.USER_PASSWORD);
private static String userSignInDTO =
"""
{
"username": "%s",
"userPassword": "%s"
}
"""
.formatted(username, userPassword);
private static String accessToken;
private static String refreshToken;
.formatted(TestConstant.USERNAME, TestConstant.USER_PASSWORD);

private static String invalidUserDTO =
"""
Expand All @@ -75,7 +71,7 @@ public class AuthenticationControllerTest {
"userPassword": "%s"
}
"""
.formatted(username, userPassword + "wrong");
.formatted(TestConstant.USERNAME, TestConstant.USER_PASSWORD + "wrong");

/**
* Test sign in with invalid user information
Expand All @@ -88,7 +84,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());
Expand All @@ -105,24 +101,22 @@ public void testSignUpValid() throws Exception {
@Test
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public void testSignInValid() throws Exception {
String jsonResponse =
var response =
mvc.perform(
MockMvcRequestBuilders.post(
ApiPathConstant.AUTHENTICATION_SIGN_IN_API_PATH)
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);
}

/**
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(),
Expand Down
48 changes: 48 additions & 0 deletions src/test/java/edu/cmipt/gcs/controller/UserControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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 edu.cmipt.gcs.constant.ApiPathConstant;
import edu.cmipt.gcs.constant.HeaderParameter;
import edu.cmipt.gcs.constant.TestConstant;
import edu.cmipt.gcs.enumeration.ErrorCodeEnum;
import edu.cmipt.gcs.util.MessageSourceUtil;

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;
Expand All @@ -14,4 +27,39 @@
@AutoConfigureMockMvc
public class UserControllerTest {
@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))));
}
}

0 comments on commit 1a8f6c7

Please sign in to comment.