This works fine until I have to test a service that needs a logged in user, how do I add user to context :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-test.xml")
@WebAppConfiguration
public class FooTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Resource(name = "aService")
private AService aService; //uses logged in user
@Before
public void setup() {
this.mockMvc = webAppContextSetup(this.webApplicationContext).build();
}
If you want to use MockMVC with the latest spring security test package, try this code:
Principal principal = new Principal() {
@Override
public String getName() {
return "TEST_PRINCIPAL";
}
};
getMockMvc().perform(get("http://your-url.com").principal(principal))
.andExpect(status().isOk()));
Keep in mind that you have to be using Principal based authentication for this to work.
If successful authentication yields some cookie, then you can capture that (or just all cookies), and pass it along in the next tests:
@Autowired
private WebApplicationContext wac;
@Autowired
private FilterChainProxy filterChain;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(filterChain).build();
}
@Test
public void testSession() throws Exception {
// Login and save the cookie
MvcResult result = mockMvc.perform(post("/session")
.param("username", "john").param("password", "s3cr3t")).andReturn();
Cookie c = result.getResponse().getCookie("my-cookie");
assertThat(c.getValue().length(), greaterThan(10));
// No cookie; 401 Unauthorized
mockMvc.perform(get("/personal").andExpect(status().isUnauthorized());
// With cookie; 200 OK
mockMvc.perform(get("/personal").cookie(c)).andExpect(status().isOk());
// Logout, and ensure we're told to wipe the cookie
result = mockMvc.perform(delete("/session").andReturn();
c = result.getResponse().getCookie("my-cookie");
assertThat(c.getValue().length(), is(0));
}
Though I know I'm not making any HTTP requests here, I kind of like the stricter separation of the above integration test and my controllers and Spring Security implementation.
To make the code a bit less verbose, I use the following to merge the cookies after making each request, and then pass those cookies along in each subsequent request:
/**
* Merges the (optional) existing array of Cookies with the response in the
* given MockMvc ResultActions.
* <p>
* This only adds or deletes cookies. Officially, we should expire old
* cookies. But we don't keep track of when they were created, and this is
* not currently required in our tests.
*/
protected static Cookie[] updateCookies(final Cookie[] current,
final ResultActions result) {
final Map<String, Cookie> currentCookies = new HashMap<>();
if (current != null) {
for (Cookie c : current) {
currentCookies.put(c.getName(), c);
}
}
final Cookie[] newCookies = result.andReturn().getResponse().getCookies();
for (Cookie newCookie : newCookies) {
if (StringUtils.isBlank(newCookie.getValue())) {
// An empty value implies we're told to delete the cookie
currentCookies.remove(newCookie.getName());
} else {
// Add, or replace:
currentCookies.put(newCookie.getName(), newCookie);
}
}
return currentCookies.values().toArray(new Cookie[currentCookies.size()]);
}
...and a small helper as cookie(...)
needs at least one cookie:
/**
* Creates an array with a dummy cookie, useful as Spring MockMvc
* {@code cookie(...)} does not like {@code null} values or empty arrays.
*/
protected static Cookie[] initCookies() {
return new Cookie[] { new Cookie("unittest-dummy", "dummy") };
}
...to end up with:
Cookie[] cookies = initCookies();
ResultActions actions = mockMvc.perform(get("/personal").cookie(cookies)
.andExpect(status().isUnauthorized());
cookies = updateCookies(cookies, actions);
actions = mockMvc.perform(post("/session").cookie(cookies)
.param("username", "john").param("password", "s3cr3t"));
cookies = updateCookies(cookies, actions);
actions = mockMvc.perform(get("/personal").cookie(cookies))
.andExpect(status().isOk());
cookies = updateCookies(cookies, actions);
You should be able to just add the user to the security context:
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new GrantedAuthorityImpl("ROLE_USER"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, password,list);
SecurityContextHolder.getContext().setAuthentication(auth);
With spring 4 this solution mock the formLogin and logout using sessions and not cookies because spring security test not returning cookies.
Because it's not a best practice to inherit tests you can @Autowire this component in your tests and call it's methods.
With this solution each perform operation on the mockMvc
will be called as authenticated if you called the performLogin
on the end of the test you can call performLogout
.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.Filter;
import static com.condix.SessionLogoutRequestBuilder.sessionLogout;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Component
public class SessionBasedMockMvc {
private static final String HOME_PATH = "/";
private static final String LOGOUT_PATH = "/login?logout";
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private Filter springSecurityFilterChain;
private MockMvc mockMvc;
public MockMvc createSessionBasedMockMvc() {
final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.defaultRequest(defaultRequestBuilder)
.alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
.apply(springSecurity(springSecurityFilterChain))
.build();
return this.mockMvc;
}
public void performLogin(final String username, final String password) throws Exception {
final ResultActions resultActions = this.mockMvc.perform(formLogin().user(username).password(password));
this.assertSuccessLogin(resultActions);
}
public void performLogout() throws Exception {
final ResultActions resultActions = this.mockMvc.perform(sessionLogout());
this.assertSuccessLogout(resultActions);
}
private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
final MockHttpServletRequest request) {
requestBuilder.session((MockHttpSession) request.getSession());
return request;
}
private void assertSuccessLogin(final ResultActions resultActions) throws Exception {
resultActions.andExpect(status().isFound())
.andExpect(authenticated())
.andExpect(redirectedUrl(HOME_PATH));
}
private void assertSuccessLogout(final ResultActions resultActions) throws Exception {
resultActions.andExpect(status().isFound())
.andExpect(unauthenticated())
.andExpect(redirectedUrl(LOGOUT_PATH));
}
}
Because default LogoutRequestBuilder
doesn't support session we need to create another logout request builder.
import org.springframework.beans.Mergeable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.request.ConfigurableSmartRequestBuilder;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
/**
* This is a logout request builder which allows to send the session on the request.<br/>
* It also has more than one post processors.<br/>
* <br/>
* Unfortunately it won't trigger {@link org.springframework.security.core.session.SessionDestroyedEvent} because
* that is triggered by {@link org.apache.catalina.session.StandardSessionFacade#invalidate()} in Tomcat and
* for mocks it's handled by @{{@link MockHttpSession#invalidate()}} so the log out message won't be visible for tests.
*/
public final class SessionLogoutRequestBuilder implements
ConfigurableSmartRequestBuilder<SessionLogoutRequestBuilder>, Mergeable {
private final List<RequestPostProcessor> postProcessors = new ArrayList<>();
private String logoutUrl = "/logout";
private MockHttpSession session;
private SessionLogoutRequestBuilder() {
this.postProcessors.add(csrf());
}
static SessionLogoutRequestBuilder sessionLogout() {
return new SessionLogoutRequestBuilder();
}
@Override
public MockHttpServletRequest buildRequest(final ServletContext servletContext) {
return post(this.logoutUrl).session(session).buildRequest(servletContext);
}
public SessionLogoutRequestBuilder logoutUrl(final String logoutUrl) {
this.logoutUrl = logoutUrl;
return this;
}
public SessionLogoutRequestBuilder session(final MockHttpSession session) {
Assert.notNull(session, "'session' must not be null");
this.session = session;
return this;
}
@Override
public boolean isMergeEnabled() {
return true;
}
@SuppressWarnings("unchecked")
@Override
public Object merge(final Object parent) {
if (parent == null) {
return this;
}
if (parent instanceof MockHttpServletRequestBuilder) {
final MockHttpServletRequestBuilder parentBuilder = (MockHttpServletRequestBuilder) parent;
if (this.session == null) {
this.session = (MockHttpSession) ReflectionTestUtils.getField(parentBuilder, "session");
}
final List postProcessors = (List) ReflectionTestUtils.getField(parentBuilder, "postProcessors");
this.postProcessors.addAll(0, (List<RequestPostProcessor>) postProcessors);
} else if (parent instanceof SessionLogoutRequestBuilder) {
final SessionLogoutRequestBuilder parentBuilder = (SessionLogoutRequestBuilder) parent;
if (!StringUtils.hasText(this.logoutUrl)) {
this.logoutUrl = parentBuilder.logoutUrl;
}
if (this.session == null) {
this.session = parentBuilder.session;
}
this.postProcessors.addAll(0, parentBuilder.postProcessors);
} else {
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
}
return this;
}
@Override
public SessionLogoutRequestBuilder with(final RequestPostProcessor postProcessor) {
Assert.notNull(postProcessor, "postProcessor is required");
this.postProcessors.add(postProcessor);
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
for (final RequestPostProcessor postProcessor : this.postProcessors) {
request = postProcessor.postProcessRequest(request);
if (request == null) {
throw new IllegalStateException(
"Post-processor [" + postProcessor.getClass().getName() + "] returned null");
}
}
return request;
}
}
After calling the performLogin
operation all your request in the test will be automatically performed as logged in user.
Somewhy solution with principal didn't worked for me, thus, I'd like to mention another way out:
mockMvc.perform(get("your/url/{id}", 5).with(user("anyUserName")))
Yet another way... I use the following annotations:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners(listeners={ServletTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
WithSecurityContextTestExcecutionListener.class})
@WithMockUser
public class WithMockUserTests {
...
}
(source)