spring-projects / spring-projects/spring-session

Improve MockMvc testability

Open
#1,019 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
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

  1. Convenience calls to retrieve the real (previously wrapped) HttpSession or the SecurityContext.
  2. Some tests within Spring Session's baseline validating proper HttpSession creation 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.