Spring Security Authorized Test
@WithMockUser, @WithAnonymousUser는 실제 DB의 데이터가 아닌 username, password, role 등의 간단한 테스트 일 때 사용하고,구현한 UserDetails로 인증정보를 관리하고 있을 때는 @WithUserDetails를 통해 인가된 사용자에 대한 테스트가 가능하다.
@WithUserDetails
Test시 구현한 UserDetails를 UserDetailsService.loadUserByUsername을 통해 Security Context에 넣어준다. 유저를 DB에 저장하는 로직을 @BeforeEach에 위치할 때 @WithUserDetails가 @BeforeEach보다 빠르게 동작해서 DB에 유저를 저장하기 전에 불러오기 때문에 setupBefore 옵션에 TestExecutionEvent.TEST_EXECUTION 선택해야한다.
@WithSecurityContext
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithUserDetailsSecurityContextFactory.class)
public @interface WithUserDetails {
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
String value();
}
@WithUserDetails에 포함되는 어노테이션이지만 직접 Security Context에 테스트에 활용한 인증정보를 넣어주기 위해 커스텀 어노테이션을 생성할 때 사용할 수 있다.
WithSecurityContextFactory
@RequiredArgsConstructor
public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {
private final AccountService accountService;
@Override
public SecurityContext createSecurityContext(WithAccount withAccount) {
String nickname = withAccount.value();
SignUpForm signUpForm = new SignUpForm();
signUpForm.setNickname(nickname);
signUpForm.setEmail("email@email.com");
signUpForm.setPassword("1q2w3e4r");
accountService.signUp(signUpForm);
UserDetails principal = accountService.loadUserByUsername(nickname);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal,
principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
@WithSecurityContext에서 사용하는 팩토리는 WithSecurityContextFactory를 구현해서 만들고 커스텀 어노테이션을 제네릭으로 설정해 미리 설정한 value를 사용할 수 있다. Empty SecuirtyContext를 만들고 authentication을 설정하고 context를 반환하는게 메인로직이며 해당 방식이 @WithUserDetails의 동작방식이다. 해당 어노테이션 사용시 UserDetailService에 저장하는 로직을 위에 위치시켜 @BeforeEach로 유저관리를 안해도되지만 @AfterEach로 유저정보를 지워줘야한다.
@Test
@DisplayName("프로필 수정 - 입력값 정상")
//@WithAccount("bebe")
//@WithUserDetails(value = "bebe", setupBefore = TestExecutionEvent.TEST_EXECUTION)
public void updateProfile() throws Exception {
String changeBio = "짧은 소개";
mockMvc.perform(post(SETTINGS_PROFILE_URL)
.param("bio", changeBio)
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl(SETTINGS_PROFILE_URL))
.andExpect(flash().attributeExists("message"));
Account bebe = accountRepository.findByNickname("bebe");
assertEquals(changeBio, bebe.getBio());
}
@BeforeEach // WithUserDetails 사용시
public void beforeEach(){
String nickname = "bebe";
SignUpForm signUpForm = new SignUpForm();
signUpForm.setNickname(nickname);
signUpForm.setEmail("email@email.com");
signUpForm.setPassword("1q2w3e4r");
accountService.signUp(signUpForm);
}
@AfterEach
public void afterEach(){
accountRepository.deleteAll();
}
RedirectAttributes
Redirect시 원하는 값을 전달할 수 있는 Model
무언가 업데이트됬지만 같은 창을 보여주는 불편한 UX를 해결할 수 있다.
// Controller
@PostMapping(SETTINGS_PROFILE_URL)
public String updateProfile(@CurrentAccount Account account, @Valid @ModelAttribute Profile profile,
Errors errors, Model model, RedirectAttributes attributes){
if(errors.hasErrors()){
model.addAttribute(account);
return SETTINGS_PROFILE_VIEW_NAME;
}
accountService.updateProfile(account, profile);
attributes.addFlashAttribute("message", "프로필 수정했습니다.");
return "redirect:" + SETTINGS_PROFILE_URL;
}
FlasAttribute는 Redirect시 한번만 사용하고 휘발되는 Attribute이다.
// View
<div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
<span th:text="${message}">메시지</span>
</div>
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard