728x90
반응형
SMALL

테스트 코드를 작성할 때 Spring Security를 적용한 애플리케이션은 어떻게 인증/인가를 해야하는지 알아보자.

다음 테스트 코드를 보자.

package cwchoiit.springsecurity.domain.note.controller;

import cwchoiit.springsecurity.domain.user.entity.User;
import cwchoiit.springsecurity.domain.user.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.TestExecutionEvent;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

import static cwchoiit.springsecurity.domain.user.entity.User.Role.ROLE_USER;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@Transactional
@ActiveProfiles("test")
class NoteControllerTest {
    private MockMvc mockMvc;
    @Autowired
    private WebApplicationContext webApplicationContext;
    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(springSecurity())
                .alwaysDo(print())
                .build();

        userRepository.save(new User("user", "user", ROLE_USER));
    }

    @Test
    void getNotes_not_authentication() throws Exception {
        mockMvc.perform(get("/note"))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrlPattern("**/login"));
    }

    @Test
    @WithUserDetails(
            value = "user",
            userDetailsServiceBeanName = "userServiceImpl", // UserDetailsService 구현한 구현체의 빈 이름
            setupBefore = TestExecutionEvent.TEST_EXECUTION //언제 유저가 세팅되는지를 지정 (TEST_EXECUTION -> 이 테스트가 실행되기 바로 직전에 유저를 세팅)
    )
    void getNotes_authenticated() throws Exception {
        mockMvc.perform(get("/note"))
                .andExpect(status().isOk());
    }
}
  • 우선 당연히 @SpringBootTest 애노테이션을 붙여서, 스프링 애플리케이션으로 테스트를 실행해야 한다. 스프링 시큐리티가 결국 스프링 위에 있는 녀석이니까.
  • 그리고 필요한게 MockMvc이다. 요청을 날려봐야 하니까.

그래서, 테스트가 실행되기 전에 사전 작업이 필요한데 그 부분이 아래 부분이다.

@BeforeEach
void setUp() {
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .apply(springSecurity())
            .alwaysDo(print())
            .build();

    userRepository.save(new User("user", "user", ROLE_USER));
}
  • @BeforeEach 애노테이션으로 각 테스트가 실행하기 전에 실행되게 한다.
  • springSecurity()를 적용해서 스프링 시큐리티 애플리케이션의 테스트임을 지정한다.
  • 유저를 Mocking해서 요청 시 인증/인가에 적용하려면 일단 유저가 필요하다. 그래서 `ROLE_USER`권한의 유저를 하나 만든다. 

 

다음은 인증이 되지 않은 요청이 들어왔을 때 테스트 코드이다.

@Test
void getNotes_not_authentication() throws Exception {
    mockMvc.perform(get("/note"))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrlPattern("**/login"));
}
  • 어떠한 인증도 하지 않은 요청이 들어오면 로그인 페이지로 리다이렉트 되는지를 확인하는 테스트이다.

 

다음은 인증을 특정 유저로 했을 때 정상적으로 동작하는지를 확인하는 테스트이다. 이 테스트가 중요하다.

@Test
@WithUserDetails(
        value = "user",
        userDetailsServiceBeanName = "userServiceImpl", // UserDetailsService 구현한 구현체의 빈 이름
        setupBefore = TestExecutionEvent.TEST_EXECUTION //언제 유저가 세팅되는지를 지정 (TEST_EXECUTION -> 이 테스트가 실행되기 바로 직전에 유저를 세팅)
)
void getNotes_authenticated() throws Exception {
    mockMvc.perform(get("/note"))
            .andExpect(status().isOk());
}
  • 여기서, @WithUserDetails() 애노테이션으로, UserDetails 타입의 유저를 mockMvc로 요청할 때 사용한다.
  • 그러니까, UserDetailsService를 구현한 구현체의 빈 이름을 알려주면, 해당 빈을 찾아서 그 빈이 구현한 loadUserByUsername()을 사용한다. 그때 유저이름을 value값으로 지정한 `user`로 사용한다.
  • 쉽게 말해, `/note`로 요청을 날릴 때 SecurityContext안에 Authentication으로 이 유저를 집어넣는다는 의미가 된다. 그래서 실제로 알려준 빈을 통해 loadUserByUsername('user')를 호출해서 찾아진 유저를 SecurityContext안에 넣는다.
참고로, @WithMockUser, @WithAnonymousUser라는 애노테이션도 있는데, @WithMockUser는 반환하는 타입이 org.springframework.security.core.userdetails.User라서 만약, 서비스 내에서 UserDetails를 구현한 본인만의 User를 사용한다면 이 애노테이션을 사용할 순 없다. 그리고 @WithAnonymousUser는 말 그대로 Authentication 정보로 `anonymousUser`가 들어간 상태로 테스트가 진행된다. 
또한, @WithSecurityContext 애노테이션도 있는데, 이 애노테이션은 Authentication을 가짜로 만드는게 아니라, 아예 진짜 SecurityContext를 만드는 애노테이션이다.

 

728x90
반응형
LIST

+ Recent posts