본문 바로가기
카테고리 없음

인증이 필요한 요청에 대한 테스트 작성 (SpringBoot & JUnit5)

by 찐세 2021. 6. 20.

 

스프링 시큐리티를 사용해서 애플리케이션을 작성하다보면 대부분의 요청에 대해서 사용자의 인증을 필요로 하게 된다. 그렇기 때문에 애플리케이션의 대부분의 요청은 인증된 사용자만 접근이 가능하다.

 


그렇다면 테스트는 어떨까?
테스트도 동일하게 애플리케이션을 띄우고 작성한 코드에 의해서 요청을 보내고 테스팅을 하는 것이기 때문에 마찬가지로 인증을 필요로 한다.

 

인증이 필요한 요청을 인증 없이 접근한 경우

이처럼 테스트시에도 인증을 필요로하는 요청을 인증 없이 요청하게되면, 스프링 시큐리티에 의해서 /login으로 리다이랙트된다.

 

 

그렇다면 테스트마다 인증이 필요할 것이고, 매번 로그인을 해줘야할 것이다. 
하지만 그렇다고 매 테스트마다 사용자를 생성하고, 로그인을 하는 것을 반복하기는 쪼오끔 그렇다^^

 

 

그렇기 때문에 스프링 시큐리티에서는 Testing에 대한 기능을 제공한다. 이 기능을 통해서 인증된 상태로 테스트를 진행할 수 있는 것이다.

자세한 내용은 여기를 참고하자.

https://docs.spring.io/spring-security/site/docs/current/reference/html/test.html

 

 

 

 

1. @WithMockUser

 

첫번째 선택은 @WithMockUser가 될 수 있다. 

 

이는 기본적으로 username 을 "user"로 password를 "password"로 하는 User를 생성한다.

그리고 이를 Principal로 하는 Authentication(UsernamePasswordAuthenticationToken)을 세션에 등록한다.

 

이것은 간단하게 사용할 수 있지만 현재 나의 서비스의 경우, DB에 저장되어 있는 사용자의 정보를 가져와서 일치하는 경우에만 세션에 등록해야하기 때문에 이는 적합하지 않다.

 

 

 

 

 

2. @WithUserDetails

 

두번째 선택은 @WithUserDetails 이다.

 

내용을 읽어보면 @WithUserDetails를 사용하면 커스텀 UserDetailsService를 통해 리턴한 UserDetails의 구현체를 가지고 인증을 할 수 있다고 되어 있다. 

 

그렇다면 이는 내가 로그인할때 사용한 UserDetailsService의 구현체를 사용해서 내가 원하는 방식으로 인증 정보를 세션에 등록할 수 있을 것처럼 보인다. 한번 해보자!!

 

그런데 자세히 읽어보니 다음과 같은 내용이 있었다.

JUnit의 @Before 가 실행되기 이전에 @WithUserDetails가 먼저 실행되는 것이 디폴트라고 되어있다.

그렇다는 말은 우리가 매번 테스트를 실행할때 사용자를 DB에 등록하고, 그 정보를 세션에 등록해야하는데, @Before 보다 먼저 수행된다면 사용자를 생성 하기도 전에 데이터를 접근해서 세션에 넣을 것이다. 이는 맞지 않다.

 

 

다행이도 setupBefore = TestExcutionEvent.TEST_EXECUTION을 통해서 @Before 실행 이후, 테스트 매서드 실행하기 전에 @WithUserDetails가 실행되게 할 수 있다고 나와있다. 

그렇다면 @Before을 통해서 테스트용 사용자를 DB에 등록하고, 그 다음 세션에 등록할 수 있을 것이다.

 

하지만,,, 이것은 버그가 존재한다고 한다. 위 설정을 하더라도 여전히 @WithUserDetails가 먼저 실행이 된다. 

사용자를 등록하기도 전에 UserDeatilsService로 사용자의 정보를 찾으려고 하니까 당연히 에러가 난다.

어쩔수 없이 이 방법도 사용하는 것은 힘들게 되었다.

 

 

 

 

 

 

3. @WithSecurityContext

 

마지막으로 스프링 시큐리티가 제공하는 기능은 @WithSecurityContext이다.

 

@WithSecurityContext를 사용해서 커스텀 애너테이션을 생성할 수 있고, 몹시 유연하게 사용자의 인증 정보를 등록할 수 있다. 

 

WithMockCustomUserSecurityContextfactory 클래스를 알맞게 구현해서 사용하면 된다.

WithSecurityContextFactory 인터페이스의 creatSecurityContext 메서드를 적절하게 오버라이딩하면 된다.

그렇다면 이 메서드를 실행할 때 사용자를 생성하고, 이 정보로 Authentication을 생성하면 될 것이다.

 

import org.springframework.security.test.context.support.WithSecurityContext;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
    String value();
}

 

package com.jingeore;

import com.jingeore.account.AccountService;
import com.jingeore.account.form.SignUpForm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.context.support.WithSecurityContextFactory;


@RequiredArgsConstructor
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {

    private final AccountService accountService;

    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser withMockCustomUser) {

        String nickname = withMockCustomUser.value();

        // 사용자의 정보를 세션에서 등록하기 전에 사용자의 정보를 생성해서 DB에 저장하기 때문에 에러가 발생하지 않는다.
        SignUpForm signUpForm = new SignUpForm();
        signUpForm.setNickname(nickname);
        signUpForm.setPassword("32165432132");
        signUpForm.setEmail(nickname+"@test.com");
        accountService.saveNewAccount(signUpForm);

        //사용자의 정보를 세션에 등록해준다.
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        UserDetails userDetails = accountService.loadUserByUsername(nickname);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

        context.setAuthentication(authentication);
        return context;
    }
}

 

이렇게 @WithSecurityContext로 커스텀 애너테이션을 생성해서 테스트 코드가 실행되기 전에 테스트용 사용자에 대해서 인증이 되고, 인증이 된 상태로 테스트를 진행할 수 있게 된다.

 

아 맞다!  매번 테스트가 실행되기 전에 커스텀 애너테이션에 의해서 테스트용 사용자가 생성되어 DB에 저장이 되기 때문에, 테스트가 종료될 때마다 이를 삭제해주어야한다.

 

 @AfterEach
    void afterEach(){
        accountRepository.deleteAll();
    }