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:
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)
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With