복붙노트

[SPRING] Spring REST 컨트롤러를 테스트 할 때 @AuthenticationPrincipal 삽입

SPRING

Spring REST 컨트롤러를 테스트 할 때 @AuthenticationPrincipal 삽입

@AuthenticationPrincipal 주석이있는 매개 변수로 UserDetails를받는 나머지 끝점을 테스트하는 데 문제가 있습니다.

테스트 시나리오에서 생성 된 사용자 인스턴스가 사용되지 않는 것처럼 보이지만 대신 기본 생성자를 사용하여 인스턴스를 생성하려고 시도합니다. org.springframework.beans.BeanInstantiationException : [com.andrucz.app.AppUserDetails]의 인스턴스 생성에 실패했습니다 : 기본값 없음 생성자가 발견되었습니다.

REST 엔드 포인트 :

@RestController
@RequestMapping("/api/items")
class ItemEndpoint {

    @Autowired
    private ItemService itemService;

    @RequestMapping(path = "/{id}",
                    method = RequestMethod.GET,
                    produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Callable<ItemDto> getItemById(@PathVariable("id") String id, @AuthenticationPrincipal AppUserDetails userDetails) {
        return () -> {
            Item item = itemService.getItemById(id).orElseThrow(() -> new ResourceNotFoundException(id));
            ...
        };
    }
}

테스트 클래스 :

public class ItemEndpointTests {

    @InjectMocks
    private ItemEndpoint itemEndpoint;

    @Mock
    private ItemService itemService;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(itemEndpoint)
                .build();
    }

    @Test
    public void findItem() throws Exception {
        when(itemService.getItemById("1")).thenReturn(Optional.of(new Item()));

        mockMvc.perform(get("/api/items/1").with(user(new AppUserDetails(new User()))))
                .andExpect(status().isOk());
    }

}

webAppContextSetup으로 전환하지 않고도이 문제를 어떻게 해결할 수 있습니까? standaloneSetup을 사용하고 있으므로 서비스 모의를 완벽하게 제어하는 ​​테스트를 작성하고 싶습니다.

해결법

  1. ==============================

    1.이것은 Mock MVC 컨텍스트 또는 독립 실행 형 설정에 HandlerMethodArgumentResolver를 삽입하여 수행 할 수 있습니다. @AuthenticationPrincipal이 ParticipantDetails 유형이라고 가정합니다.

    이것은 Mock MVC 컨텍스트 또는 독립 실행 형 설정에 HandlerMethodArgumentResolver를 삽입하여 수행 할 수 있습니다. @AuthenticationPrincipal이 ParticipantDetails 유형이라고 가정합니다.

    private HandlerMethodArgumentResolver putAuthenticationPrincipal = new HandlerMethodArgumentResolver() {
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.getParameterType().isAssignableFrom(ParticipantDetails.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            return new ParticipantDetails(…);
        }
    };
    

    이 인수 분석자는 ParticipantDetails 유형을 처리 할 수 ​​있으며이를 단순한 외부에서 생성하지만, 많은 컨텍스트를 얻게됩니다. 나중에이 인수 해석자는 mock MVC 객체에 연결됩니다.

    @BeforeMethod
    public void beforeMethod() {
        mockMvc = MockMvcBuilders
                .standaloneSetup(…)
                .setCustomArgumentResolvers(putAuthenticationPrincipal)
                .build();
    }
    

    이렇게하면 @AuthenticationPrincipal 주석이 달린 메서드 인수에 리졸버의 세부 정보가 채워집니다.

  2. ==============================

    2.어떤 이유로 나는 Michael Piefel의 해결책이 나에게 도움이되지 않았기 때문에 또 하나를 생각해 냈습니다.

    어떤 이유로 나는 Michael Piefel의 해결책이 나에게 도움이되지 않았기 때문에 또 하나를 생각해 냈습니다.

    우선, 추상적 인 구성 클래스를 만듭니다.

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        WithSecurityContextTestExecutionListener.class})
    public abstract MockMvcTestPrototype {
    
        @Autowired
        protected WebApplicationContext context;
    
        protected MockMvc mockMvc;
    
        protected org.springframework.security.core.userdetails.User loggedUser;
    
        @Before
        public voivd setUp() {
             mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    
            loggedUser =  (User)  SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        } 
    }
    

    그런 다음 아래와 같은 테스트를 작성할 수 있습니다.

    public class SomeTestClass extends MockMvcTestPrototype {
    
        @Test
        @WithUserDetails("someUser@app.com")
        public void someTest() throws Exception {
            mockMvc.
                    perform(get("/api/someService")
                        .withUser(user(loggedUser)))
                    .andExpect(status().isOk());
    
        }
    }
    

    @AuthenticationPrincipal은 자신의 User 클래스 구현을 컨트롤러 메소드에 삽입해야합니다.

    public class SomeController {
    ...
        @RequestMapping(method = POST, value = "/update")
        public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user) {
            ...
            user.getUser(); // works like a charm!
           ...
        }
    }
    
  3. ==============================

    3.질문은 오래되었지만 여전히 찾고있는 사람들은 @AuthenticationPrincipal (그리고 모든 인스턴스에서 작동하지 않을 수도 있음)을 사용하여 Spring Boot 테스트를 작성하고, @WithMockUser ( "testuser1") 테스트에 주석을 달았습니다.

    질문은 오래되었지만 여전히 찾고있는 사람들은 @AuthenticationPrincipal (그리고 모든 인스턴스에서 작동하지 않을 수도 있음)을 사용하여 Spring Boot 테스트를 작성하고, @WithMockUser ( "testuser1") 테스트에 주석을 달았습니다.

    @Test
    @WithMockUser("testuser1")
    public void successfullyMockUser throws Exception {
        mvc.perform(...));
    }
    

    @WithMockUser에 대한 Spring 문서에 대한 링크가있다.

  4. ==============================

    4.잘 설명되어 있지 않지만 독립 객체 MockMvc에 MVC 메서드의 매개 변수로 Authentication 객체를 삽입하는 방법이 있습니다. SecurityContextHolder에서 Authentication을 설정하면, SecurityContextHolderAwareRequestFilter 필터는 대개 Spring Security에 의해 인스턴스화되고 인증을 주입합니다.

    잘 설명되어 있지 않지만 독립 객체 MockMvc에 MVC 메서드의 매개 변수로 Authentication 객체를 삽입하는 방법이 있습니다. SecurityContextHolder에서 Authentication을 설정하면, SecurityContextHolderAwareRequestFilter 필터는 대개 Spring Security에 의해 인스턴스화되고 인증을 주입합니다.

    다음과 같이 MockMvc 설정에 필터를 추가하기 만하면됩니다.

    @Before
    public void before() throws Exception {
        SecurityContextHolder.getContext().setAuthentication(myAuthentication);
        SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
        authInjector.afterPropertiesSet();
        mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
    }
    
  5. from https://stackoverflow.com/questions/38330597/inject-authenticationprincipal-when-unit-testing-a-spring-rest-controller by cc-by-sa and MIT license