Spring Test returning 401 for unsecured URLs
Asked Answered
G

7

32

I am using Spring for MVC tests

Here is my test class

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

Here is the MVC config

@Configuration
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/login/form").setViewName("login");
    }
}

Here is the security config

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

When I run my test it fails with the 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)

I understand that it fails due to the fact that the url is protected with spring security, but when I run my application I can access that url even without being authenticated.

Am I doing something wrong?

Gabriella answered 18/9, 2016 at 5:0 Comment(14)
Does the request work with the same configuration from a browser?Muldoon
Yes, it does workGabriella
Smells like a problem with your test configuration, then. What happens if you put a breakpoint inside your configure and debug the test?Muldoon
Sorry, just saw in the documentation that the WebMvcTest annotation searches only WebMvcConfigurer and not WebSecurityConfigurerGabriella
In that case, I suggest figuring out what configuration is required to apply the security configurer and writing a self-answer. This is a reasonable question that is likely to happen to someone else using the new Boot test features.Muldoon
But it still don't make sense, if my configuration is not picked, why the url is returning 401 as if it were secured? It should return okGabriella
Boot is automatically applying Basic authentication to all URLs. You may be able to see this in the startup logs (I don't remember whether that's the case for tests).Muldoon
You're right, but the problem is the apply(springSecurity()). It is applying a default spring security config since @WebMvcTest doesn't pick security configurationsGabriella
It is a little weird, since this new concept of test slices gives the hability to run tests only with the Mvc layer, but in my opinion almost every application has security implemented. It forces me to use the full aplication, making the @WebMvcTest pointlessGabriella
i think you should change this in test .andExpect(content().string(containsString("login"))); instead of .andExpect(content().string(containsString("Login")));Shuma
You should be able to just use @ContextConfiguration to load that one specific configurer class.Muldoon
I just answered the question exactly with this. Thanks, your comments helped me find the answerGabriella
FYI: this is basically a duplicate of #38675520Marketing
An answer in Unit test Springboot MockMvc returns 403 Forbidden worked for me: @AutoConfigureMockMvc(addFilters = false), but I'm not 100% of whether it's actually related.Dunne
G
31

I found the answer
Spring docs says that:

@WebMvcTest will auto-configure the Spring MVC infrastructure and limit scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver. Regular @Component beans will not be scanned when using this annotation.

And according to this issue in github:

https://github.com/spring-projects/spring-boot/issues/5476

The @WebMvcTest by default auto configure spring security if spring-security-test is present in the class path (which in my case is).

So since WebSecurityConfigurer classes aren't picked, the default security was being auto configured, that is the motive I was receiving the 401 in url's that was not secured in my security configuration. Spring security default auto configuration protects all url's with basic authentication.

What I did to solve the problem was to annotate the class with @ContextConfiguration, and @MockBean like it is described in the documentation:

Often @WebMvcTest will be limited to a single controller and used in combination with @MockBean to provide mock implementations for required collaborators.

And here is the test class

@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 and SecurityConfig are all my configuration classes

Gabriella answered 18/9, 2016 at 6:8 Comment(2)
You probably don't need to use @ContextConfiguration. Simply adding @Import(SecurityConfig.class) should typically suffice.Marketing
For further details, see: github.com/spring-projects/spring-boot/issues/6514Marketing
I
11

Not sure if this was available when the original question was asked, but if truly not wanting to test the security portion of a web request (which seems reasonable if the endpoint is known to be unsecure), then I think this could be done simply by using the secure attribute of the @WebMvcTest annotation (it defaults to true so setting it to false should disable the auto-configuration of Spring Security's MockMvc support):

@WebMvcTest(secure = false)

More info available in the javadocs

Isom answered 23/8, 2018 at 7:55 Comment(4)
I was using (at)AutoConfigureMockMvc instead of (at)WebMvcTest, but supplying secure=false to that annotation solved my 401 response from MockMvc where I am not using Spring Security at all.Ticonderoga
Alas, this didn't work for me. I get an IllegalStateException.Inboard
This attribute is deprecated since 2.1.0.Hairworm
On spring boot 1.5.9, this didn't help at allGabon
S
7

Try this if using spring boot 2.0+

@WebMvcTest(controllers = TestController.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class})

Scevour answered 1/6, 2022 at 11:19 Comment(0)
T
6

I had the same problem and solve the issue with the help of the answers here and @Sam Brannen comment.

You probably don't need to use @ContextConfiguration. Simply adding @Import(SecurityConfig.class) should typically suffice.

To simplify and update the answers a bit more I want to share how i fix it in my spring-boot2 project.

I want to test below endpoint.

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

All service methods must be interface otherwise application context can't be initialized. You can check my 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);
    }
}

And below you can see my SystemOptionControllerTest class.

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

So I just need to add @Import(SecurityConfig.class) to my mvc test class.

Tautology answered 23/2, 2019 at 8:6 Comment(0)
W
0

If you use SpringJUnit4ClassRunner instead of SpringRunner you can catch your requests in security layer. If you are using basic authentication you have to user httpBasic method inside mockMvc.perform

 mockMvc.perform(get("/").with(httpBasic(username,rightPassword))
Weathered answered 3/4, 2018 at 11:33 Comment(2)
httpBasic() is found where?Inboard
@MattCampbell org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasicFroehlich
C
0

I had the same problem when using @SpringBootTest and @AutoConfigureMockMvc. Then I tried adding webEnvironment = WebEnvironment.RANDOM_PORT attribute in @SpringBootTest and it worked!

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class AuthControllerTest {

    private @Autowired MockMvc mockMvc;

    @Test
    public void givenUsernameAndPassword_whenLogin_thenSuccess() throws Exception {
        mockMvc.perform(post("/api/v1/auth/login")
                        .param("username", "user1")
                        .param("password", "11111111"))
                .andExpect(status().isOk());
    }
}

Here is the security config:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(registry -> registry
                    .requestMatchers(HttpMethod.POST, "/api/v1/auth/**").permitAll()
                    .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .build();
}

Hope it will help you.

Carrion answered 6/5 at 17:16 Comment(0)
P
-1

You can add this one .header("apikey","apiValue");

if you declared this values in resources then only you can add it

Priory answered 12/10, 2023 at 6:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.