-
Notifications
You must be signed in to change notification settings - Fork 26
6. Testing
스프링 시큐리티는 테스트를 위한 지원을 포함하고 있습니다. 스프링 시큐리티가 지원하는 테스트 모듈을 통해 단위 테스트에서 활용하는 법을 설명합니다.
Web Security 예제의 테스트 케이스를 참고하세요
다음과 같이 스프링 시큐리티의 메소드 보안 표현식을 적용한 서비스 클래스가 있습니다.
@Service
public class RepositoryService extends AbstractService {
@PreAuthorize("hasAuthority('REPOSITORY_CREATE')")
public String create(Repository repository) {
return UUID.randomUUID().toString();
}
@PreAuthorize("@repositoryChecker.owner(authentication, #repository.id)")
public boolean update(@P("repository") Repository repository) {
return true;
}
@PreAuthorize("@repositoryChecker.ownerWithName(authentication, #id, #name)")
public boolean delete(String id, String name) {
return true;
}
}
위 예제에서 각 생성, 수정, 삭제 함수는 저장소 생성 권한과 저장소의 소유자를 체크하는 부분이 적용되어있습니다. 그렇기 때문에 서비스 단위 테스트 시 보안을 통과하기 위해서는 현재 보안 컨텍스트에 인증된 사용자의 정보를 저장해야합니다.
다행히도 친절한 스프링 시큐리티는 메소드 보안 표현식을 적용했을때 단위 테스트를 지원하기 위하여 보안 컨텍스트에 인증된 사용자 정보를 저장할 수 있는 몇가지 어노테이션을 지원합니다.
@WithMockUser 어노테이션은 org.springframework.security.core.userdetails.User를 보안 컨텍스트에 저장할 수 있도록 지원합니다.
@WithMockUser(authorities = {"REPOSITORY_CREATE"})
@Test
public void TEST_000_createRepository() {
Repository repository = new Repository();
String repositoryId = repositoryService.create(repository);
Assert.assertNotNull(repositoryId);
LOG.info("repositoryId : {}", repositoryId);
}
@WithMockUser(authorities = {})
@Test(expected = AccessDeniedException.class)
public void TEST_000_createRepositoryNoneAuthority() {
Repository repository = new Repository();
String repositoryId = repositoryService.create(repository);
Assert.assertNotNull(repositoryId);
}
현재 인증된 사용자가 REPOSITORY_CREATE
권한을 가지지 않는다면 AccessDeniedException
이 발생하는 것을 두번째 테스트 케이스에서 확인할 수 있습니다.
@WithUserDetails는 UserDetailsService로 제공된 UserDetails 구현체를 보안 컨텍스트에 저장할 수 있도록 지원합니다.
@WithUserDetails(userDetailsServiceBeanName = "userService")
@Test
public void TEST_001_updateRepository() {
Repository repository = new Repository();
repository.setId(UUID.randomUUID().toString());
boolean updated = repositoryService.update(repository);
LOG.info("updated : {}", updated);
}
만약, 다음과 같이 저장소를 삭제할 때 저장소의 이름과 같은지 비교하기 위한 검증 함수를 만들었을때 저장소의 이름이 null
인 경우 삭제 함수를 통과하지 않도록 방지합니다.
public boolean ownerWithName(Authentication authentication, String id, String name) {
if(id == null || name == null) {
LOG.error("id or name is null");
return false;
}
return true;
}
@PreAuthorize("@repositoryChecker.ownerWithName(authentication, #id, #name)")
public boolean delete(String id, String name) {
LOG.info("Call delete method successfully!");
return true;
}
@WithUserDetails(userDetailsServiceBeanName = "userService")
@Test(expected = AccessDeniedException.class)
public void TEST_002_deleteRepositoryWithNameIsNull() {
boolean deleted = repositoryService.delete("dummy", null);
LOG.info("deleted : {}", deleted);
}
위 테스트 케이스에서 name
매개변수가 null
이므로 AccessDeniedException
이 발생합니다.
메소드 보안 표현식에 대한 테스트를 적용하기 위해서 @SpringBootTest
를 이용하지 않아도 됩니다.
스프링 테스트 모듈은 JdbcTest
와 같은 테스트 환경을 구성하기 위한 어노테이션을 제공합니다. 이를 이용해서 스프링 시큐리티 설정을 적용한 메타 어노테이션을 구성할 수 있습니다.
다음은 Web Security
에서 사용한 서비스 단위 테스트 어노테이션 예시 입니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@JdbcTest(excludeAutoConfiguration = TestDatabaseAutoConfiguration.class)
@ImportAutoConfiguration(classes = {
SecurityAutoConfiguration.class
})
@EnableConfigurationProperties({ ResourceProperties.class })
@ComponentScan(basePackages = {
"kr.kdev.demo.service",
"kr.kdev.demo.repository"
})
@ContextConfiguration(classes = {
SecurityConfig.class
})
public @interface ServiceTest {
}
스프링 테스트 모듈에서 제공하는 @JdbcTest
를 기반으로 SecurityAutoConfiguration
을 활성화하여 스프링 시큐리티 기본 설정을 적용하고 ContextConfiguration
을 통해 보안 설정 클래스를 적용하였습니다.
그러면 @ServiceTest
로 스프링 시큐리티를 적용한 서비스 단위 테스트를 진행할 수 있습니다.
@ActiveProfiles({"test"})
@RunWith(SpringJUnit4ClassRunner.class)
@ServiceTest
public class ServiceTestConfigurer {
protected Logger LOG = LoggerFactory.getLogger(getClass());
protected SecurityContext securityContext;
@Before
public void setUp() {
this.securityContext = SecurityContextHolder.getContext();
}
}
스프링 시큐리티 테스트 모듈에는 MVC 테스트를 쉽게 만들 수 있게 다양한 구현체를 제공합니다.
- SecurityMockMvcRequestPostProcessors
- SecurityMockMvcRequestBuilders
- SecurityMockMvcResultMatchers
SecurityMockMvcRequestBuilders를 통해 폼 로그인 테스트를 지원합니다.
@Test
public void TEST_000_formLogin() throws Exception {
MvcResult mvcResult = mvc.perform(SecurityMockMvcRequestBuilders.formLogin().user("admin").password("password"))
.andExpect(SecurityMockMvcResultMatchers.authenticated())
.andDo(MockMvcResultHandlers.print())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
int status = response.getStatus();
Assert.assertEquals(302, status);
}
SecurityMockMvcResultMatchers를 통해 결과에 대한 테스트를 지원합니다.
다음과 같이 잘못된 비밀번호로 로그인 했을 때 실패 결과를 확인할 수 있습니다.
@Test
public void TEST_000_formLoginInvalidPassword() throws Exception {
mvc.perform(SecurityMockMvcRequestBuilders.formLogin().user("admin").password("admin"))
.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
}
SecurityMockMvcRequestBuilders를 통해 로그아웃 테스트를 지원합니다.
@Test
public void TEST_002_logout() throws Exception {
mvc.perform(SecurityMockMvcRequestBuilders.logout())
.andDo(MockMvcResultHandlers.print());
}