wake-up-neo.com

Injizieren Sie @AuthenticationPrincipal, wenn das Gerät eine Feder testet REST Regler

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.

14
andrucz

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.

6
Michael Piefel

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!
       ...
    }
}
4
pzeszko

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

1
Sam

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