Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MockMvc.perform throws an ConcurrentModificationException with parallel streams if MockHttpServletRequestBuilder cached

I'm writting some spring integration tests to test Spring Security in my application. I use RequestPostProcessor to create test users with different authorities. Also I cache them for reuse in all tests. See code below:

public final class Users {
    public static final RequestPostProcessor
        ANONYMOUS = anonymous();

    public static final RequestPostProcessor
        PERMISSIONS_READ = buildUser(Permissions.PERMISSIONS_READ);
    public static final RequestPostProcessor
        PERMISSIONS_WRITE = buildUser(Permissions.PERMISSIONS_WRITE);
    public static final RequestPostProcessor
        PERMISSIONS_DELETE = buildUser(Permissions.PERMISSIONS_DELETE);

    public static final RequestPostProcessor
        ROLES_READ = buildUser(Permissions.ROLES_READ);
    public static final RequestPostProcessor
        ROLES_WRITE = buildUser(Permissions.ROLES_WRITE);
    public static final RequestPostProcessor
        ROLES_DELETE = buildUser(Permissions.ROLES_DELETE);

    public static final RequestPostProcessor
        USERS_READ = buildUser(Permissions.USERS_READ);
    public static final RequestPostProcessor
        USERS_WRITE = buildUser(Permissions.USERS_WRITE);
    public static final RequestPostProcessor
        USERS_DELETE = buildUser(Permissions.USERS_DELETE);


    private Users() {}

    private static RequestPostProcessor buildUser(Permissions permission) {
        return buildUser(permission.toString(), permission.toString());
    }

    private static RequestPostProcessor buildUser(String name, String... authorities) {
        return user(name).authorities(SecurityUtils.authoritiesFromStrings(authorities));
    }
}

And when I use them in a test, I've got an ConcurrentModificationException.

Usage:

....................
@Autowired private WebApplicationContext context;
@Autowired private Filter springSecurityFilterChain;
MockMvc mvc = MockMvcBuilders
        .webAppContextSetup(context)
        .addFilters(springSecurityFilterChain)
        .build();
....................
MockHttpServletRequestBuilder req = get("some-url");
mvc.perform(req.with(Users.ANONYMOUS))
        .andExpect(status().isFound())
        .andExpect(header().string("Location", "login-url"));
Stream.of(Users.PERMISSIONS_WRITE, Users.PERMISSIONS_DELETE,
        Users.ROLES_WRITE, Users.ROLES_DELETE,
        Users.USERS_WRITE, Users.USERS_DELETE)
        .parallel()
        .forEach(Unchecked.consumer(user -> mvc.perform(req.with(user)) //Exception is here and caused by .with(user)
                .andExpect(status().isForbidden())));
....................

An exception:

....................
Caused by: java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder.postProcessRequest(MockHttpServletRequestBuilder.java:754)
at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:145)
at com.ipan.fin.man.integrational.PermissionsTest.lambda$checkReadSecurity$5(PermissionsTest.java:162)//line with my code
....................

Exception is thrown in org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder because I'm caching it (MockHttpServletRequestBuilder req) and using in multiple streams. So, in case, when one stream iterate over postProcessors

@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
    for (RequestPostProcessor postProcessor : this.postProcessors) { //here
        request = postProcessor.postProcessRequest(request);
        if (request == null) {
            throw new IllegalStateException(
                    "Post-processor [" + postProcessor.getClass().getName() + "] returned null");
        }
    }
    return request;
}

And the second one adds new postProcessor to postProcessors

@Override
public MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor) {
    Assert.notNull(postProcessor, "postProcessor is required");
    this.postProcessors.add(postProcessor); //here
    return this;
}

ConcurrentModificationException will be thrown.

As I understand in my code above it's not allowed to cache MockHttpServletRequestBuilder because of two reasons:

  1. It's not thread safe
  2. MockHttpServletRequestBuilder.with call adds new RequestPostProcessor, not replaces the old one as I expected.

Am I right?

P.S. With sequental streams the tests works fine and it looks like call of MockHttpServletRequestBuilder.with replaces the old RequestPostProcessor object, because I always get correct test result (response status from a server)

like image 317
Bohdan Petrenko Avatar asked Mar 27 '26 07:03

Bohdan Petrenko


1 Answers

Yes, your analysis is correct: MockHttpServletRequestBuilder is not designed to be used concurrently like that.

There isn't actually any real noticeable overhead in creating a MockHttpServletRequest anyway.

So, I'd suggest you simply create a new request each time you need one, and the use of parallel streams doesn't really buy you much (if anything at all) with such tests.

In summary, try not to over-engineer your tests. ;-)

Regards,

Sam (author of the Spring TestContext Framework)

like image 178
Sam Brannen Avatar answered Mar 29 '26 21:03

Sam Brannen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!