@WithCustomMockUser으로 인증된 객체를 만들어 테스트하기

박준수·2023년 9월 30일
0

이것저것

목록 보기
6/9
post-custom-banner

DiaryController

@Tag(name = "Diary", description = "일기 API")
@RestController
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@RequestMapping("/diaries")
public class DiaryController {

    private final DiaryService diaryService;
    private final S3UploadService s3UploadService;

    @Operation(summary = "일기 등록")
    @PostMapping
    public ResponseEntity<ResultResponse> createDiary(@AuthenticationPrincipal UserDetail user,
                                                      @RequestPart("file") MultipartFile multipartFile,
                                                      @Valid @RequestPart(value="createRequest") DiaryCreateReq createRequest) throws IOException {
        String drawingUrl = s3UploadService.saveFile(multipartFile, user.getUsername());
        diaryService.createDiary(user, createRequest, drawingUrl);
        return ResponseEntity.ok(ResultResponse.of(ResultCode.DIARY_CREATE_SUCCESS));
    }
}
  • 다음과 같이 사용자가 일기에 대한 CRUD를 실행하는 Controller 메서드에는 @AuthenticationPrincipal UserDetail user 이 매개 변수로 붙여져 있습니다.
  • Spring Security + JWT를 이용한 인증방법에서 정상적인 토큰이라면 커스텀한 UserDetail 객체를 가져올 수 있습니다

@AuthenticationPrincipal

CustomUserDetailsService 의 loadUserByUsername이라는 메서드가 return한 객체를 파라미터로 받아 사용할 수 있도록 해주는 어노테이션

@Service
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User principal = userRepository.findUserByEmail(email)
                .orElseThrow(UserNotFoundException::new);

        return new UserDetail(principal);
    }
}

@AuthenticationPrincipal UserDetail user가 사용된 Controller 메서드에 대한 테스트 코드를 작성해보자.

Spring Security가 적용된 곳을 효율적으로 테스트하자.

  • 이 블로그에서 여러가지 테스트 하는 방식이 있다는 것을 알 수 있었다.


@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDetail implements UserDetails {
    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collections = new ArrayList<>();
        collections.add(() -> "ROLE_" + user.getRole());

        return collections;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public Long getUserId(){
        return user.getId();
    }
}
  • @WithMockUser를 사용하면 Spring security.core.userdetails의 User객체가 들어가기 때문에 직접 생성한 특정 User 객체를 이용할 때는 Error가 발생합니다.

  1. core user에는 usrname, password, role만이 들어가 있기 때문이기도하고
  2. @AuthenticationPrincipal 로 인해 바인딩 되는 과정에서, 내가 주입받고자하는 instance 타입이 아니라면 바인딩이 되지 않기 때문입니다. → null을 반환

@WithMockUser 에 Custom User 객체를 주입하는 방법

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomMockUserSecurityContextFactory.class)
public @interface WithCustomMockUser {
    String userUuid() default "test@email";
    String role() default "USER";
}
  • @WithSecurityContext 어노테이션을 사용하면 사용할 SecurityContext를 지정해줄 수 있습니다. 즉 어떤 객체로 바인딩 할 것인지를 직접 만들어서 적용할 수 있습니다.

public class WithCustomMockUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {

    @Override
    public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
        String userEmail = annotation.userUuid();
        String role = annotation.role();

        User user = User.builder()
                .email(userEmail)
                .nickname("테스트 유저")
                .password("Test012@")
                .role(Role.valueOf(role))
                .build();
        UserDetail userDetail = UserDetail.builder().user(user).build();

        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(userDetail, "password", List.of(new SimpleGrantedAuthority(role)));
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(token);
        return context;
    }
}
  • 사용할 SecurityContext를 정의해줄 클래스입니다.
  • WithSecuriryContextFactory 제네릭 바운디드 타입이 Annotation이므로 위에서 만들어준 WithMockUser를 타입으로 넣어줍니다.

Success

@WebMvcTest(DiaryController.class)
@ExtendWith(RestDocumentationExtension.class)
@DisplayName("Diary 컨트롤러에 ")
class DiaryControllerTest extends MockApiTest {

    @MockBean
    private DiaryService diaryService;
    @MockBean
    private S3UploadService s3UploadService;

    @Test
    @WithCustomMockUser
    @DisplayName("일기가 등록될 수 있다.")
    void createDiary() throws Exception {
        //given
        MockMultipartFile file = new MockMultipartFile(
                "file",      // 파라미터 이름
                "file_name", // 파일 이름
                "image/jpeg",// 파일 타입
                "test file".getBytes(StandardCharsets.UTF_8) // 파일 내용
        );
        given(s3UploadService.saveFile(any(), any())).willReturn(String.valueOf(file));

        DiaryCreateReq createReq = DiaryControllerFixture.CREATE_REQ;
        String jsonByCreateReq = objectMapper.writeValueAsString(createReq);
        MockMultipartFile request = new MockMultipartFile(
                "createRequest",
                "createRequest",
                "application/json",
                jsonByCreateReq.getBytes(StandardCharsets.UTF_8)
        );
        String token = "accessToken";
        diaryService.createDiary(any(), any(), any());

        //when
        ResultActions perform =
                mockMvc.perform(
                        multipart("/diaries")
                                .file(file)
                                .file(request)
                                .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                );

        // then
        perform.andExpect(status().isOk());

        // docs
        perform.andDo(print())
                .andDo(document("register diary",
                        getDocumentRequest(),
                        getDocumentResponse()));

    }

참고

[Spring Security] @AuthenticationPrincipal 유닛 테스트 - Custom Mock User 삽입하기

profile
방구석개발자
post-custom-banner

0개의 댓글