[테스트] 컨트롤러 (Controller) 테스트 할 때 로그인한 사용자가 필요한 API일 경우 테스트(Spring Security)

손경이·2024년 2월 20일
1

2024.02.18
[테킷 백엔드] 프로젝트 - alcoholfriday
환경 - 스프링부트 3.2.1, 자바 JDK17
작업 - 장바구니에 상품 추가, 조회 테스트


💡 컨트롤러에서 로그인한 사용자가 필요한 API일 경우 테스트(Spring Security)

- @WithMockUser, @WithUserDetails, @WithSecurityContext 3가지 중 하나를 사용하면 된다.

  • @WithMockUser
    • UserPrincipal에 값이 들어가는 것이 아닌 '로그인 했다 치고' 가정으로, Controller에서 UserPrincipal의 정보를 안 쓰면 사용해도 된다.
  • @WithUserDetails
    • 해당 어노테이션은 @BeforeEach가 실행 되기 전에 @WithUserDetails가 바로 실행되서 사용을 안했다.
    • 그런데 나중에 찾아보니깐 setupBefore 옵션을 설정하여 @WithUserDetails가 실행되기 전에 @BeforeEach가 실행될 수 있도록 설정할 수 있다고 한다.
    @WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
    • 이거를 쓰면 된다고 하는데 제가 사용 해보지 않아서 자세한 설명은 아직 드리지 못하겠습니다.
  • @WithSecurityContext
    • 그리고 제가 사용한 어노테이션 @WithSecurityContext
    • @WithSecurityContext()를 사용해서 어노테이션을 만드는 것이다.
    • 임의로 우리 환경과 맞는 테스트 UserPrincipal를 사용할 수 있다. (WithAccountSecurityContextFactory.class에 작성)
    • 컨트롤러 테스트를 할 때 UserPrincipal를 테스트에 사용해야 하는 경우, 직접 만든 어노테이션을 사용하여 원하는 테스트용 UserPrincipal를 설정할 수 있다.
    • 특정 사용자의 인증 정보를 시뮬레이션하기 위해 @WithAccount 어노테이션을 사용할 수 있습니다.
      (저의 예시로는 @WithAccount 입니다.)

- 전체 코드

  • @WithAccount (어노테이션 이름은 자유롭게 자신이 원하는 대로 지으면 된다.)
import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
    String email() default "test@example.com";
}
  • WithAccountSecurityContextFactory 클래스
import com.drunkenlion.alcoholfriday.domain.auth.enumerated.ProviderType;
import com.drunkenlion.alcoholfriday.domain.member.dao.MemberRepository;
import com.drunkenlion.alcoholfriday.domain.member.entity.Member;
import com.drunkenlion.alcoholfriday.domain.member.enumerated.MemberRole;
import com.drunkenlion.alcoholfriday.global.security.auth.UserDetailsServiceImpl;
import com.drunkenlion.alcoholfriday.global.security.auth.UserPrincipal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

import java.time.LocalDateTime;

public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {
    @Autowired
    MemberRepository memberRepository;
    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Override
    public SecurityContext createSecurityContext(WithAccount annotation) {
        Member member = Member.builder()
                .email(annotation.email())
                .provider(ProviderType.KAKAO)
                .name("테스트")
                .nickname("test")
                .role(MemberRole.MEMBER)
                .phone(1012345678L)
                .certifyAt(null)
                .agreedToServiceUse(true)
                .agreedToServicePolicy(true)
                .agreedToServicePolicyUse(true)
                .createdAt(LocalDateTime.now())
                .updatedAt(null)
                .deletedAt(null)
                .build();

        memberRepository.save(member);

        UserPrincipal userPrincipal = UserPrincipal.create(member);

        Authentication authentication =
                new UsernamePasswordAuthenticationToken(
                        userPrincipal, null, userPrincipal.getAuthorities()
                );
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);

        return context;
    }
}
  • 컨트롤러 API
@PostMapping
    public ResponseEntity<CartResponse> addCartList(@RequestBody CartReqList cartReqList, @AuthenticationPrincipal UserPrincipal userPrincipal) {
        CartResponse cartResponse = cartService.addCartList(cartReqList.getCartRequestList(), userPrincipal.getMember());

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(cartResponse.getCartId())
                .toUri();

        return ResponseEntity.created(location).body(cartResponse);
    }
  • 테스트 사용 예시
	@Test
    @DisplayName("장바구니에 한 개 상품 등록")
    @WithAccount
    void addCartOneItem() throws Exception {
        // when
        ResultActions resultActions = mvc
                .perform(post("/v1/carts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .characterEncoding("UTF-8")
                        .content("""
                                {
                                  "cartRequestList": [
                                    {
                                      "itemId": "%d",
                                      "quantity": "2"
                                    }
                                  ]
                                }
                                """.formatted(itemId))
                )
                .andDo(print());

        // then
        resultActions
                .andExpect(status().isCreated())
                .andExpect(handler().handlerType(CartController.class))
                .andExpect(handler().methodName("addCartList"))
                .andExpect(jsonPath("$", instanceOf(LinkedHashMap.class)))
                .andExpect(jsonPath("$.cartId", instanceOf(Number.class)))
                .andExpect(jsonPath("$.cartDetailResponseList[0].item.id", instanceOf(Number.class)))
                .andExpect(jsonPath("$.cartDetailResponseList[0].quantity", notNullValue()));
    }

참고

0개의 댓글