API 테스트를 작성할 때 인증/인가가 필요한 엔드포인트를 테스트하기 위해서는 각 테스트마다 로그인 과정이 선행되어야 한다.
Spring Security를 사용하는 환경이라면 @WithSecurityContext
를 통해 이를 쉽게 해결할 수 있지만, Spring Security를 사용하지 않는 환경에서는 어떻게 해결할 수 있을까?
Spring Security 환경에서의 해결방법
Spring Security 환경에서는 @WithSecurityContext
와 커스텀 애노테이션을 조합하여 간단히 해결할 수 있다. 테스트 코드에 @MockUser
같은 커스텀 애노테이션을 만들고, 이를 @WithSecurityContext
와 연결하면 테스트 실행 전 자동으로 로그인 처리가 된다.
Spring Security에서는 SecurityContextHolder
에 SecurityContext
를 설정하는 방식으로 인증 정보를 관리하지만, Spring Security를 사용하지 않는 환경에서는 다른 접근이 필요하다.
JWT 인증/인가 프로세스
현재 구현하고자 하는 본 서비스의 인증/인가 프로세스는 다음과 같은 흐름을 가진다.
- 사용자 로그인
- JWT 토큰 발급
- Authorization 헤더에 토큰을 포함하여 요청
- Filter에서 권한/유효성 검증
이러한 흐름을 테스트 환경에서 구현하기 위해 다음과 같은 요소들이 필요하다.
구현
1. MockUser 애노테이션 정의
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(MockUserExtension.class)
@Import(TestSecurityConfig.class)
public @interface MockAdminUser {
String userId() default "admin";
String password() default "";
String name() default "테스트 사용자";
}
MockUserExtension
을 적용하려면 MockUser
에 @ExtendWith(MockUserExtension.class)
를 추가해야 한다.
2. MockUserExtension 구현
JUnit 5의 Extension을 활용하여 beforeEach, afterEach의 작업을 정의할 수 있다.
public class MockUserExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
MockAdminUser annotation = context.getTestMethod()
.orElseThrow(() -> new IllegalStateException("Test method not found"))
.getAnnotation(MockAdminUser.class);
if (annotation != null) {
ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);
JwtProvider jwtProvider = applicationContext.getBean(JwtProvider.class);
LoginResDto loginResDto = LoginResDto.builder()
.id(annotation.userId())
.name(annotation.name())
.build();
String accessToken = jwtProvider.generateAccessToken(loginResDto);
MockTokenHolder.setToken(accessToken);
}
}
@Override
public void afterEach(ExtensionContext context) {
MockTokenHolder.clear();
}
}
beforeEach
에서는 @MockUser
애노테이션이 적용된 테스트 메서드를 찾고, JwtProvider
클래스를 사용하여 토큰을 발급한다.afterEach
에서는 저장해 둔 토큰을 제거한다.
3. ThreadLocal을 활용한 토큰 관리
public class MockTokenHolder {
private static final ThreadLocal<String> tokenHolder = new ThreadLocal<>();
public static void setToken(String token) {
tokenHolder.set(token);
}
public static String getToken() {
return tokenHolder.get();
}
public static void clear() {
tokenHolder.remove();
}
}
발급한 토큰은 테스트 환경 내 필터까지 유지해야 하니 SecurityContextHolder
의 기본 전략과 동일하게 ThreadLocal
에 담아준다. 이를 위해 MockTokenHolder
클래스도 커스텀하게 만들어준다.
4. 테스트 환경 내 필터 등록
발급한 토큰을 테스트 환경 내 필터까지 유지하기 위해 ThreadLocal
변수에 저장하고, 테스트 환경 내 필터를 등록하여 Authorization
헤더에 MockUserExtension
에서 발급한 토큰을 설정한다.
public class TestJwtAuthorizationFilter extends JwtAuthorizationFilter {
public TestJwtAuthorizationFilter(JwtProvider jwtProvider, PermissionManager permissionManager) {
super(jwtProvider, permissionManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 테스트 환경이고 MockTokenHolder에 토큰이 있는 경우
if (isTestEnvironment() && MockTokenHolder.getToken() != null) {
String token = MockTokenHolder.getToken();
// Authorization 헤더에 토큰 설정
request = new HttpServletRequestWrapper(request) {
@Override
public String getHeader(String name) {
if ("Authorization".equals(name)) {
return "Bearer " + token;
}
return super.getHeader(name);
}
};
}
// 기존 필터 로직 실행
super.doFilterInternal(request, response, filterChain);
}
private boolean isTestEnvironment() {
return Arrays.asList(getEnvironment().getActiveProfiles()).contains("test");
}
}
API 테스트 코드 작성
이제 API 테스트 코드를 작성할 때 @MockUser
를 사용하면, beforeEach
에서 로그인 선행 작업이 진행되고, 테스트 환경 내 필터에서 Authorization
헤더가 설정된다.
@MockUser
@Test
@DisplayName("Google Docs 조회")
void getGoogleDocs() throws Exception {
// ...
}
'Spring' 카테고리의 다른 글
Spring Security Remember-Me (0) | 2024.06.22 |
---|---|
QueryDsl - 중복 필터 where문 적용 (0) | 2024.02.10 |
Spring Cloud Config URL 조회 시 보안 (0) | 2023.11.04 |
Embedded Redis로 테스트 환경 구축하기 (0) | 2023.10.30 |
Spring Cloud Config Watch (1) | 2023.10.11 |