Sunday, May 25, 2014

Work around to authenticate during unit testing on Spring MVC

Following method is the "usual practice" used in unit testing the authentication on Spring MVC.
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(value = {"classpath:/WEB-INF/Project-servlet.xml", "classpath:/WEB-INF/security.xml", "classpath:/WEB-INF/datasource.xml", "classpath:/WEB-INF/user.xml"})

public class HelloControllerTest extends AbstractJUnit4SpringContextTests {

 private MockMvc mockMvc;
 
 @Autowired
 private WebApplicationContext wac;
 
 @Autowired
 @Qualifier("authServiceProvider4")
 private UserDetailsService userDetailsService;

 @Autowired
 private FilterChainProxy proxy;
 
 @Before
 public void setUp() {
  mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilter(proxy).build();
 }

 @Test
 public void testUserWithAdminRoleLandOnWelcomeUrl() throws Exception {
  
  UserDetails ud = userDetailsService.loadUserByUsername("user1");
  Authentication auth = new UsernamePasswordAuthenticationToken(ud.getUsername(), ud.getPassword(), ud.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(auth);

  ...

  mockMvc.perform(get("/welcome"))
   .andExpect(status().isOk());

 }
}
Code sample above showing a test method testing on valid user, user1, landing on welcome site which require admin role in order to render the page. Somehow the test were failed, and following failure trace were seen:
java.lang.AssertionError: Status expected:<200> but was:<302>
 at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
 at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
 at org.springframework.test.web.servlet.result.StatusResultMatchers$5.match(StatusResultMatchers.java:546)
 at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:141)
 at org.huahsin.sit.HelloControllerTest.testUserWithAdminRoleLandOnWelcomeUrl(HelloControllerTest.java:101)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:622)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


From what I understand from this answer, the session of user1 were gone missing, I need to hold the session of user1 when MockMvc doing its work. To do this, a mocking session class is require:
    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }
After that create a mock session and pass it to MockMvc to perform its test work.
 @Test
 public void testUserWithAdminRoleLandOnWelcomeUrl() throws Exception {
  
  UserDetails ud = userDetailsService.loadUserByUsername("user1");
  Authentication auth = new UsernamePasswordAuthenticationToken(ud.getUsername(), ud.getPassword(), ud.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(auth);

  MockHttpSession session = new MockHttpSession();
  session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, new MockSecurityContext(auth));

  mockMvc.perform(get("/welcome").session(session))
   .andExpect(status().isOk());

 }

No comments: