Steps to Reproduce
- Download this Spring Boot v4.0.0-M3 app (be sure to use the
rest-test-client-spring-securitybranch) - Run the tests in
ItemControllerTests
Expected Outcome
Both tests should pass, because they both attempt to access a secured endpoint without authenticating
Actual Outcome
getWithMockMvcpasses because the expected 401 (UNAUTHORIZED) status is returnedgetWithRestTestClientfails because a 200 (OK) status is returned
AFAIK, both of these tests should return the same status because they are using the same configuration.
Discussion
The demo application depends on org.springframework.boot:spring-boot-starter-security. It doesn't define an explicit security configuration, so by default all endpoints should require authentication.
I observed the same behaviour in a production application that does provide an explicit security configuration i.e. endpoints that require authentication (but are not restricted to a specific role via @PreAuthorize, @Secured, etc.) are correctly inaccessible to an unauthenticated MockMvcTester, but incorrectly accessible to an unauthenticated RestTestClient. Some debugging confirmed that the configured security filters are invoked when a request is made by MockMvcTester, but not when the request originates from RestTestClient.
Incidentally, if the Spring Security starter dependency is removed, both clients can access the endpoint without authenticating.
Comment From: wilkinsona
You're defining the RestTestClient bean yourself and binding it directly to the application context. This bypasses the Spring Security filter configuration that Spring Boot has configured on the auto-configured MockMvc instance. You can avoid the problem by binding the RestTestClient instance to the auto-configured MockMvc instance instead:
diff --git a/src/test/java/com/example/demo/RestSpringBeans.java b/src/test/java/com/example/demo/RestSpringBeans.java
index 567ed40..479745b 100644
--- a/src/test/java/com/example/demo/RestSpringBeans.java
+++ b/src/test/java/com/example/demo/RestSpringBeans.java
@@ -6,8 +6,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
+import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.RestTestClient;
-import org.springframework.web.context.WebApplicationContext;
/**
* This class defines Spring beans that are useful for testing the application via it's REST API i.e. by making
@@ -21,8 +21,8 @@ import org.springframework.web.context.WebApplicationContext;
public class RestSpringBeans {
@Bean
- RestTestClient restTestClient(WebApplicationContext context, JacksonJsonHttpMessageConverter jsonMessageConverter) {
- return RestTestClient.bindToApplicationContext(context)
+ RestTestClient restTestClient(MockMvc mockMvc, JacksonJsonHttpMessageConverter jsonMessageConverter) {
+ return RestTestClient.bindTo(mockMvc)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.configureMessageConverters(clientBuilder ->
clientBuilder.registerDefaults().jsonMessageConverter(jsonMessageConverter)
Comment From: wilkinsona
In Spring Boot 4.0.0-RC1 the testing support has been modularized, requiring some tweaks to imports and dependencies. Here's the diff for 4.0.0-RC1 including the change above as well:
diff --git a/build.gradle b/build.gradle
index acd7bf9..45649b1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '4.0.0-M3'
+ id 'org.springframework.boot' version '4.0.0-RC1'
id 'io.spring.dependency-management' version '1.1.6'
}
@@ -18,10 +18,11 @@ repositories {
}
dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-security'
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-restclient'
+ testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
+ testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}
tasks.named('test') {
diff --git a/src/test/java/com/example/demo/ItemControllerTests.java b/src/test/java/com/example/demo/ItemControllerTests.java
index 1b8bf3b..2d641ef 100644
--- a/src/test/java/com/example/demo/ItemControllerTests.java
+++ b/src/test/java/com/example/demo/ItemControllerTests.java
@@ -2,7 +2,7 @@ package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
diff --git a/src/test/java/com/example/demo/RestSpringBeans.java b/src/test/java/com/example/demo/RestSpringBeans.java
index 567ed40..479745b 100644
--- a/src/test/java/com/example/demo/RestSpringBeans.java
+++ b/src/test/java/com/example/demo/RestSpringBeans.java
@@ -6,8 +6,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
+import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.RestTestClient;
-import org.springframework.web.context.WebApplicationContext;
/**
* This class defines Spring beans that are useful for testing the application via it's REST API i.e. by making
@@ -21,8 +21,8 @@ import org.springframework.web.context.WebApplicationContext;
public class RestSpringBeans {
@Bean
- RestTestClient restTestClient(WebApplicationContext context, JacksonJsonHttpMessageConverter jsonMessageConverter) {
- return RestTestClient.bindToApplicationContext(context)
+ RestTestClient restTestClient(MockMvc mockMvc, JacksonJsonHttpMessageConverter jsonMessageConverter) {
+ return RestTestClient.bindTo(mockMvc)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.configureMessageConverters(clientBuilder ->
clientBuilder.registerDefaults().jsonMessageConverter(jsonMessageConverter)
Comment From: donalmurtagh
@wilkinsona thanks very much for the response, which was well above-and-beyond the call of duty.
A minor follow-up: if an app is only using RestTestClient in the tests, i.e. RestClient is not used in the app itself, should the dependency
testImplementation 'org.springframework.boot:spring-boot-starter-restclient-test'
be used instead of
testImplementation 'org.springframework.boot:spring-boot-starter-restclient'
Aside: how did you convert the diff output to markdown?
Comment From: wilkinsona
Neither. org.springframework.boot:spring-boot-starter-restclient is for main code that's using RestClient, in other words your app has some HTTP client functionality. org.springframework.boot:spring-boot-starter-restclient-test provides support for testing code that's using RestClient.
Instead, you can use a test-scoped dependency on spring-boot-resttestclient to add Boot's support for RestTestClient to your app's tests if it hasn't already been pulled in by one of the other starters.