web-dev-qa-db-fra.com

Injecter @AuthenticationPrincipal lorsque l'unité teste un ressort REST manette

Je ne parviens pas à essayer de tester un noeud final de repos qui reçoit une UserDetails en tant que paramètre annoté avec @AuthenticationPrincipal.

On dirait que l'instance d'utilisateur créée dans le scénario de test n'est pas utilisée, mais une tentative d'instanciation à l'aide du constructeur par défaut est effectuée à la place: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;

Point de terminaison REST:

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

Classe de test:

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

}

Comment puis-je résoudre ce problème sans avoir à passer à webAppContextSetup? Je voudrais écrire des tests avec un contrôle total des simulacres de service, donc j'utilise standaloneSetup.

14
andrucz

Cela peut être fait en injectant une HandlerMethodArgumentResolver dans votre contexte Mock MVC ou votre configuration autonome. En supposant que votre @AuthenticationPrincipal est de 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(…);
    }
};

Ce résolveur d'arguments peut gérer le type ParticipantDetails et le crée tout seul, mais vous voyez que vous obtenez beaucoup de contexte. Plus tard, ce résolveur d'arguments est attaché à l'objet MVC simulé:

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

Ainsi, les arguments de votre méthode annotée @AuthenticationPrincipal seront renseignés avec les détails de votre résolveur.

6
Michael Piefel

Pour une raison quelconque, la solution de Michael Piefel ne fonctionnant pas pour moi, j'en ai donc proposé une autre.

Tout d’abord, créez une classe de configuration abstraite:

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

Ensuite, vous pouvez écrire des tests comme celui-ci:

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

    }
}

Et @AuthenticationPrincipal devrait injecter votre propre implémentation de classe User dans la méthode du contrôleur 

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

Je sais que la question est ancienne, mais pour les personnes qui recherchent toujours, ce qui a fonctionné pour moi pour écrire un test Spring Boot avec @AuthenticationPrincipal (et cela peut ne pas fonctionner avec toutes les instances), annotait le test @WithMockUser("testuser1")

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

Voici un lien vers la documentation Spring sur @WithMockUser

1
Sam

Ce n'est pas bien documenté, mais il existe un moyen d'injecter l'objet Authentication en tant que paramètre de votre méthode MVC dans un standalone MockMvc . Si vous définissez Authentication dans SecurityContextHolder, le filtre SecurityContextHolderAwareRequestFilter est généralement instancié par Spring Security et effectue l'injection de l'auth à votre place.

Vous devez simplement ajouter ce filtre à votre configuration MockMvc, comme ceci:

@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