spring-projects / spring-projects/spring-session
Improve MockMvc testability
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.9k
- Forks
- 1.2k
- Avg merge
- 4h 27m
- Merged PRs (30d)
- 55
Description
Summary
SessionRepositoryFilter wraps the incoming HttpServletRequest and alters the expected SecurityContext retrieval when performing MockMvc requests.
Configuration
Once setting up Spring Security + Spring Session + Spring Session JDBC, I had to force the SessionRepositoryFilter to kick in before Spring Security's filter chain as follow :
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private SessionRepositoryFilter springSessionRepositoryFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(springSessionRepositoryFilter, ChannelProcessingFilter.class)
...
}
}
Problem
Tests written with MockMvc cannot leverage from Spring Security's authentication validation and requires 'cumbersome' reflection to get through authentication details.Tests written with MockMvc
The SecurityMockMvcResultMatchers and/or WebTestUtils are able to retrieve the HttpSessionSecurityContextRepository from the MvcResult.
However, the MockHttpServletRequest contains a null MockHttpSession which is what HttpSessionSecurityContextRepository.loadContext is 'ultimately' checking.
Therefore, it will return null as well instead of the valid SecurityContext which is meant - in such setup - to be found through the HttpSession (attached as a request attribute - key = "org.springframework.session.SessionRepository.CURRENT_SESSION").
Here is an example of problematic test :
@Test
public void login() throws Exception {
User user = createUser();
MockMvcBuilders.webAppContextSetup(wac)
.apply(springSecurity())
.build()
.perform(
post("/login")
.param("email", user.getEmail())
.param("password", "somepassword"))
// FAILURE BELOW
.andExpect(authenticated().withUsername(user.getUsername()));
}
Workaround
All classes are closed from extension with private or final. It makes sense but makes testing difficult.
In order to retrieve a valid SecurityContext with authentication, I used the following 'trick' :
@Test
public void login() throws Exception {
User user = createUser();
MockMvcBuilders.webAppContextSetup(wac)
.apply(springSecurity())
.build()
.perform(
post("/login")
.param("email", user.getEmail())
.param("password", "somepassword"))
.andExpect(matchAuthenticationPrincipal(principal ->
assertThat(principal.getId(), equalTo(user.getId()))
));
}
...
protected User getAuthenticatedPrincipalUserFromResult(MvcResult result) {
HttpSession wrappedSession = getWrappedSessionFromAttributes(result.getRequest());
SecurityContext securityContext = getSecurityContextFromSession(wrappedSession);
assertThat(securityContext, notNullValue());
assertThat(securityContext.getAuthentication(), notNullValue());
assertThat(securityContext.getAuthentication().getPrincipal(), instanceOf(User.class));
return (User) securityContext.getAuthentication().getPrincipal();
}
protected HttpSession getWrappedSessionFromAttributes(MockHttpServletRequest request) {
Object sessionRepositoryFilter_sessionRepositoryRequestWrapper = request.getAttribute(
"org.springframework.session.SessionRepository.CURRENT_SESSION");
notNull(
sessionRepositoryFilter_sessionRepositoryRequestWrapper,
"The request should contain an attribute wrapping the request including the session");
isInstanceOf(HttpSession.class, sessionRepositoryFilter_sessionRepositoryRequestWrapper);
return (HttpSession) sessionRepositoryFilter_sessionRepositoryRequestWrapper;
}
protected SecurityContext getSecurityContextFromSession(HttpSession wrappedSession) {
Object jdbcOperationsSessionRepository_jdbcSession = ReflectionTestUtils.getField(
wrappedSession,
"session");
notNull(
jdbcOperationsSessionRepository_jdbcSession,
"The wrapped request should contain an instance of a JdbcSession");
Object mapSession = ReflectionTestUtils.getField(
jdbcOperationsSessionRepository_jdbcSession,
"delegate");
notNull(
mapSession,
"The session should contain an instance of MapSession containing the session");
Object sessionAttributes = ReflectionTestUtils.getField(
mapSession,
"sessionAttrs");
notNull(
sessionAttributes,
"The mapper should contain session attributes");
isInstanceOf(Map.class, sessionAttributes);
Map<String, Object> attributesMap = (Map<String, Object>) sessionAttributes;
assertThat(attributesMap, hasKey(equalTo(SPRING_SECURITY_CONTEXT_KEY)));
Object securityContext = attributesMap.get(SPRING_SECURITY_CONTEXT_KEY);
isInstanceOf(SecurityContext.class, securityContext);
return (SecurityContext) securityContext;
}
Ideally expected
- Convenience calls to retrieve the real (previously wrapped)
HttpSessionor theSecurityContext. - Some tests within Spring Session's baseline validating proper
HttpSessioncreation in a similar setup 🤔 ?
Disclaimer : I shortened the configuration/setup to only highlights the key-points.
Please let me know if you need a more comprehensive example or if the idea is explicit enough 😃
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by examining SecurityMockMvcResultMatchers, WebTestUtils, HttpSessionSecurityContextRepository, and SessionRepositoryFilter to trace how MockMvc requests expose the wrapped session. Compare the requested convenience accessors with baseline Spring Session tests, and consider the work complete when authentication and HttpSession creation can be validated without reflection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- backend, testing
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100