Inject @AuthenticationPrincipal when unit testing a Spring REST controller

后端 未结 4 1637
不知归路
不知归路 2021-01-03 23:04

I am having trouble trying to test a REST endpoint that receives an UserDetails as a parameter annotated with @AuthenticationPrincipal.

It

相关标签:
4条回答
  • 2021-01-03 23:31

    It's not well documented but there's a way to inject the Authentication object as parameter of your MVC method in a standalone MockMvc. If you set the Authentication in the SecurityContextHolder, the filter SecurityContextHolderAwareRequestFilter is usually instantiated by Spring Security and makes the injection of the auth for you.

    You simply need to add that filter to your MockMvc setup, like this:

    @Before
    public void before() throws Exception {
        SecurityContextHolder.getContext().setAuthentication(myAuthentication);
        SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
        authInjector.afterPropertiesSet();
        mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
    }
    
    0 讨论(0)
  • 2021-01-03 23:34

    This can be done by injection a HandlerMethodArgumentResolver into your Mock MVC context or standalone setup. Assuming your @AuthenticationPrincipal is of type 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(…);
        }
    };
    

    This argument resolver can handle the type ParticipantDetails and just creates it out of thin air, but you see you get a lot of context. Later on, this argument resolver is attached to the mock MVC object:

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

    This will result in your @AuthenticationPrincipal annotated method arguments to be populated with the details from your resolver.

    0 讨论(0)
  • 2021-01-03 23:34

    I know the question is old but for folks still looking, what worked for me to write a Spring Boot test with @AuthenticationPrincipal (and this may not work with all instances), was annotating the test @WithMockUser("testuser1")

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

    Here is a link to the Spring documentation on @WithMockUser

    0 讨论(0)
  • 2021-01-03 23:56

    For some reason Michael Piefel's solution didn't work for me so I came up with another one.

    First of all, create abstract configuration class:

    @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();
        } 
    }
    

    Then you can write tests like this:

    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());
    
        }
    }
    

    And @AuthenticationPrincipal should inject your own User class implementation into controller method

    public class SomeController {
    ...
        @RequestMapping(method = POST, value = "/update")
        public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user) {
            ...
            user.getUser(); // works like a charm!
           ...
        }
    }
    
    0 讨论(0)
提交回复
热议问题