Ich habe Probleme beim Versuch, einen Restendpunkt zu testen, der eine UserDetails
als mit @AuthenticationPrincipal
kommentierten Parameter erhält.
Anscheinend wird die im Testszenario erstellte Benutzerinstanz nicht verwendet. Stattdessen wird versucht, mithilfe des Standardkonstruktors zu instanziieren: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;
REST-Endpunkt:
@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));
...
};
}
}
Testklasse:
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());
}
}
Wie kann ich dieses Problem lösen, ohne zu webAppContextSetup
wechseln zu müssen? Ich möchte Tests schreiben, die die vollständige Kontrolle über Service-Mocks haben, also verwende ich standaloneSetup
.
Dies kann durch Injektion einer HandlerMethodArgumentResolver
in Ihren Mock MVC-Kontext oder in eine eigenständige Konfiguration erfolgen. Angenommen, Ihr @AuthenticationPrincipal
ist vom Typ 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(…);
}
};
Dieser Argument-Resolver kann den Typ ParticipantDetails
verarbeiten und erstellt ihn einfach aus dem Nichts, aber Sie sehen, dass Sie viel Kontext bekommen. Später wird dieser Argument-Resolver an das Mock-MVC-Objekt angehängt:
@BeforeMethod
public void beforeMethod() {
mockMvc = MockMvcBuilders
.standaloneSetup(…)
.setCustomArgumentResolvers(putAuthenticationPrincipal)
.build();
}
Dies führt dazu, dass Ihre @AuthenticationPrincipal
-kommentierten Methodenargumente mit den Details Ihres Resolvers aufgefüllt werden.
Aus irgendeinem Grund funktionierte die Lösung von Michael Piefel nicht für mich, also kam ich zu einer anderen.
Erstellen Sie zunächst eine abstrakte Konfigurationsklasse:
@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();
}
}
Dann kannst du Tests wie folgt schreiben:
public class SomeTestClass extends MockMvcTestPrototype {
@Test
@WithUserDetails("[email protected]")
public void someTest() throws Exception {
mockMvc.
perform(get("/api/someService")
.withUser(user(loggedUser)))
.andExpect(status().isOk());
}
}
@AuthenticationPrincipal sollte Ihre eigene Benutzerklassenimplementierung in die Controller-Methode einfügen
public class SomeController {
...
@RequestMapping(method = POST, value = "/update")
public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user) {
...
user.getUser(); // works like a charm!
...
}
}
Ich weiß, die Frage ist alt, aber für Leute, die immer noch suchen, was für mich funktionierte, um einen Spring Boot-Test mit @AuthenticationPrincipal
zu schreiben (und dies funktioniert möglicherweise nicht bei allen Instanzen), war der Test @WithMockUser("testuser1")
.
@Test
@WithMockUser("testuser1")
public void successfullyMockUser throws Exception {
mvc.perform(...));
}
Hier ist ein Link zur Spring-Dokumentation zu @WithMockUser
Es ist nicht gut dokumentiert, aber es gibt eine Möglichkeit, das Authentication
-Objekt als Parameter Ihrer MVC-Methode in ein Standalone-MockMvc einzufügen. Wenn Sie die Authentication
in der SecurityContextHolder
setzen, wird der Filter SecurityContextHolderAwareRequestFilter
normalerweise von Spring Security instanziiert und nimmt die Auth-Injektion für Sie vor.
Sie müssen diesen Filter einfach zu Ihrem MockMvc-Setup hinzufügen:
@Before
public void before() throws Exception {
SecurityContextHolder.getContext().setAuthentication(myAuthentication);
SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
authInjector.afterPropertiesSet();
mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
}