[Spring] 테스트 코드에서 권한이 필요한 API 호출하기
문제 상황
Controller에 정의한 API 중 로그인한 사용자만 사용 가능한 권한이 필요한 API가 있는 상황이었다.
해당 함수에 대한 테스트 코드를 작성하고 실행했는데 API 함수를 호출하기도 전에 계속 빈 생성을 덜했다거나 의존성 문제가
남발했고, 그렇게 생성안된 빈들은 모두 Spring Security에 정의된 것들이었다. 테스트하는데 꼭 필요한 내용인지는 모르겠어서
해당되는 빈들을 모두 생성하지 않도록 걸러주는 방법이 없을까 생각해봤다.
일단 Controller라고 콕찝어 얘기한 이유는 Controller 단위 테스트를 수행하고 싶었기 때문이다! (@WebMvcTest
사용)
즉, Presentation Layer 관련 컴포넌트만 스캔하고 Service, Repository는 @MockBean
으로 주입받으려 했다.
Controller에 정의한 메소드에 대해서 적절한 응답이 오는지만 체크하고, Service, Repository는 별도로 검증하고 싶었다.
java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@5df92089 testClass = fittering.mall.controller.CategoryControllerTest Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘jwtAuthenticationFilter’ Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘fittering.mall.config.jwt.JwtTokenProvider’ available: expected at least 1 bean which qualifies as autowire candidate.
인증/인가 방식으로는 프로젝트에서 JWT를 사용하고 있었고 Spring Security 설정 클래스 내에서 JWT 관련 클래스를 주입받는
상황이다보니 테스트 함수를 실행하기 전에 빈 생성이 제대로 되지 않았다는 문제가 발생했었다.
추가로 @AuthenticationPrincipal
주입 문제도 있었는데, 아래 해결 방법 중 @WithCustomMockUser
를 참고하자.
해결 방법
@WithMockUser
빈 생성 제외 (Optional)
Optional
이라고 적은 이유는 Spring Security 설정 파일 내에 JwtAuthenticationFilter
를 주입받아 의존성이 있는 상황
인 경우에 한해서이다. 그래서 문제 상황에 적힌 예외도 발생한거니까 해당되는 경우에만 아래를 따라가면 된다. 방식은 2가지다!
Controller 단위 테스트를 위해 @WebMvcTest
를 사용하고 있어서 다음처럼 빈 생성을 제외하는 옵션을 줄 수 있었다.
type은 FilterType.ASSIGNABLE_TYPE
을, 빈 생성 제외 클래스들은 classes에 적어주면 된다.
Spring Security 설정 클래스인 SecurityConfig
와 거기에 등록된 JWT 필터 JwtAuthenticationFilter
를 제외했다.
SecurityConfig
만 등록해봤는데 잘 해결되지 않아서 JwtAuthenticationFilter
도 같이 등록해주니 해결됐다!
@WebMvcTest(
controllers = CategoryController.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {
SecurityConfig.class,
JwtAuthenticationFilter.class
}
)
)
class CategoryControllerTest { ... }
- controllers : 단위 테스트 대상 Controller 정보
excludeFilters
를 걸어서 처리해주고 싶지 않다면, 아래처럼 @MockBean
을 이용해보자.
@WebMvcTest(controllers = MallController.class)
class CategoryControllerTest {
@MockBean
JwtAuthenticationFilter jwtAuthenticationFilter;
...
}
유저 설정과 CSRF
카테고리 등록 메소드가 유저 권한이 필요했는데 @WithMockUser
를 설정해주니 해결할 수 있었다.
그리고 CSRF 설정도 추가해줘야 한다! 그렇지 않으면 403 Forbidden
에러를 뱉는다.
Spring Security 기본 설정에는 CSRF 설정을 해줘야하는 것으로 알고 있는데, 프로젝트에서 적용하던 Security 설정 클래스
SecurityConfig
를 위에서 제외하는 바람에 기본 설정으로 들어간 것 같다. 때문에 @WithMockUser
도 먹힌듯 하다.
@DisplayName("카테고리를 등록한다.")
@Test
@WithMockUser(username = "test", roles = "USER") //추가
void createCategory() throws Exception {
//given
String categoryName = "상의";
//when //then
mockMvc.perform(
post("/api/v1/auth/categories/" + categoryName)
.with(csrf()) //추가
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("카테고리 등록 완료"));
}
@WithCustomMockUser
@AuthenticationPrincipal Mock 객체 주입하기
@WithMockUser
방식은 매우매우 간단해서 편리하지만 안타깝게도 아래처럼 @AuthenticationPrincipal
를 사용해서 현재
사용자 정보를 얻으려 하는 API의 경우 PrincipalDetails
이 null로 들어오기 때문에 사용할 수 없다.
@GetMapping("/api/v1/auth/malls/{mallId}")
public ResponseEntity<?> getMall(@PathVariable("mallId") @NotEmpty Long mallId,
@AuthenticationPrincipal User user) { //@WithMockUser를 쓰면 null이 들어온다.
...
}
이는 @WithMockUser
를 통해 생성되는 User 객체 타입이 주입받고자 하는 타입과 다르거나, 갖고 있는 필드가 부족해
null
이 들어오는 것이다. 때문에 직접 사용하고 있는 User 객체를 생성해주도록 설정해줄 필요가 있다.
아래는 그런 역할을 하는 @WithCustomMockUser
를 정의하고 적용하는 내용을 다룬다.
@WithCustomMockUser
를 사용할 수 있도록 애노테이션을 정의해주자.
그리고 생성하려는 User 객체에 필요한 내용을 설정해주자.
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomMockUserSecurityContextFactory.class)
public @interface WithCustomMockUser {
String username() default "test";
String password() default "password";
String role() default "USER";
}
WithSecurityContextFactory
를 구현하는 WithCustomMockUserSecurityContextFactory
를 정의해주자.
테스트 환경에서 사용할 SecurityContext
를 정의해줄 클래스를 의미한다.
제네릭으로 WithCustomMockUser
를 설정해줌으로써 annotation
에서 위에서 정의한 값을 참조할 수 있다.
public class WithCustomMockUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
@Override
public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
User user = User.builder()
.username(annotation.username())
.password(annotation.password())
.build();
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user, user.getPassword(), List.of(new SimpleGrantedAuthority(annotation.role())));
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(token);
return context;
}
그리고 테스트 클래스에서 MockMvc
를 통해 테스트한다면 다음 설정을 추가해줘야 한다.
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
다음은 적용했을 때 테스트 클래스 코드다.
@WebMvcTest(controllers = MallController.class)
class MallControllerTest {
@Autowired
private WebApplicationContext context; //setUp()에서 사용하기 위해 주입
@MockBean
JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MallService mallService;
//설정 추가
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@DisplayName("쇼핑몰을 조회한다.")
@Test
@WithCustomMockUser //추가
void getMall() throws Exception {
//given
Long mallId = 1L;
RequestMallDto request = RequestMallDto.builder()
.url("https://www.test.com")
.description("테스트 쇼핑몰")
.build();
ResponseMallDto response = ResponseMallDto.builder()
.id(1L)
.url("https://www.test.com")
.description("테스트 쇼핑몰")
.build();
when(mallService.findById(1L, 1L)).thenReturn(response);
//when //then
mockMvc.perform(
get("/api/v1/auth/malls/" + mallId)
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk());
}
}
References
- 민돌v: [Spring Security] @AuthenticationPrincipal 유닛 테스트 - Custom Mock User 삽입하기
- Tecoble: Spring Security가 적용된 곳을 효율적으로 테스트하자
- gom20: [Spring Boot] Controller 단위 테스트 (@WebMvcTest, MockMvc)
- 세댕댕이: @WebMvcTest 에서 Spring Security 적용, 401/403 에러 해결하기 - csrf
- highright96: Test / Spring Security / 단위 테스트에 Security 추가하기
- JiwonDev: spring security 테스트용 설정 방법
댓글남기기