J'utilise Spring pour les tests MVC
Voici ma classe de test
@RunWith(SpringRunner.class)
@WebMvcTest
public class ITIndexController {
@Autowired
WebApplicationContext context;
MockMvc mockMvc;
@MockBean
UserRegistrationApplicationService userRegistrationApplicationService;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
public void should_render_index() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("index"))
.andExpect(content().string(containsString("Login")));
}
}
Voici la configuration MVC
@Configuration
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/login/form").setViewName("login");
}
}
Voici la configuration de sécurité
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/resources/**", "/signup", "/signup/form", "/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login/form").permitAll().loginProcessingUrl("/login").permitAll()
.and()
.logout().logoutSuccessUrl("/login/form?logout").permitAll()
.and()
.csrf().disable();
}
@Autowired
public void configureGlobalFromDatabase(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
Lorsque j'exécute mon test, il échoue avec le message:
Java.lang.AssertionError: Status expected:<200> but was:<401>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.Java:54)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.Java:81)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.Java:664)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.Java:171)
at com.marco.nutri.integration.web.controller.ITIndexController.should_render_index(ITIndexController.Java:46)
at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:62)
at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:43)
at Java.lang.reflect.Method.invoke(Method.Java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.Java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.Java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.Java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.Java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.Java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.Java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.Java:94)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.Java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.Java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.Java:191)
at org.Eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.Java:86)
at org.Eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.Java:38)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:459)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:678)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.Java:382)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.Java:192)
Je comprends qu'il échoue en raison du fait que l'url est protégée par une sécurité de printemps, mais lorsque j'exécute mon application, je peux accéder à cette URL même sans être authentifié.
Est-ce que je fais quelque chose de mal?
J'ai trouvé la réponse
Spring docs dit que:
@WebMvcTest configurera automatiquement l'infrastructure Spring MVC et limitera les beans analysés à @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer et HandlerMethodArgumentResolver. Les beans @Component standard ne seront pas analysés lors de l'utilisation de cette annotation.
Et selon ce problème dans github:
https://github.com/spring-projects/spring-boot/issues/5476
Le @WebMvcTest par défaut configure automatiquement la sécurité du ressort si spring-security-test est présent dans le chemin de classe (ce qui dans mon cas l'est).
Donc, puisque les classes WebSecurityConfigurer ne sont pas sélectionnées, la sécurité par défaut était en cours de configuration automatique, c'est le motif que je recevais le 401 dans les URL qui n'était pas sécurisé dans ma configuration de sécurité. La configuration automatique par défaut de Spring Security protège toutes les URL avec une authentification de base.
Ce que j'ai fait pour résoudre le problème était d'annoter la classe avec @ContextConfiguration, et @MockBean comme il est décrit dans la documentation:
Souvent, @WebMvcTest sera limité à un seul contrôleur et utilisé en combinaison avec @MockBean pour fournir des implémentations fictives aux collaborateurs requis.
Et voici la classe de test
@RunWith(SpringRunner.class)
@WebMvcTest
@ContextConfiguration(classes={Application.class, MvcConfig.class, SecurityConfig.class})
public class ITIndex {
@Autowired
WebApplicationContext context;
MockMvc mockMvc;
@MockBean
UserRegistrationApplicationService userRegistrationApplicationService;
@MockBean
UserDetailsService userDetailsService;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
public void should_render_index() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("index"))
.andExpect(content().string(containsString("Login")));
}
}
Application, MvcConfig et SecurityConfig sont toutes mes classes de configuration
Je ne sais pas si cela était disponible lorsque la question d'origine a été posée, mais si vous ne voulez vraiment pas tester la partie sécurité d'une demande Web (ce qui semble raisonnable si le point de terminaison est connu pour être non sécurisé), je pense que cela pourrait être fait simplement par en utilisant l'attribut secure
de @WebMvcTest
annotation (la valeur par défaut est true
, donc la définir sur false
devrait désactiver la configuration automatique du support MockMvc de Spring Security):
@WebMvcTest(secure = false)
Plus d'informations disponibles dans le javadocs
Si vous utilisez SpringJUnit4ClassRunner au lieu de SpringRunner, vous pouvez intercepter vos demandes dans la couche de sécurité. Si vous utilisez l'authentification de base, vous devez utiliser la méthode httpBasic dans mockMvc.perform
mockMvc.perform(get("/").with(httpBasic(username,rightPassword))
J'ai eu un problème et résoudre le problème à l'aide des réponses ici et du commentaire de @Sam Brannen.
Vous n'avez probablement pas besoin d'utiliser @ContextConfiguration. L'ajout simple de @Import (SecurityConfig.class) devrait généralement suffire.
Pour simplifier et mettre à jour les réponses un peu plus, je veux partager comment je le corrige dans mon projet spring-boot2.
Je veux tester sous le point final.
@RestController
@Slf4j
public class SystemOptionController {
private final SystemOptionService systemOptionService;
private final SystemOptionMapper systemOptionMapper;
public SystemOptionController(
SystemOptionService systemOptionService, SystemOptionMapper systemOptionMapper) {
this.systemOptionService = systemOptionService;
this.systemOptionMapper = systemOptionMapper;
}
@PostMapping(value = "/systemoption")
public SystemOptionDto create(@RequestBody SystemOptionRequest systemOptionRequest) {
SystemOption systemOption =
systemOptionService.save(
systemOptionRequest.getOptionKey(), systemOptionRequest.getOptionValue());
SystemOptionDto dto = systemOptionMapper.mapToSystemOptionDto(systemOption);
return dto;
}
}
Toutes les méthodes de service doivent être des interfaces sinon le contexte d'application ne peut pas être initialisé. Vous pouvez vérifier mon SecurityConfig.
@Configuration
@EnableWebSecurity
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices resourceServerTokenServices;
@Override
public void configure(final HttpSecurity http) throws Exception {
if (Application.isDev()) {
http.csrf().disable().authorizeRequests().anyRequest().permitAll();
} else {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().regexMatchers("/health").permitAll()
.antMatchers("/prometheus").permitAll()
.anyRequest().authenticated()
.and()
.authorizeRequests()
.anyRequest()
.permitAll();
http.csrf().disable();
}
}
@Override
public void configure(final ResourceServerSecurityConfigurer resources) {
resources.tokenServices(resourceServerTokenServices);
}
}
Et ci-dessous, vous pouvez voir ma classe SystemOptionControllerTest.
@RunWith(SpringRunner.class)
@WebMvcTest(value = SystemOptionController.class)
@Import(SecurityConfig.class)
public class SystemOptionControllerTest {
@Autowired private ObjectMapper mapper;
@MockBean private SystemOptionService systemOptionService;
@MockBean private SystemOptionMapper systemOptionMapper;
@MockBean private ResourceServerTokenServices resourceServerTokenServices;
private static final String OPTION_KEY = "OPTION_KEY";
private static final String OPTION_VALUE = "OPTION_VALUE";
@Autowired private MockMvc mockMvc;
@Test
public void createSystemOptionIfParametersAreValid() throws Exception {
// given
SystemOption systemOption =
SystemOption.builder().optionKey(OPTION_KEY).optionValue(OPTION_VALUE).build();
SystemOptionDto systemOptionDto =
SystemOptionDto.builder().optionKey(OPTION_KEY).optionValue(OPTION_VALUE).build();
SystemOptionRequest systemOptionRequest = new SystemOptionRequest();
systemOptionRequest.setOptionKey(OPTION_KEY);
systemOptionRequest.setOptionValue(OPTION_VALUE);
String json = mapper.writeValueAsString(systemOptionRequest);
// when
when(systemOptionService.save(
systemOptionRequest.getOptionKey(), systemOptionRequest.getOptionValue()))
.thenReturn(systemOption);
when(systemOptionMapper.mapToSystemOptionDto(systemOption)).thenReturn(systemOptionDto);
// then
this.mockMvc
.perform(
post("/systemoption")
.contentType(MediaType.APPLICATION_JSON)
.content(json)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(content().string(containsString(OPTION_KEY)))
.andExpect(content().string(containsString(OPTION_VALUE)));
}
}
J'ai donc juste besoin d'ajouter @Import(SecurityConfig.class)
à ma classe de test mvc.