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.
[…] « Testing with mock users in Spring / Spring MVC […]