Skip to content

Testing with mock users in Spring / Spring MVC

A common unit test scenario for Spring / Spring MVC applications is to verify behavior when logged in as a particular user. The new spring-security-test library available with Spring Security version 4 makes testing user access controls in Spring and Spring MVC applications far simpler.

Testing method level security

Testing method level security annotations used to require manually creating an Authentication object and setting it in the test’s SecurityContext. This is described in a previous post on Protecting Service Methods with Spring Security Annotations. It’s relatively straightforward to do this but it does clutter the test somewhat. We want to test what happens when a user is logged in and not concern ourselves with how to log the user in.

@Test
public void testViewerAccess() {
 
    // Login as viewer by creating an Authentication and setting it in the SecurityContext
    SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("viewer", "password"));
 
    // Viewer should have access to get* methods - just call the method to check no exception is thrown
    spannersDAO.get(1);
    spannersDAO.getAll();
 
    // Viewer should not have access to create / update / delete
    Spanner spanner = newSpanner();
    verifyException(spannersDAO, AccessDeniedException.class).create(spanner);
    verifyException(spannersDAO, AccessDeniedException.class).update(spanner);
    verifyException(spannersDAO, AccessDeniedException.class).delete(spanner);
}

The @WithMockUser annotation in spring-security-test allows us run a test as if we’re logged in as a user just by annotating the test method:

@Test
@WithMockUser(roles=ROLE_VIEWER)
public void testViewerAccess() {

	// Viewer should have access to get* methods - just call the method to check no exception is thrown
	spannersDAO.get(1);
	spannersDAO.getAll();

	// Viewer should not have access to create / update / delete
	Spanner spanner = newSpanner();
	verifyException(spannersDAO, AccessDeniedException.class).create(spanner);
	verifyException(spannersDAO, AccessDeniedException.class).update(spanner);
	verifyException(spannersDAO, AccessDeniedException.class).delete(spanner);
}

In this case, I want my test user to have ROLE_VIEWER. The user’s name and password are not important to this test case so I don’t need to specify them.

Testing controller access via URLs

In Spring Security, access to controllers can be restricted based on the controller’s URL mapping:

<http auto-config="true" disable-url-rewriting="true" use-expressions="true">
	<intercept-url pattern="/" access="permitAll" />
	<intercept-url pattern="/resources/**" access="permitAll" />
	<intercept-url pattern="/signin" access="permitAll" />
	<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
	<intercept-url pattern="/**" access="isAuthenticated()" />
</http>

In this security context definition, I want the signin page to be available to everyone, everything on /admin/ to be restricted to those with the ADMIN role and all other pages to be available to any logged in (isAuthenticated()) user. MockMvc can be used to call Spring MVC controllers and test whether or not they are allowed for a particular user.

In the simplest case, I can assert that the signin page is available to users who have not yet logged in:

@Test
public void testSigninIsAvailableToAnonymous() throws Exception {
	mockMvc.perform(get(SigninController.CONTROLLER_URL))
			.andExpect(status().isOk());
}

A more complicated test case would be to verify that the SwitchUser page (on /admin/switchUser) is available only to users who have logged in and have the correct role. Again, the new @WithMockUser annotation can be used to simply run the test as if a given user was logged in:

@Test
@WithMockUser(roles=ROLE_ADMIN)
public void testAdminPathIsAvailableToAdminRole() throws Exception {
	mockMvc.perform(get(SwitchUserController.CONTROLLER_URL))
			.andExpect(status().isOk()); // Expect that ADMIN users can access this page
}

@Test
@WithMockUser(roles=ROLE_VIEWER)
public void testAdminPathIsNotAvailableToViewer() throws Exception {
	mockMvc.perform(get(SwitchUserController.CONTROLLER_URL))
			.andExpect(status().isForbidden()); // Expect that VIEWER users are forbidden from accessing this page
}

 Setting up MockMVC against the Spring Security Context

The above tests require MockMVC to be started against the Spring Web Application Context and the Security Context. Again, this has been simplified in spring-security-test version 4. Perviously, the Spring Security Filter chain had to be injected into the test class and then added to the MockMvcBuilder:

@Autowired protected WebApplicationContext wac;
@Autowired private FilterChainProxy springSecurityFilterChain; // Inject the filter chain created by Spring Security
protected MockMvc mockMvc;

@Before
public void setup() throws Exception {
			
	// Wire up Spring MVC context AND spring security filter
	mockMvc = webAppContextSetup(wac)
				.addFilters(springSecurityFilterChain) // Add the injected springSecurityFilterChain
				.build();
}

A new static method now exists to do this for you – SecurityMockMvcConfigurers.springSecurity(). This simplifies the creation of the MockMvc object a little:

@Autowired protected WebApplicationContext wac;
protected MockMvc mockMvc;

@Before
public void setup() throws Exception {
	// Set up a mock MVC tester based on the web application context and spring security context
	mockMvc = webAppContextSetup(wac)
					.apply(springSecurity()) // This finds the Spring Security filter chain and adds it for you
					.build(); 
}

Further information

The Spring Security Reference lists additional new features in the spring-security-test package including enhancements for testing method level security and Spring MVC security. In addition, a series of preview blog posts from Spring demonstrate testing method level security, testing Spring MVC and testing with HtmlUnit.

Published inHow ToSecurityTesting

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *