Thursday, September 1, 2016

Spring Boot 1.4 Test Driven Development (MVC, Security, Service Layer, Mongo DB) – Part 1

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

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.