인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지

작성한 질문수

스프링부트 테스트에서 @AuthenticationPrincipal UserDetails userDetails 테스트

24.02.12 14:23 작성

·

797

·

수정됨

0

컨트롤러 테스트에서 @AuthenticationPrincipal UserDetails userDetails에 해당하는 부분을 테스트하려고 하는데 계속 실패해서 질문드립니다 ㅠㅠ

 

    // 회원 탈퇴
    @DeleteMapping("/{memberId}")
    @Tag(name = "member")
    @Operation(summary = "삭제 API", description = "유저를 삭제하는 API입니다.")
    @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
    public String remove(@PathVariable Long memberId,
                         @AuthenticationPrincipal UserDetails userDetails) {
        try {
            String email = userDetails.getUsername();
            log.info("email : " + email);
            String remove = memberService.removeUser(memberId, email);
            return remove;
        } catch (Exception e) {
            return "회원탈퇴 실패했습니다. :" + e.getMessage();
        }
    }

이런식으로 프론트가 헤더에 accessToken을 보내주면 검증을 한 후 @AuthenticationPrincipal UserDetails userDetails이거로 정보를 가져와서 권한과 해당 유저인지 체크 후 처리하는 로직을 구성하고 있는데 이거를 테스트할 때 막혔습니다.

일단 구글에서 검색해서 나온 방법들을 다 사용해도 실패가 뜨네요.

↓구글에서 나온 방법 적용

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAuthUserSecurityContextFactory.class)
public @interface WithAuthUser {
    String user() default "test@test.com";
    String role() default "USER";
}
public class WithAuthUserSecurityContextFactory implements WithSecurityContextFactory<WithAuthUser> {

    @Override
    public SecurityContext createSecurityContext(WithAuthUser annotation) {
        String email = annotation.user();
        String role = annotation.role();

        MemberEntity member = MemberEntity.builder()
                .memberId(1L)
                .email(email)
                .memberPw("dudtjq8990!")
                .memberName("테스터")
                .memberRole(Role.valueOf(role))
                .nickName("테스터")
                .memberPoint(0)
                .provider(null)
                .providerId(null)
                .address(AddressEntity.builder()
                        .memberAddr("서울시 강남구")
                        .memberZipCode("103-332")
                        .memberAddrDetail("102")
                        .build())
                .build();

        UserDetails userDetails = new PrincipalDetails(member);

        Authentication authentication =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        return context;
    }
}
    @Test
    @WithAuthUser
    void remove() throws Exception {
        Long id = 1L;
        String email = "test@test.com";

        mockMvc.perform(delete("/api/v1/users/{memberId}", id)
                        .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());

        verify(memberService).removeUser(id, email);
    }

 

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.security.core.userdetails.UserDetails

 

이런 오류가 발생합니다. 찾아보니까 UserDetails를 구현한 클래스 PrincipalDetails에 생성자가 없어서 나오는 오류라고 하는데 문제는 해당 클래스에 기본 생성자가 있다는 것입니다. 생성자가 있는데 계속 없다고 에러가 발생합니다. @ㅇNoArgsConstructorㅇ

답변 1

0

인프런 AI 인턴님의 프로필 이미지

2024. 02. 13. 14:27

안녕하세요, 인프런 AI 인턴입니다.

UserDetails 관련한 PrincipalDetails 클래스에서 기본 생성자가 있다고 하셨지만, 스프링 시큐리티의 테스트에서 UserDetails 객체를 사용하기 위해서는 UserDetails를 생성하여 Authentication 객체에 설정하고 이를 SecurityContext에 주입하는 과정이 필요합니다.

@WithMockUser 어노테이션을 사용하여 UserDetails 객체를 테스트 내에서 간단하게 모의할 수 있습니다. @WithMockUser 어노테이션은 스프링 시큐리티 테스트 컨텍스트에 모의 사용자를 추가해주며, 이 사용자는 미리 정의된 권한을 가지고 있는 것으로 인식됩니다. 위에서 제공된 정보에 따르면 @WithMockUser 예시 사용법은 다음과 같습니다:

@Test
@WithMockUser(roles = "ADMIN")
void requestProtectedUrlWithUser() throws Exception {
    this.mvc.perform(get("/"));
}

위 코드는 ADMIN 역할을 가진 가짜 사용자를 이용하여 URL을 요청하는 테스트를 실행하게 됩니다. 이를 바탕으로, 여러분의 코드에서 @AuthenticationPrincipal로 주입받은 UserDetails 객체를 테스트하려면, 테스트 메서드에서 @WithMockUser를 사용하여 알맞은 권한과 함께 테스트를 진행할 수 있습니다. 권한을 “ROLE_USER” 또는 “ROLE_ADMIN”으로 설정하여 테스트를 진행해 보십시오.

예를 들어 다음과 같이 테스트를 수정할 수 있습니다:

@Test
@WithMockUser(username = "test@test.com", roles = "USER") // 혹은 "ADMIN" 권한으로 테스트
void remove() throws Exception {
    Long id = 1L;

    mockMvc.perform(delete("/api/v1/users/{memberId}", id)
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk());

    verify(memberService).removeUser(id, "test@test.com");
}

@WithMockUser로 인해 현재의 Authentication은 모의 UserDetails 객체로 채워질 것이며, 이 객체는 요청 처리 동안 사용되어 여러분이 구현한 로직을 테스트할 수 있습니다. 만약 PrincipalDetails 클래스에 특별한 구성 요소가 없다면, 테스트 작성에 있어서 @WithMockUser를 사용하는 것으로 충분합니다.

추가적으로 PrincipalDetails 클래스에 대한 문제가 여전히 발생한다면, PrincipalDetails 클래스의 생성자가 올바르게 정의되어 있고, 해당 클래스가 UserDetails 인터페이스를 올바르게 구현하고 있는지 확인해야 합니다. 생성자의 문제가 실제로 해결되었는지 다시 한번 검토해 주세요.

참고하실 수 있도록 해당 내용에 관련된 문서의 링크를 첨부합니다.

질문에 대한 답변이 되길 바랍니다. 추가적인 질문이 있으시면 언제든지 문의해 주세요.