简体   繁体   English

在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal

[英]Inject @AuthenticationPrincipal when unit testing a Spring REST controller

I am having trouble trying to test a REST endpoint that receives an UserDetails as a parameter annotated with @AuthenticationPrincipal.我在尝试测试接收UserDetails作为用@AuthenticationPrincipal.

It seems like the user instance created in the test scenario is not being used, but an attempt to instantiate using the default constructor is made instead: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;似乎没有使用在测试场景中创建的用户实例,而是尝试使用默认构造函数进行实例化: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;

REST endpoint:休息端点:

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

Test class:测试类:

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

}

How can I solve that problem without having to switch to webAppContextSetup ?如何在不必切换到webAppContextSetup的情况下解决该问题? I want to write tests having total control of service mocks, so I am using standaloneSetup.我想编写完全控制服务模拟的测试,所以我正在使用standaloneSetup.

This can be done by injection a HandlerMethodArgumentResolver into your Mock MVC context or standalone setup.这可以通过将HandlerMethodArgumentResolver注入您的 Mock MVC 上下文或独立设置来完成。 Assuming your @AuthenticationPrincipal is of type ParticipantDetails :假设您的@AuthenticationPrincipalParticipantDetails类型:

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.这个参数解析器可以处理ParticipantDetails类型并且只是凭空创建它,但是你会看到你得到了很多上下文。 Later on, this argument resolver is attached to the mock MVC object:稍后,此参数解析器附加到模拟 MVC 对象:

@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.这将导致您的@AuthenticationPrincipal注释方法参数被您的解析器的详细信息填充。

For some reason Michael Piefel's solution didn't work for me so I came up with another one.出于某种原因,Michael Piefel 的解决方案对我不起作用,所以我想出了另一个。

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并且@AuthenticationPrincipal 应该将您自己的 User 类实现注入到控制器方法中

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

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 .它没有很好的文档记录,但是有一种方法可以将Authentication对象作为 MVC 方法的参数注入到独立的 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.如果您在SecurityContextHolder设置Authentication ,过滤器SecurityContextHolderAwareRequestFilter通常由 Spring Security 实例化,并为您注入 auth。

You simply need to add that filter to your MockMvc setup, like this:您只需将该过滤器添加到您的 MockMvc 设置中,如下所示:

@Before
public void before() throws Exception {
    SecurityContextHolder.getContext().setAuthentication(myAuthentication);
    SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
    authInjector.afterPropertiesSet();
    mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
}

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")我知道这个问题很老,但对于仍在寻找的人来说,使用@AuthenticationPrincipal编写 Spring Boot 测试对我@AuthenticationPrincipal (这可能不适用于所有实例),是注释测试@WithMockUser("testuser1")

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

Here is a link to the Spring documentation on @WithMockUser 这是@WithMockUser上 Spring 文档的链接

Simplification of @pzeszko answer : @pzeszko 答案的简化:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
public class ControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails(value = "user@gmail.com")
    void get() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(URL))
                .andExpect(status().isOk())
                .andDo(print());
    }

See:见:

This solution worked for me and I found it really handy.这个解决方案对我有用,我发现它真的很方便。

Create a TestIUserDetails service that implements UserDetailsServce in test package:test包中创建一个实现 UserDetailsS​​ervce 的TestIUserDetails服务:

@Service
@Primary
@Profile("test")
public class TestIUserDetails implements UserDetailsService {
public static final String ADMIN_USERNAME = "admin@example.com";
    public static final String USERNAME = "user@example.com";

    private User getUser() {
        User user = new User();
        user.setEmail(USERNAME);
        user.setId(1L);
        return user;
    }
    ...
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (Objects.equals(username, ADMIN_USERNAME))
            return getAdminUser();
        else if (Objects.equals(username, USERNAME))
            return getUser();
        return getPublicUser();
    }
}

Now, on your test:现在,在您的测试中:

@SpringMockWebEnvTestConfig
class AbcControllerTest {
    @Autowired
    private MockMvc mvc;
    @Autowired
    UserDetailsService userDetailsService;
    private User user;

    @BeforeEach
    void setUp() {
        user = (User) userDetailsService.loadUserByUsername(TestUserDetailsImpl.USERNAME);
    }

   @Test
   public void testAbc(){
     this.mvc.perform(post(endpoint).with(user(user))
     ...
     .andExpect(status().isCreated())...
   }
}

My @AuthenticationPrincipal is expecting a Jwt, so had to implement a custom argument resolver that implements HandlerMethodArgumentResolver.我的@AuthenticationPrincipal 需要一个 Jwt,因此必须实现一个实现 HandlerMethodArgumentResolver 的自定义参数解析器。

public class JwtArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(Jwt.class);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {

        var jwtToken = Jwt.withTokenValue("token")
                .header("alg", "none")
                .claim("sub", "user")
                .claim("scope", "read").build();
        return jwtToken;
    }
}

In test:在测试中:

@BeforeAll
public void setup() {
    MockitoAnnotations.initMocks(this);
    mockMvc =  MockMvcBuilders.standaloneSetup(myController).setCustomArgumentResolvers(new JwtArgumentResolver()).build();
}

And the mockMvc.perform can be used without passing a RequestPostProcessor .并且 mockMvc.perform 可以在不传递RequestPostProcessor的情况下使用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM