Kotlin

Kotlin Springboot Rest API 만들기 6 - 테스트 코드 작성, exception handling, Spring Security 인증 인가 적용하기

pepega 2023. 2. 5. 15:37

이전 포스팅에서는

jwt token, SpringSecurity로 인증 인가, 로그인을 추가했습니다.

 

이번 포스팅에서는

이전 포스팅에서 언급했던 

 

1. Jwt 토큰 없이 api를 호출하였을 경우

2. 형식에 맞지 않거나 만료된 Jwt 토큰으로 api를 호출한 경우

3. Jwt 토큰으로 api를 호출하였으나 해당 리소스에 대한 권한이 없는 경우

 

내용과

필터 적용 후의 테스트 코드를 작성하고

exception handling을 진행하겠습니다.

 

 

전체 코드는 여기에 있습니다.

 

테스트를 하기 위해서

postman으로 호출하는 것이 너무 귀찮아서

테스트 코드를 추가하게 되었습니다.

 

지금까지 만든 API는

signIn, signUp, helloWorld 입니다.

 

그 외에 다른 API는 만들지 않았습니다.

signIn, signUp은 SecurityFilterChain에서

permitAll로 설정되어 있어서

아무리 호출해도 X-AUTH-TOKEN을 테스트 할 수가 없습니다.

 

그래서 이 글에서는

actuator를 사용하여 진행하기로 하였습니다.

 

actuator의 모든 web entpoint는 /actuator 형식의 URL로 사용 할 수 있습니다.

/actuator/{id} 형식으로 사용 할 수 있습니다.

예를 들어 info 라는 entpoint

/actuator/info 처럼 사용 할 수 있습니다.

 

테스트를 위해서

health 라는 endpoint를 사용하겠습니다.

 

actuator 테스트 코드를 만들기 전

signUp, signIn 테스트 코드를 생성 한 후 진행하겠습니다.

 

SignController 테스트 코드 만들기

원하는 클래스명 위에서

alt + enter를 클릭하면 Create test 을 클릭 할 수 있습니다.

 

아래 사진처럼 세팅 후

OK를 클릭하면 됩니다.

 

아래와 같이 세팅됩니다.

 

@BeforeEach는 

@Test 보다 먼저 실행되는 annotation입니다.

@BeforEach는 void를 return 해야합니다.

private, static 메서드가 아니어야 합니다.

 

@AfterEach는

@Test 이후에 실행되는 annotation 입니다.

 

@BeforeEach, @AfterEach 메서드는 연결되지 않습니다.

예를 들어, 두 개의 @BeforEach 메서드가

A(), B()를 만들고

두 개의 @AfterEach 메서드가 A()를 destroy 할 때

@BeforeEach 메서드가 실행되는 순서(ex, CreateB()를 만들기 전에 A()를 생성하는 순서)는

@AfterEach 메서드에 대한 순서 의미를 부여하지 않습니다.

 

즉, destory A()는 desctory B() 이전 혹은 이후에 호출 될 수 있습니다.

@BeforeEach 메서드 사이에 종속성이 없는 경우

혹은 @AfterEach 메서드 사이에 종속성이 없는 경우

클래스 혹은 테스트 인터페이스 하나당 최대 하나의 @AfterEach

메서드를 선언하는 것을 권장하고 있습니다.

 

위 내용을 기반으로

테스트 코드를 마저 작성해보겠습니다.

 

이 글에서는 단위 테스트보다

SpringBootTest, AutoConfigureMockMvc annotation을 활용해서

통합 테스트를 진행 할 예정입니다.

단위 테스트는 추후 포스팅 하도록 하겠습니다.

 

통합테스트, DI 하기 위해 SpringBootTest annotation을 추가했습니다.

테스트가 실패 했을 경우 원복하기 위해 Transaction annotation을 추가하였습니다.

MockMvc의 자동 구성을 구성하기 위해 AutoConfigureMockMvc annotation을 추가하였습니다.

MockMvc서버 측 Spring MVC 테스트 지원을 위한 주 진입점입니다.

 

 

회원가입이 성공한 케이스에 대해 테스트 코드를 추가해보겠습니다.

 

회원가입 성공 테스트 코드

post : POST request를 위해 MockHttpServletRequestDsl를 생성합니다.

content : 기본적으로 UTF-8으로 설정되어 있습니다. body를 넣는 곳입니다.

andDo : 일반적인 작업을 수행합니다. 주로 print(), log()를 수행합니다.

print : System.out으로 출력합니다. log()를 사용하면 로그 레벨에서 로깅을 합니다.

andExpect : 결과에 대한 기대 값을 넣을 수 있습니다.

 

 

로그인 성공 테스트 코드

로그인을 하기 위해선

사전에 회원가입 되어있는 계정이 필요합니다.

 

@BeforEach 에서 회원가입을 진행한 후 로그인 성공 테스트 케이스를 작성하겠습니다.

BeforEach는 @Test 이전에 실행되는 코드가 들어있습니다.

 

@BeforEach 와 상수 선언

 

 

 

@Test

 

 

테스트 코드 작성이 끝났으니

테스트 코드를 실행해보겠습니다.

성공!

 

테스트 코드 클래스명을 클릭하고

ctrl + shift + f10을 클릭하여

전체 코드를 테스트해보겠습니다.

 

두 테스트 코드가 성공했습니다.

 

이제 실패 테스트 코드를 작성하겠습니다.

 

회원가입 실패 코드 (이미 있는 계정)

회원가입 시도 시

이미 있는 계정인 경우

UserExistExceptionCustom 을 throw하게 개발하였습니다.

 

테스트가 성공하였습니다.

 

위와 같은 방식으로

존재하지 않는 회원, 패스워드가 일치하지 않는 회원의 경우를 추가하면 됩니다.

성공!

 

 

블로그에 모든 코드를 적기에는 많으니

추가 코드는 github에 남기겠습니다!

 

이제 actuator를 활용하여

테스트 코드를 실행해보겠습니다.

 

 

/actuator 테스트 코드 만들기

 

common.controller 밑에 ActuatorControllerTest 클래스를 생성합니다.

 

필요한 상수 및 변수를 선언합니다.

token은 로그인 성공 시 도출되는 결과를 입력하기 위해서

lateinit을 사용했습니다.

 

 

@BeforEach를 만듭니다.

 

회원가입 후 로그인하여

lateinit한 token을 set 해줍니다.

 

 

token을 기반으로

actuator 성공 케이스를 작성합니다.

 

 

테스트가 성공했습니다.

 

 

이제

1. Jwt 토큰 없이 api를 호출하였을 경우

2. 형식에 맞지 않거나 만료된 Jwt 토큰으로 api를 호출한 경우

과정에 대해 테스트해보겠습니다.

 

 

토큰이 유효하지 않을테니

400대 오류가 날 것으로 예상하였습니다.

 

결과는 실패할 것이고

아직 관련 코드는 선언하지 않았지만

exception handing한다고 하면 -1002로 설정하겠습니다.

특정 메시지도 도출 될 예정입니다.

 

당연히 테스트 코드는 실패합니다.

io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

 

위와 같은 오류 메시지가 확인됩니다.

위와 같은 메시지가 나올 경우

유효하지 않은 토큰이라는 메시지를 도출 할 예정입니다.

 

이 글에서는 AuthenticationEntryPoint 을 활용 할 예정입니다.

AuthenticationEntryPoint 은 클라이언트에서 인증 정보를 요청하는 HTTP 응답을 보내는데 사용합니다.

 

 

인증이 안된 경우 exception handling 추가

config.security package 아래에 AuthenticationEntryPointCustom 클래스를 생성합니다.

handlerExceptionResolver 의 resolveException메서드를 사용하여

AuthenticationException를 throw 합니다.

 

토큰 관련 오류가 발생할 경우

exceptionHandling 할 수 있도록

SecurityFilterChain에 authenticationEntryPoint를 추가합니다.

 

 

AuthenticationException이 발생할 경우

오류 메시지를 response할 수 있도록

ExceptionAdvice 클래스에 코드를 추가합니다.

 

 

properties에 오류 코드와 메시지를 추가합니다.

 

 

 

다시 테스트 코드를 실행합니다.

 

성공!

1. Jwt 토큰 없이 api를 호출하였을 경우

2. 형식에 맞지 않거나 만료된 Jwt 토큰으로 api를 호출한 경우

문제는 해결됐습니다.

 

마지막으로 3번이 남았습니다.

3. Jwt 토큰으로 api를 호출하였으나 해당 리소스에 대한 권한이 없는 경우

 

 

Users API와 common MutableListResult 만들기

 

유저 정보를 list로 얻어올 수 있는

/users API와 common ListResult 클래스를 생성하겠습니다.

 

우선 model.response package 아래 MutableListResult 클래스를 생성합니다.

 

MutableListResult를 controller에서 response로 사용 할 수 있도록

ResponseService 클래스에 메서드를 추가합니다.

 

 

 

controller에서 응답을 response 할 수 있는 dto를 생성합니다.

 

모든 유저를 조회하는 service를 생성합니다.

common.service.user 패키지 아래에 UserService 클래스를 생성합니다.

 

 

users controller를 생성합니다.

 

 

users controller 테스트 코드 작성

상수는 위와 같이 선언했습니다.

ADMIN 계정은 따로 생성하고

일반 유저는

ID = User

PW = @real.com 을 사용합니다.

 

for loop 을 사용하여 0...4로 5 계정을 생성 할 예정입니다.

총 6개의 계정을 생성 할 예정입니다.

 

 

BeforEach에서

ADMIN 권한을 가진 계정을 생성합니다.

이후 for loop에서 테스트 계정 5개를 추가로 생성합니다.

 

 

총 6개의 계정을 생성했으니

results list 기대 값을 6으로 설정합니다.

성공!

 

이제 권한이 없는 경우 (AccessDenied) exception handling을 추가하겠습니다.

 

 

권한이 없는 경우 exception 추가

SpringSecurity의 AccessDeniedHandler를 사용합니다.

AuthenticationEntryPointCustom 클래스와 마찬가지로

config.security package 아래에 AccessDeniedHandlerCustom 클래스를 추가합니다.

 

 

EntryPointCustom 클래스와 마찬가지로

handlerExceptionResolver에게 Exception throw를 맡깁니다.

 

 

AccessDeniedException 을 handling 할 수 있도록

ExceptionAdvice 클래스에 ExceptionHandler를 추가합니다.

 

 

 

 

추가가 완료되었으면

SecurityFilterChain에 accessDeniedHandler를 추가해줍니다.

 

 

이제 테스트 코드를 추가해줍니다.

 

권한이 없는 경우 exception 테스트 코드 추가

모든 유저 정보를 가져오는 클래스(UserControllerTest.kt)클래스와 같은 클래스 입니다.

일반 유저의 토큰으로 API를 조회하는 테스트 코드입니다.

 

성공!

이로써 3번까지

3. Jwt 토큰으로 api를 호출하였으나 해당 리소스에 대한 권한이 없는 경우

 

적용이 완료되었습니다!

 

다음 글은 어떤걸 할지 고민 해야겠습니다

 

 

참고자료

https://cheese10yun.github.io/spring-actuator/

https://docs.spring.io/spring-boot/docs/current/actuator-api/htmlsingle/#overview

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-authenticationentrypoint

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/access/AccessDeniedHandler.html