可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am having trouble trying to test a REST endpoint that receives an UserDetails
as a parameter annotated with @AuthenticationPrincipal.
It seems like the user instance created in the test scenario is not being used, but an attempt to instantiate using default constructor is made instead: 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
? I want to write tests having total control of service mocks, so I am using standaloneSetup.
回答1:
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.
回答2:
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!
...
}
}
回答3:
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
回答4:
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();
}