Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to login a user with spring 3.2 new mvc testing

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();
}
like image 580
NimChimpsky Avatar asked Jan 13 '13 21:01

NimChimpsky


People also ask

What method on the authentication object can be used to obtain the username?

In order to get the current username, you first need a SecurityContext , which is obtained from SecurityContextHolder . This SecurityContext keep the user details in an Authentication object, which can be obtained by calling getAuthentication() method.

How do I enable HTTP Security in spring?

The first thing you need to do is add Spring Security to the classpath. The WebSecurityConfig class is annotated with @EnableWebSecurity to enable Spring Security's web security support and provide the Spring MVC integration.


5 Answers

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.

like image 197
seanbdoherty Avatar answered Oct 06 '22 23:10

seanbdoherty


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);
like image 32
Arjan Avatar answered Oct 07 '22 00:10

Arjan


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);
like image 23
Andres Olarte Avatar answered Oct 06 '22 22:10

Andres Olarte


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")))
like image 35
ryzhman Avatar answered Oct 06 '22 23:10

ryzhman


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.

like image 23
Nagy Attila Avatar answered Oct 07 '22 00:10

Nagy Attila