로그인을 할 때, 입력값과 출력값을 생각해보자.
- 입력
- 아이디
- 비밀번호
- 출력
- Access Token
- Refresh Token
- 발생가능한 예외사항
- 아이디가 존재하지 않을 때
- 아이디를 입력하지 않았을 때
- 비밀번호가 일치하지 않을 때
- 비밀번호를 입력하지 않았을 때
이 정도의 경우들을 생각할 수 있다.
발생가능한 예외사항부터 처리하고, 성공하는 경우들을 만들어보려고 한다.
예외를 크게 두 방식으로 묶어보자면,
- @Valid로 인해 생기는 예외(Validation 이용)
- 자체적으로 Service에서 확인했을 때 발생하는 예외
이렇게 두 가지가 있다.
일단, Validation을 위한 에러에서는,
@RestControllerAdvice
public class ValidationExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response needToWriteParameterException(MethodArgumentNotValidException e){
return Response.failure(400, e.getFieldErrors().get(0).getDefaultMessage());
}
}
@NotNull, @NotEmpty에 적혀있는 메시지와 함께 400코드를 반환하도록 예외 처리 기능을 만들었다.
이걸 이용해, ValidationException이 정상적으로 동작하는지 검증하는 코드를 만들었다.
@Test
@DisplayName("입력중에 아이디가 null값일 때 validation으로 예외처리된다.")
public void signIn_Fail_UsernameNullInput() throws Exception{
//given
SignInRequestDto signInRequestDto = SignInRequestDto.builder()
.password("test")
.build();
//expected
mvc.perform(MockMvcRequestBuilders.post("/api/auth/sign_in")
.contentType(MediaType.APPLICATION_JSON)
.content(makeJson(signInRequestDto)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("아이디를 입력해야 합니다."))
.andDo(MockMvcResultHandlers.print());
}
@Test
@DisplayName("입력중에 아이디가 공백문자열일 때 validation으로 예외처리된다.")
public void signIn_Fail_UsernameBlankInput() throws Exception{
//given
SignInRequestDto signInRequestDto = SignInRequestDto.builder()
.username(" ")
.password("test")
.build();
//expected
mvc.perform(MockMvcRequestBuilders.post("/api/auth/sign_in")
.contentType(MediaType.APPLICATION_JSON)
.content(makeJson(signInRequestDto)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("아이디를 입력해야 합니다."))
.andDo(MockMvcResultHandlers.print());
}
@Test
@DisplayName("입력중에 비밀번호가 null값일 때 validation으로 예외처리된다.")
public void signIn_Fail_PasswordNullInput() throws Exception{
//given
SignInRequestDto signInRequestDto = SignInRequestDto.builder()
.username("testUser")
.build();
//expected
mvc.perform(MockMvcRequestBuilders.post("/api/auth/sign_in")
.contentType(MediaType.APPLICATION_JSON)
.content(makeJson(signInRequestDto)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("비밀번호를 입력해야 합니다."))
.andDo(MockMvcResultHandlers.print());
}
@Test
@DisplayName("입력중에 비밀번호가 null값일 때 validation으로 예외처리된다.")
public void signIn_Fail_PasswordBlankInput() throws Exception{
//given
SignInRequestDto signInRequestDto = SignInRequestDto.builder()
.username("testUser")
.password(" ")
.build();
//expected
mvc.perform(MockMvcRequestBuilders.post("/api/auth/sign_in")
.contentType(MediaType.APPLICATION_JSON)
.content(makeJson(signInRequestDto)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("비밀번호를 입력해야 합니다."))
.andDo(MockMvcResultHandlers.print());
}
전부 @BeforeEach로 회원을 만들고, 이에 대한 로그인을 요청할 때, 필요한 값중 하나씩을 null, 또는 공백문자로 처리했다.
이제, 아이디가 존재하지 않을 때 동작하는 것을 검증해야 한다.
아이디 = testUser, 비밀번호 = test로 만든 상태에서, test1을 아이디로 적으면 어떻게 되어야 할까?
없는 값이기에 not found(404)와 함께, 회원을 찾을 수 없음을 반환해야 한다.
@Test
@DisplayName("존재하지 않는 아이디로 로그인을 요청할 때, 404에러와 회원이 없음을 바디에 반환한다.")
public void signIn_Fail_NOT_FOUND() throws Exception{
//given
SignInRequestDto signInRequestDto = SignInRequestDto.builder()
.username("testUser1")
.password("test")
.build();
//expected
mvc.perform(MockMvcRequestBuilders.post("/api/auth/sign_in")
.contentType(MediaType.APPLICATION_JSON)
.content(makeJson(signInRequestDto)))
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(false))
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(404))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.failMessage").value("해당 사용자를 찾을 수 없습니다."))
.andDo(MockMvcResultHandlers.print());
}
이런식으로 404에러가 나오는 것을 검증했다.
비밀번호는 Bad Request 400에러와 함께 failMessage에 '비밀번호가 일치하지 않습니다' 를 넣어 구현했다.
로그인을 성공할 때에는 어떨까?
일단, jwt를 쓰면서, 우리는 헤더에 토큰을 주고, 이 토큰을 권한이 필요한 url에 요청할 때 사용한다.
그렇다면, 토큰을 반환하는 곳은 바디가 아닌, 헤더여야 한다.
이 헤더를 사용하기 위해서, TokenPath라는 클래스를 만들었다.
(말 그대로 Token을 파라미터로 받아 헤더에 넣던가, 헤더에서 토큰을 받아 반환해주는 기능을 담당하는 토큰의 통로 이다.)
@Configuration
public class TokenPath {
public ReissueRequestDto getReissueResponseDtoFromHeader(HttpServletRequest request){
return ReissueRequestDto.builder()
.accessToken(resolveAccessToken(request))
.refreshToken(resolveRefreshToken(request))
.build();
}
public void putTokensOnHeader(HttpServletResponse response, TokenResponseDto tokenResponseDto){
setHeaderAccessToken(response, tokenResponseDto.getAccessToken());
setHeaderRefreshToken(response, tokenResponseDto.getRefreshToken());
}
private void setHeaderAccessToken(HttpServletResponse response, String accessToken) {
response.setHeader("Authorization", "Bearer "+ accessToken);
}
// 리프레시 토큰 헤더 설정
private void setHeaderRefreshToken(HttpServletResponse response, String refreshToken) {
response.setHeader("RefreshToken", "Bearer "+ refreshToken);
}
private String resolveAccessToken(HttpServletRequest request) {
if(request.getHeader("Authorization") == null) {
throw new LogInAgainException();
}
return request.getHeader("Authorization").substring(7);
}
private String resolveRefreshToken(HttpServletRequest request) {
if(request.getHeader("RefreshToken") == null )
throw new LogInAgainException();
return request.getHeader("RefreshToken").substring(7);
}
}
Access Token은 Authorization이라는 key로, Refresh Token은 RefreshToken이라는 key로 헤더에 넣을 계획을 가지고 있었다.
이에 맞게, HttpServletResponse에 header를 넣어주던가, request에서 getHeader를 해주었다.
(reissue에 필요한 기능도 미리 만들어져 있는 상태이다)
이걸 Controller에 넣어주었다.
이후, sign_in을 성공했을 때에는, 헤더에 해당 key가 존재하는지 확인하는 방법으로 검증했다.
이렇게 로그인이 성공하면, header에 .exists가 있다는 것을 andExpect로 추가했다.
(한국어로 바꾸자면, 우리는 헤더에 "Authorization"이라는 key의 값이 있을거라고 믿어요~~ 라는 뜻 정도로 해석해보자.)
이렇게 테스트코드를 뭐빠지게 만들어보았다.
이제 이걸 검증하려고 하면, 실패한다.
왜냐면 실제 저 url에 맞는 컨트롤러 로직도 없고, 저 url은 Security Filter Chain에서 acceptAll되지 않은 주소니까.
이렇게 컨트롤러 로직을 만들어주었고,
이런식으로 해당 url에 대한 필터 체인의 검증을 무시하게 해주었다.
이제야 테스트를 정상적으로 돌릴 수 있다.
이전에 컨트롤러 테스트코드를 만드는 법을 책을 찾아보고, 강의도 여럿 보면서 익혔었는데,
이번에 헤더를 쓰는 법은 조금 다른 방법으로 공부했다.
'까짓거 뜯어보다보면 어디든 만드는 방법이 SpringBoot에 주석으로 있겠지' 하면서 공부해보았는데, 원하는 대로 만들어져서 그래도 다행이라는 생각이 든다.
이제 reissue를 만들어보고, 멤버에서 벗어나 게시물(Post)를 만들어보아야 겠다.
'스프링 공부 > 게시판 프로젝트 만들기' 카테고리의 다른 글
[스프링] 36. MemberController.edit을 조금 다듬어서 테스트해보자. (0) | 2023.02.20 |
---|---|
[스프링] 35. AuthController.reissue 테스트부터 프로젝트 코드까지 (0) | 2023.02.20 |
[스프링] 33. 잠깐 쉬면서 만들었던 것들을 리펙토링 한 과정을 요약해보자. (0) | 2023.02.16 |
[스프링] 32. MemberController.register 중복과 기타사유의 예외 테스트 (0) | 2023.02.12 |
[스프링] 31. MemberController-회원가입 기능 구현하기, Null/Success 테스트 (0) | 2023.02.12 |