Describe the bug
mockJwt() WebTestClientConfigurer does not seem to work with MockMvcWebTestClient that was introduced in Spring 5.3 as documented.
To Reproduce
@Test
void getMessagesWebTestClient() {
final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc)
.build();
testClient.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read")))
.get()
.uri("/messages")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0]").isEqualTo("hello")
.jsonPath("$[1]").isEqualTo("world"); ;
}
throws the exception below
java.lang.NullPointerException: Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null
at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$JwtMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:540)
at org.springframework.test.web.reactive.server.DefaultWebTestClientBuilder.apply(DefaultWebTestClientBuilder.java:247)
at org.springframework.test.web.reactive.server.DefaultWebTestClient.mutateWith(DefaultWebTestClient.java:160)
at com.example.demojwttest.MessageControllerTest.getMessagesWebTestClient(MessageControllerTest.java:51)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
while the equivalent following tests work
// MockMvc
@Test
void getMessages() throws Exception {
this.mockMvc.perform(get("/messages")
.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0]").value("hello"))
.andExpect(jsonPath("$[1]").value("world"));
}
// @WithMockUser
@Test
@WithMockUser(authorities = "SCOPE_message:read")
void getMessagesWebTestClientWithMockUser() {
final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc)
.build();
testClient.get()
.uri("/messages")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0]").isEqualTo("hello")
.jsonPath("$[1]").isEqualTo("world"); ;
}
Expected behavior
The first example works
Sample https://github.com/making/spring-security-gh-9257
Comment From: jzheaux
While there may be a wider picture to consider, the basic reason for the NPE is because MockMvcWebTestClient.bindTo ultimately wires WebTestClient with a ClientHttpConnector instead of a WebHttpHandlerAdapter. Spring Security uses this adapter in order to introduce WebFilters into the mock client.
Comment From: rwinch
UPDATED: Fixed workaround, demo CSRF support workaround, and provide link to complete example.
@making Thanks for the report. We will look into a proper solution.
In the meantime, you can work around it using TestSecurityContextHolder.setAuthentication(Authentication). I put together a complete example in the gh-9257-webtestclient branch of my sample repository. You can see an excerpt below :
@SpringBootTest
@AutoConfigureMockMvc
public class WebTestClientTest {
WebTestClient client;
@MockBean
// mock the JwtDecoder so that the jwks is not resolved since no AuthZ Server Setup
JwtDecoder jwtDecoder;
// Override the CsrfTokenRepository. Must explicitly wire CsrfTokenRepository Bean into DSL for this to work
@MockBean
CsrfTokenRepository csrfTokenRepository;
DefaultCsrfToken csrf = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "123");
@Autowired
void setMockMvc(MockMvc mockMvc) {
this.client = MockMvcWebTestClient.bindTo(mockMvc)
.build();
}
@BeforeEach
void setupCsrf() {
given(this.csrfTokenRepository.generateToken(any())).willReturn(csrf);
}
private Consumer<HttpHeaders> csrf() {
return (headers) -> headers.set(csrf.getHeaderName(), csrf.getToken());
}
@Test
void getWhenAuthenticated() {
TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());
client
.get()
.uri("/")
.exchange()
.expectStatus().isOk();
}
@Test
void getWhenNotAuthenticated() {
client
.get()
.uri("/")
.exchange()
.expectStatus().is4xxClientError();
}
@Test
void csrfWhenNoToken() {
TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());
client
.post()
.uri("/")
.exchange()
.expectStatus().is4xxClientError();
}
@Test
void csrfWhenValidToken() {
TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());
client
.post()
.uri("/")
.headers(csrf())
.exchange()
.expectStatus().isOk();
}
private static JwtAuthenticationToken jwtAuthenticationToken() {
return new JwtAuthenticationToken(jwt().build(), AuthorityUtils.createAuthorityList("SCOPE_message:read"));
}
public static Jwt.Builder jwt() {
// @formatter:off
return Jwt.withTokenValue("token")
.header("alg", "none")
.audience(Arrays.asList("https://audience.example.org"))
.expiresAt(Instant.MAX)
.issuedAt(Instant.MIN)
.issuer("https://issuer.example.org")
.jti("jti")
.notBefore(Instant.MIN)
.subject("mock-test-subject");
// @formatter:on
}
}
Comment From: rwinch
@rstoyanchev Any ideas on how we can get Spring Security to integrate with the MockMvcWebTestClient support? With WebTestClient we typically add a WebFilter which can access the attribute on ServerWebExchange and that sets up the context, but it doesn't appear there is a way to do this when using MockMvcWebTestClient.
We need to be able to access the attribute on the ServerWebExchange that is populated by the mutateWith method and use it to setup the SecurityContext.
Comment From: rstoyanchev
I think there is some misunderstanding. As of 5.3 WebTestClient can be used to exercise not only WebFlux but also WebMvc controllers with MockMvc as the server. In other words it's all MockMvc, and unrelated to WebFlux, and therefore any WebFlux related hooks do not apply.
For the most part what can be done directly with MockMvc can also be done via MockMvcWebTestClient. For example, you can configure and apply extension hooks to MockMvc just the same. However since it it a client, it is not as easy to modify individual requests with some special support via server side Filter. I can work with you more closely if we want to find a way to improve that.
For further reference the sections on WebTestClient and MockMvc have been updated to reflect this. You can also see all the framework samples tests for MockMvc ported to use with WebTestClient.
Comment From: gursahibsahni
@making Thanks for the report. We will look into a proper solution. In the meantime, you can work around it using:
java @Test void getMessagesWebTestClient() { TestingAuthenticationToken authentication = new TestingAuthenticationToken("a", "b", "SCOPE_message:read"); TestSecurityContextHolder.setAuthentication(authentication); final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc) .build(); testClient.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read"))) .get() .uri("/messages") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$[0]").isEqualTo("hello") .jsonPath("$[1]").isEqualTo("world"); ; }
This still doesn't work for me.
Comment From: membersound
Similar also happens in a test with @Autowired WebTestClient webTestClient and executing a webTestClient.mutateWith(csrf()).post()...:
java.lang.NullPointerException
at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$CsrfMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:259)
at org.springframework.test.web.reactive.server.DefaultWebTestClientBuilder.apply(DefaultWebTestClientBuilder.java:265)
at org.springframework.test.web.reactive.server.DefaultWebTestClient.mutateWith(DefaultWebTestClient.java:167)
Comment From: soasada
I'm experiencing the same behaviour than @membersound, do you have any workaround?
Comment From: rwinch
@gursahibsahni Sorry the workaround I posted previously, should have removed the mutateWith method. I've updated the example and provided a link to a repository that demonstrates the workaround in its entirety.
@soasada @membersound I've updated the sample above to demonstrate how to workaround CSRF test support not working too.
@rstoyanchev Thanks for the reply. I'd like to figure out a way that Spring Security users can use WebTestClient using the same APIs for WebMvc and WebFlux backends. It is confusing that mutateWith does not work for MockMvc based tests. What's more is there is currently no way to use many of Spring Security's test features when WebTestClient + MockMvc.
Comment From: nasrmohammad4804
hi i have same issue with webTestClient and get null pointer exception i think because don't use jwt for webTestClient
and i solved this with add @AutoConfigureWebTestClient as class level annotation for integration test
Comment From: eiswind
The suggested workaround does not work with the current spring security versions (6.0.2), as csrf token handling has changed quite a bit. So far I was not able to create a working solution.
Comment From: siaavush
as @eiswind mentioned, I have the same issue with spring boot 3 and security 6 this is my test class:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@AutoConfigureWireMock(stubs = ["classpath:/mappings/authservice"], port = 0)
@TestPropertySource(
properties = [
"spring.flyway.enabled=false",
],
)
@Testcontainers
@ExtendWith(SpringExtension::class)
@ActiveProfiles("test")
@AutoConfigureWebTestClient
class ServiceIntegrationTest {
@Test
fun test() {
webTestClient.mutateWith(mockJwt().jwt(JwtUtils.generateJwt()))
.mutateWith(csrf())
.post()
.uri("/api/planning/stores/$businessUnitId/units")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isForbidden
}
}
and this is the error I am getting:
Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null
java.lang.NullPointerException: Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null
at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$JwtMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:540)
none of the workarounds seem effective on it
Comment From: justin-tay
It's currently not possible to do this cleanly with MockMvc and using the mutateWith api because the functionality is in MockMvcHttpConnector and the WebTestClient.Builder does not expose a method to set a modified MockMvcHttpConnector as the ClientHttpConnector is final in the builder.
The best workaround currently if you still want to use WebTestClient instead of just using the MockMvc API is https://github.com/spring-projects/spring-security/issues/9304#issuecomment-841495717.
Some ideas
* The WebTestClient.Builder needs to expose a method to set the ClientHttpConnector or some ClientHttpConnectorBuilder
* The MockMvcHttpConnector needs to store the RequestPostProcessors to be applied on the MockHttpServletRequestBuilder and a means to create a new MockMvcHttpConnector with the existing RequestPostProcessors and additional ones
* The MockMvcHttpConnector needs to apply the RequestPostProcessors in adaptRequest.
Comment From: justin-tay
As a workaround I wrote a simple builder which can be used with the org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.
Edit: Have edited the builder as when I was doing testing I realised that what I typically wanted to use was the filters customised by Spring Boot in the MockMvcBuilder and not binding to the WebApplicationContext with springSecurity.
package com.example;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.springframework.http.HttpMethod;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder;
import org.springframework.web.context.WebApplicationContext;
public class DefaultMockMvcWebTestClient implements WebTestClient {
private final Function<MockHttpServletRequestBuilder, WebTestClient> builder;
private final List<RequestPostProcessor> requestPostProcessors = new ArrayList<>();
public DefaultMockMvcWebTestClient(WebApplicationContext context) {
this.builder = (requestBuilder) -> MockMvcWebTestClient.bindToApplicationContext(context)
.apply(springSecurity()).defaultRequest(requestBuilder);
}
public DefaultMockMvcWebTestClient(AbstractMockMvcBuilder<?> mockMvcBuilder) {
this.builder = (requestBuilder) -> MockMvcWebTestClient
.bindTo(mockMvcBuilder.defaultRequest(requestBuilder).build()).build();
}
public DefaultMockMvcWebTestClient(Function<MockHttpServletRequestBuilder, WebTestClient> builder) {
this.builder = builder;
}
public DefaultMockMvcWebTestClient(DefaultMockMvcWebTestClient copy) {
this.builder = copy.builder;
this.requestPostProcessors.addAll(copy.requestPostProcessors);
}
public DefaultMockMvcWebTestClient with(RequestPostProcessor requestPostProcessor) {
this.requestPostProcessors.add(requestPostProcessor);
return this;
}
public DefaultMockMvcWebTestClient mutateWith(RequestPostProcessor requestPostProcessor) {
DefaultMockMvcWebTestClient copy = new DefaultMockMvcWebTestClient(this);
return copy.with(requestPostProcessor);
}
public WebTestClient build() {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/");
requestPostProcessors.stream().forEach(requestBuilder::with);
return this.builder.apply(requestBuilder);
}
@Override
public RequestHeadersUriSpec<?> get() {
return build().get();
}
@Override
public RequestHeadersUriSpec<?> head() {
return build().head();
}
@Override
public RequestBodyUriSpec post() {
return build().post();
}
@Override
public RequestBodyUriSpec put() {
return build().put();
}
@Override
public RequestBodyUriSpec patch() {
return build().patch();
}
@Override
public RequestHeadersUriSpec<?> delete() {
return build().delete();
}
@Override
public RequestHeadersUriSpec<?> options() {
return build().options();
}
@Override
public RequestBodyUriSpec method(HttpMethod method) {
return build().method(method);
}
@Override
public Builder mutate() {
return build().mutate();
}
@Override
public WebTestClient mutateWith(WebTestClientConfigurer configurer) {
return build().mutateWith(configurer);
}
}
It can be used something like
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
public class MyTest {
DefaultMockMvcWebTestClient webTestClient;
@Autowired
public void setupClient(AbstractMockMvcBuilder<?> mockMvcBuilder) {
this.webTestClient = new DefaultMockMvcWebTestClient(mockMvcBuilder);
}
@Test
public void test() {
this.webTestClient.mutateWith(oidcLogin()).get() // ...
}
// ...
}
Comment From: rstoyanchev
@rwinch as per my https://github.com/spring-projects/spring-framework/pull/30233#pullrequestreview-1637375454, I think we could do something with the WebTestClient#mutate(WebTestClientConfigurer) hook. For WebFlux use, I believe Spring Security is using the WebHttpHandlerBuilder argument to modify the server. For WebMvc use, it would use the ClientHttpConnector argument which would be the MockMvcClientHttpConnector. We'll need to make some changes in Spring Framework to allow MockMvcClientHttpConnector to be mutated and that should make it possible for Spring Security to address this issue.
Comment From: IsaacHu
Any target date for the final solution of this issue? We also suffer in this issue when using WebTestClient to test Spring MVC after involved security.
@WebMvcTest(controllers = XxxController.class)
@WithMockUser
class XxxControllerTest {
@Autowired
private MockMvc mockMvc;
private WebTestClient webTestClient;
@BeforeEach
void setupWebClient() {
webTestClient = MockMvcWebTestClient
.bindTo(mockMvc)
.build()
// will encounter NPE if -> .mutateWith(csrf())
;
}
Comment From: glorfidev
hi i have same issue with webTestClient and get null pointer exception i think because don't use jwt for webTestClient and i solved this with add @AutoConfigureWebTestClient as class level annotation for integration test
Could you provide a source code? I think that @AutoConfigureWebTestClient should not have an impact on the configuration of webtestclient if SpringBootTest is used with RANDOM_PORT
Comment From: Bourg
We just worked our way through this same rabbit hole at my job and came up with a solution that works in the latest versions.
For us, the key pieces were:
- Configure your own
WebTestClientin the way described below - do not rely on theWebTestClientthat is auto-configured with@SpringBootTest(webEnvironment = RANDOM_PORT)or similar - Use the Spring Security test context annotations to mock the security context - do not try to use
.mutateWithon theWebTestClient
Set your WebTestClient up like this in your test class:
private WebTestClient webTestClient;
@Autowired
public void setWebApplicationContext(final WebApplicationContext context) {
webTestClient = MockMvcWebTestClient
.bindToApplicationContext(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
this portion of the solution was found in https://github.com/spring-projects/spring-security/issues/9304#issuecomment-841495717
Then, you can annotate your test classes / methods with @WithMockUser or any custom annotation created with @MockSecurityContext + a corresponding implementation of WithSecurityContextFactory (see https://docs.spring.io/spring-security/reference/servlet/test/method.html#test-method-withsecuritycontext)
Full working test class calling a controller that echoes the principal's name:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebTestClientMockUserTest {
private WebTestClient webTestClient;
@Autowired
public void setWebApplicationContext(final WebApplicationContext context) {
webTestClient = MockMvcWebTestClient
.bindToApplicationContext(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
@WithMockUser("Austin")
void testWithMockUser() {
webTestClient
.get()
.uri("/whoami")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("You are Austin");
}
}
I think the thing that is causing everyone the most trouble is that you need to mock the security in a WebMVC-first way, not the WebFlux-first way exposed on WebTestClient.mutateWith(), which as all the above comments point out is only designed to operate on the reactive security stack.
Comment From: javolek
Since version 6.1 there seems to be a possibility to use RequestPostProcessor of MockMvc for a MockMvcHttpConnector inside WebTestClientConfigurer - see: https://github.com/spring-projects/spring-framework/issues/31298. So it is possible to solve it the following way (kotlin):
fun WebTestClient.mutateWith(rpp: RequestPostProcessor) =
this.mutateWith { builder, _, connector ->
(connector as? MockMvcHttpConnector)?.let { mockMvcConnector ->
builder.clientConnector(mockMvcConnector.with(listOf(rpp)))
}
}
This is simple kotlin extension function to WebTestClient allowing passing RequestPostProcessor instace (i.e. SecurityMockMvcRequestPostProcessors.jwt) to mutateWith method. In Java one would need to implement WebTestClientConfigurer. Is there a simpler way to do it?