This
series of articles demonstrate how to develop test driven Spring MVC
application with Spring Data Mongo DB and Spring Security.
Assume the following Controller class
@Controller
@RequestMapping("/sample")
public class SampleController {
@RequestMapping("/ananymous")
@ResponseBody
public String ananymous() {
return "Welcome Ananymous";
}
@RequestMapping("/user")
@PreAuthorize("hasRole('ROLE_USER')")
@ResponseBody
public String user(Authentication auth) {
return "Welcome User - " + auth.getName();
}
@RequestMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@ResponseBody
public String admin() {
return "Welcome Admin";
}
}
Assume the following Controller class
@Controller
@RequestMapping("/sample")
public class SampleController {
@RequestMapping("/ananymous")
@ResponseBody
public String ananymous() {
return "Welcome Ananymous";
}
@RequestMapping("/user")
@PreAuthorize("hasRole('ROLE_USER')")
@ResponseBody
public String user(Authentication auth) {
return "Welcome User - " + auth.getName();
}
@RequestMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@ResponseBody
public String admin() {
return "Welcome Admin";
}
}
It includes three methods with request mapping:
- ananymous() is accessible to all
- user() is accessible to logged in users with the role ROLE_USER
- admin() is accessible to the user with the role ROLE_ADMIN
Generally WebSecurity for this application is enabled using a sample configuration:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// permitAll for other public resources
http.authorizeRequests()
.antMatchers("/sample/ananymous").permitAll();
http.authorizeRequests()
.antMatchers("/**").authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
@Bean
public AuthenticationProvider customAuthenticationProvider() {
// return Custom Authentication Provider Implementation
}
}
The above security configuration may include custom database based authentication using DaoAuthenticationProvider.
However for testing purpose the following in memory user details manager configuration is proposed.
@TestConfiguration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityTestConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/sample/ananymous").permitAll();
// All the paths are authenticated
http.authorizeRequests().antMatchers("/**").authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService());
}
}
@Bean
public UserDetailsService userDetailsService(){
GrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
GrantedAuthority userAuthority = new SimpleGrantedAuthority("ROLE_USER");
UserDetails user1 = (UserDetails) new User("raghu", "pwd", Arrays.asList(adminAuthority));
UserDetails user2 = (UserDetails) new User("ram", "pwd", Arrays.asList(userAuthority));
return new InMemoryUserDetailsManager(Arrays.asList(user1, user2));
}
}
Spring Boot 1.4 introduced @TestConfiguration. With the annotation this class will not be accidentally picked up during ComponentScan of BootApplication.
Now testing the controller code is as follows
@RunWith(SpringRunner.class)
@WebMvcTest
@Import(SecurityTestConfig.class)
public class SampleControllerTest {
@Configuration
@ComponentScan(
basePackageClasses = { SampleController.class },
useDefaultFilters = false,
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
value = { SampleController.class }) })
static class TestConfig {
}
@Autowired
private MockMvc mvc;
@Test
public void testAnanymous() throws Exception {
mvc.perform(get("/sample/ananymous"))
.andExpect(status().isOk())
.andExpect(content() .string(Matchers.containsString("Welcome Ananymous")));
}
@Test
public void testAnanymousDenied() throws Exception {
mvc.perform(get("/sample/user"))
.andExpect(status().is4xxClientError());
}
@Test
@WithUserDetails("ram")
public void testUserAccess() throws Exception {
mvc.perform(get("/sample/user"))
.andExpect(status().isOk())
.andExpect(content().string(Matchers.containsString("ram")));
}
@Test
@WithUserDetails("raghu")
public void testAdminAccess() throws Exception {
mvc.perform(get("/sample/admin")).andExpect(status().isOk());
}
}
Test code is explained:
- @RunWith(SpringRunner.class) is an alias for the SpringJUnit4ClassRunner
- @ContextConfiguration creates application context and looks for bean definition through inner class
- Inner class TestConfig initiates component scan, but limits to pick the component that are necessary for this controller.
- @Import(SecurityTestConfig.class) injects in memory authentication
- @WebMvcTest create web context and with this we can autowire MockMvc
- with is we can perform HTTP get and post
- testAnanymous() performs GET request and verifies it is accessible
- testAnanymousDenied() performs GET request to the protected method and verifies that 403 Access denied error is returned
- @WithUserDetails("ram") annotation enables testUserAccess() method to perform GET request with authentication and ROLE_USER. The resource accessibility is verified.
- In the same way ROLE_ADMIN protected URL is also verified.