I have setup basic authentication in my controller with Spring Security in the classic way as follows:
@EnableWebSecurity
@EnableGlobalMethodSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("user").roles("USER")
.and()
.withUser("admin").password("admin").roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(....);
}
}
When it comes to the point of testing, I am using @WithMockUser to annotate my tests. A test might look like this for GET:
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = SomeController.class)
public class SomeControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void test1() {
mockMvc.perform(get(...)).andExpect(...);
}
or like this for POST:
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = SomeController.class)
public class SomeControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void test1() {
mockMvc.perform(post(...)).andExpect(...);
}
then something unexpected happens:
QUESTION: what's going on? How to fix this misbehaviour?
Looks like Spring Security is activated, however my testing is not using the authentication that I would expect to be used. Do I have to mock the authentication data myself?
EDIT The full configure method is the following
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(URL1).hasAnyRole(ROLE_ADMIN)
.antMatchers(URL2).hasAnyRole(ROLE_USER)
.antMatchers(URL3).permitAll()
.and()
.httpBasic()
.and()
.csrf().disable();
}
Simply put, Spring Security hold the principal information of each authenticated user in a ThreadLocal – represented as an Authentication object. In order to construct and set this Authentication object – we need to use the same approach Spring Security typically uses to build the object on a standard authentication.
There are two reasons behind this behavior:
@WithMockUser
annotation is not intended to execute authentication. It creates a user which is authenticated already. By default his credentials are user
: password
@WebMvcTest
does not execute MySecurityConfig.java. This annotation creates Spring mockMvc object with Security defaults for testing. Those security defaults are applied by org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityAutoConfiguration
You can double check this by putting break points on MySecurityConfig
methods and rerunning your test in debug mode. Break points are not hit.Solving issue 1
Simply change your approach to what @WithMockUser annotation does. It gives already logged-in user. It is still possible to test urls security and roles configuration with specifying concrete username, password and roles.
Solving issue 2
Create a base class for all Integration tests. It will configure mockMvc with Spring Security applied. Also note @SpringBootTest
annotation. Now test will use MySecurityConfig.java
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class IT {
@Autowired
protected WebApplicationContext wac;
@Autowired
private FilterChainProxy springSecurityFilterChain;
protected MockMvc mockMvc;
@Before
public void applySecurity() {
this.mockMvc = webAppContextSetup(wac)
.apply(springSecurity(springSecurityFilterChain))
.build();
}
}
Rewrite the test like this. Assuming you use http basic authentication. Credentials are provided inside the test. Note: no mock user annotation.
package com.example.demo;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import org.junit.Test;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
public class SomeControllerIT extends IT {
@Test
public void test1() throws Exception {
mockMvc.perform(get("/some")
.with(httpBasic("user", "user")))
.andExpect(MockMvcResultMatchers.content().string("hello"));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With