Merge 99afe38c03
into b256babad5
This commit is contained in:
commit
bf197e8f9e
|
@ -352,6 +352,7 @@
|
|||
*** xref:testing/testcontext-framework/support-classes.adoc[]
|
||||
*** xref:testing/testcontext-framework/aot.adoc[]
|
||||
** xref:testing/webtestclient.adoc[]
|
||||
** xref:testing/resttestclient.adoc[]
|
||||
** xref:testing/mockmvc.adoc[]
|
||||
*** xref:testing/mockmvc/overview.adoc[]
|
||||
*** xref:testing/mockmvc/setup-options.adoc[]
|
||||
|
|
|
@ -0,0 +1,440 @@
|
|||
[[resttestclient]]
|
||||
= RestTestClient
|
||||
|
||||
`RestTestClient` is an HTTP client designed for testing server applications. It wraps
|
||||
Spring's xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and uses it to perform requests
|
||||
but exposes a testing facade for verifying responses. `RestTestClient` can be used to
|
||||
perform end-to-end HTTP tests. It can also be used to test Spring MVC
|
||||
applications without a running server via mock server request and response objects.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resttestclient-setup]]
|
||||
== Setup
|
||||
|
||||
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one
|
||||
of several mock server setup choices or a connection to a live server.
|
||||
|
||||
|
||||
|
||||
[[resttestclient-controller-config]]
|
||||
=== Bind to Controller
|
||||
|
||||
This setup allows you to test specific controller(s) via mock request and response objects,
|
||||
without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTestClient client =
|
||||
RestTestClient.bindToController(new TestController()).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val client = RestTestClient.bindToController(TestController()).build()
|
||||
----
|
||||
======
|
||||
|
||||
[[resttestclient-context-config]]
|
||||
=== Bind to `ApplicationContext`
|
||||
|
||||
This setup allows you to load Spring configuration with Spring MVC
|
||||
infrastructure and controller declarations and use it to handle requests via mock request
|
||||
and response objects, without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig.class) // <1>
|
||||
class MyTests {
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(ApplicationContext context) { // <2>
|
||||
client = RestTestClient.bindToApplicationContext(context).build(); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `RestTestClient`
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig::class) // <1>
|
||||
class MyTests {
|
||||
|
||||
lateinit var client: RestTestClient
|
||||
|
||||
@BeforeEach
|
||||
fun setUp(context: ApplicationContext) { // <2>
|
||||
client = RestTestClient.bindToApplicationContext(context).build() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `RestTestClient`
|
||||
======
|
||||
|
||||
[[resttestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RouterFunction<?> route = ...
|
||||
client = RestTestClient.bindToRouterFunction(route).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val route: RouterFunction<*> = ...
|
||||
val client = RestTestClient.bindToRouterFunction(route).build()
|
||||
----
|
||||
======
|
||||
|
||||
[[resttestclient-server-config]]
|
||||
=== Bind to Server
|
||||
|
||||
This setup connects to a running server to perform full, end-to-end HTTP tests:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
[[resttestclient-client-config]]
|
||||
=== Client Config
|
||||
|
||||
In addition to the server setup options described earlier, you can also configure client
|
||||
options, including base URL, default headers, client filters, and others. These options
|
||||
are readily available following `bindToServer()`. For all other configuration options,
|
||||
you need to use `configureClient()` to transition from server to client configuration, as
|
||||
follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToController(TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
[[resttestclient-tests]]
|
||||
== Writing Tests
|
||||
|
||||
`RestTestClient` provides an API identical to xref:integration/rest-clients.adoc#rest-restclient[`RestClient`]
|
||||
up to the point of performing a request by using `exchange()`.
|
||||
|
||||
After the call to `exchange()`, `RestTestClient` diverges from the `RestClient` and
|
||||
instead continues with a workflow to verify responses.
|
||||
|
||||
To assert the response status and headers, use the following:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
----
|
||||
======
|
||||
|
||||
If you would like for all expectations to be asserted even if one of them fails, you can
|
||||
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
|
||||
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
|
||||
JUnit Jupiter.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectAll(
|
||||
spec -> spec.expectStatus().isOk(),
|
||||
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectAll(
|
||||
{ spec -> spec.expectStatus().isOk() },
|
||||
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) }
|
||||
)
|
||||
----
|
||||
======
|
||||
|
||||
You can then choose to decode the response body through one of the following:
|
||||
|
||||
* `expectBody(Class<T>)`: Decode to single object.
|
||||
* `expectBody()`: Decode to `byte[]` for xref:testing/resttestclient.adoc#resttestclient-json[JSON Content] or an empty body.
|
||||
|
||||
|
||||
If the built-in assertions are insufficient, you can consume the object instead and
|
||||
perform any other assertions:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> {
|
||||
// custom assertions (for example, AssertJ)...
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody<Person>()
|
||||
.consumeWith {
|
||||
// custom assertions (for example, AssertJ)...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Or you can exit the workflow and obtain a `EntityExchangeResult`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectBody<Person>()
|
||||
.returnResult()
|
||||
----
|
||||
======
|
||||
|
||||
TIP: When you need to decode to a target type with generics, look for the overloaded methods
|
||||
that accept
|
||||
{spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
|
||||
instead of `Class<T>`.
|
||||
|
||||
|
||||
|
||||
[[resttestclient-no-content]]
|
||||
=== No Content
|
||||
|
||||
If the response is not expected to have content, you can assert that as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(person)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(person)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty()
|
||||
----
|
||||
======
|
||||
|
||||
If you want to ignore the response content, the following releases the content without
|
||||
any assertions:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound
|
||||
.expectBody<Unit>()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
[[resttestclient-json]]
|
||||
=== JSON Content
|
||||
|
||||
You can use `expectBody()` without a target type to perform assertions on the raw
|
||||
content rather than through higher level Object(s).
|
||||
|
||||
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
======
|
||||
|
||||
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason")
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
|
@ -20,6 +20,9 @@ import java.net.URI;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
@ -31,6 +34,7 @@ import org.springframework.mock.http.client.MockClientHttpRequest;
|
|||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
@ -67,8 +71,14 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory
|
|||
HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders, byte[] requestBody) {
|
||||
|
||||
try {
|
||||
Cookie[] cookies = parseCookies(requestHeaders.get(HttpHeaders.COOKIE));
|
||||
MockHttpServletRequestBuilder requestBuilder = request(httpMethod, uri)
|
||||
.content(requestBody).headers(requestHeaders);
|
||||
if (cookies.length > 0) {
|
||||
requestBuilder.cookie(cookies);
|
||||
}
|
||||
MockHttpServletResponse servletResponse = this.mockMvc
|
||||
.perform(request(httpMethod, uri).content(requestBody).headers(requestHeaders))
|
||||
.perform(requestBuilder)
|
||||
.andReturn()
|
||||
.getResponse();
|
||||
|
||||
|
@ -92,6 +102,22 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory
|
|||
}
|
||||
}
|
||||
|
||||
private static Cookie[] parseCookies(@Nullable List<String> headerValues) {
|
||||
if (headerValues == null) {
|
||||
return new Cookie[0];
|
||||
}
|
||||
return headerValues.stream()
|
||||
.flatMap(header -> StringUtils.commaDelimitedListToSet(header).stream())
|
||||
.map(MockMvcClientHttpRequestFactory::parseCookie)
|
||||
.toArray(Cookie[]::new);
|
||||
}
|
||||
|
||||
private static Cookie parseCookie(String cookie) {
|
||||
String[] parts = StringUtils.split(cookie, "=");
|
||||
Assert.isTrue(parts != null && parts.length == 2, "Invalid cookie: '" + cookie + "'");
|
||||
return new Cookie(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
private HttpHeaders getResponseHeaders(MockHttpServletResponse response) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String name : response.getHeaderNames()) {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
|
||||
import org.springframework.test.web.servlet.DispatcherServletCustomizer;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Base class for implementations of {@link RestTestClient.MockMvcServerSpec}
|
||||
* that simply delegates to a {@link org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder} supplied by
|
||||
* the concrete subclasses.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @param <B> the type of the concrete subclass spec
|
||||
*/
|
||||
abstract class AbstractMockMvcServerSpec<B extends RestTestClient.MockMvcServerSpec<B>>
|
||||
implements RestTestClient.MockMvcServerSpec<B> {
|
||||
|
||||
@Override
|
||||
public <T extends B> T filters(Filter... filters) {
|
||||
getMockMvcBuilder().addFilters(filters);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final <T extends B> T filter(Filter filter, String... urlPatterns) {
|
||||
getMockMvcBuilder().addFilter(filter, urlPatterns);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends B> T defaultRequest(RequestBuilder requestBuilder) {
|
||||
getMockMvcBuilder().defaultRequest(requestBuilder);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends B> T alwaysExpect(ResultMatcher resultMatcher) {
|
||||
getMockMvcBuilder().alwaysExpect(resultMatcher);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends B> T dispatchOptions(boolean dispatchOptions) {
|
||||
getMockMvcBuilder().dispatchOptions(dispatchOptions);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends B> T dispatcherServletCustomizer(DispatcherServletCustomizer customizer) {
|
||||
getMockMvcBuilder().addDispatcherServletCustomizer(customizer);
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends B> T apply(MockMvcConfigurer configurer) {
|
||||
getMockMvcBuilder().apply(configurer);
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends B> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the concrete {@link ConfigurableMockMvcBuilder} to delegate
|
||||
* configuration methods and to use to create the {@link MockMvc}.
|
||||
*/
|
||||
protected abstract ConfigurableMockMvcBuilder<?> getMockMvcBuilder();
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder configureClient() {
|
||||
MockMvc mockMvc = getMockMvcBuilder().build();
|
||||
ClientHttpRequestFactory requestFactory = new MockMvcClientHttpRequestFactory(mockMvc);
|
||||
return RestTestClient.bindToServer(requestFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient build() {
|
||||
return configureClient().build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a {@link DefaultMockMvcBuilder}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class ApplicationContextMockMvcSpec extends AbstractMockMvcServerSpec<ApplicationContextMockMvcSpec> {
|
||||
private final DefaultMockMvcBuilder mockMvcBuilder;
|
||||
|
||||
public ApplicationContextMockMvcSpec(WebApplicationContext context) {
|
||||
this.mockMvcBuilder = MockMvcBuilders.webAppContextSetup(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigurableMockMvcBuilder<?> getMockMvcBuilder() {
|
||||
return this.mockMvcBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
|
||||
import org.springframework.http.ResponseCookie;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
|
||||
/**
|
||||
* Assertions on cookies of the response.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class CookieAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final RestTestClient.ResponseSpec responseSpec;
|
||||
|
||||
public CookieAssertions(ExchangeResult exchangeResult, RestTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expect a response cookie with the given name to match the specified value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String name, String value) {
|
||||
String cookieValue = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertEquals(message, value, cookieValue);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the value of the response cookie with the given name with a Hamcrest
|
||||
* {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Matcher<? super String> matcher) {
|
||||
String value = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
MatcherAssert.assertThat(message, value, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the value of the response cookie with the given name.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Consumer<String> consumer) {
|
||||
String value = getCookie(name).getValue();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the cookie with the given name is present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec exists(String name) {
|
||||
getCookie(name);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the cookie with the given name is not present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec doesNotExist(String name) {
|
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
|
||||
if (cookie != null) {
|
||||
String message = getMessage(name) + " exists with value=[" + cookie.getValue() + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Max-Age" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec maxAge(String name, Duration expected) {
|
||||
Duration maxAge = getCookie(name).getMaxAge();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " maxAge";
|
||||
assertEquals(message, expected, maxAge);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Max-Age" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec maxAge(String name, Matcher<? super Long> matcher) {
|
||||
long maxAge = getCookie(name).getMaxAge().getSeconds();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " maxAge";
|
||||
assertThat(message, maxAge, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Path" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec path(String name, String expected) {
|
||||
String path = getCookie(name).getPath();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " path";
|
||||
assertEquals(message, expected, path);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Path" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec path(String name, Matcher<? super String> matcher) {
|
||||
String path = getCookie(name).getPath();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " path";
|
||||
assertThat(message, path, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Domain" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec domain(String name, String expected) {
|
||||
String path = getCookie(name).getDomain();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " domain";
|
||||
assertEquals(message, expected, path);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Domain" attribute with a Hamcrest {@link Matcher}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec domain(String name, Matcher<? super String> matcher) {
|
||||
String domain = getCookie(name).getDomain();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " domain";
|
||||
assertThat(message, domain, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Secure" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec secure(String name, boolean expected) {
|
||||
boolean isSecure = getCookie(name).isSecure();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " secure";
|
||||
assertEquals(message, expected, isSecure);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "HttpOnly" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec httpOnly(String name, boolean expected) {
|
||||
boolean isHttpOnly = getCookie(name).isHttpOnly();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " httpOnly";
|
||||
assertEquals(message, expected, isHttpOnly);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "Partitioned" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec partitioned(String name, boolean expected) {
|
||||
boolean isPartitioned = getCookie(name).isPartitioned();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " isPartitioned";
|
||||
assertEquals(message, expected, isPartitioned);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a cookie's "SameSite" attribute.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec sameSite(String name, String expected) {
|
||||
String sameSite = getCookie(name).getSameSite();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name) + " sameSite";
|
||||
assertEquals(message, expected, sameSite);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private ResponseCookie getCookie(String name) {
|
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
|
||||
if (cookie != null) {
|
||||
return cookie;
|
||||
}
|
||||
else {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail("No cookie with name '" + name + "'"));
|
||||
}
|
||||
throw new IllegalStateException("This code path should not be reachable");
|
||||
}
|
||||
|
||||
private static String getMessage(String cookie) {
|
||||
return "Response cookie '" + cookie + "'";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.json.JsonAssert;
|
||||
import org.springframework.test.json.JsonComparator;
|
||||
import org.springframework.test.json.JsonCompareMode;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
import org.springframework.test.util.ExceptionCollector;
|
||||
import org.springframework.test.util.XmlExpectationsHelper;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class DefaultRestTestClient implements RestTestClient {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final AtomicLong requestIndex = new AtomicLong();
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
|
||||
DefaultRestTestClient(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.build();
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> get() {
|
||||
return methodInternal(HttpMethod.GET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> head() {
|
||||
return methodInternal(HttpMethod.HEAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec post() {
|
||||
return methodInternal(HttpMethod.POST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec put() {
|
||||
return methodInternal(HttpMethod.PUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec patch() {
|
||||
return methodInternal(HttpMethod.PATCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> delete() {
|
||||
return methodInternal(HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersUriSpec<?> options() {
|
||||
return methodInternal(HttpMethod.OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodyUriSpec method(HttpMethod method) {
|
||||
return methodInternal(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mutate() {
|
||||
return new DefaultRestTestClientBuilder(this.restClientBuilder);
|
||||
}
|
||||
|
||||
private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
|
||||
return new DefaultRequestBodyUriSpec(this.restClient.method(httpMethod));
|
||||
}
|
||||
|
||||
|
||||
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
|
||||
|
||||
private final RestClient.RequestBodyUriSpec requestHeadersUriSpec;
|
||||
private RestClient.RequestBodySpec requestBodySpec;
|
||||
private final String requestId;
|
||||
|
||||
|
||||
public DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) {
|
||||
this.requestHeadersUriSpec = spec;
|
||||
this.requestBodySpec = spec;
|
||||
this.requestId = String.valueOf(requestIndex.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec accept(MediaType... acceptableMediaTypes) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.accept(acceptableMediaTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(URI uri) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uri);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uriTemplate, uriVariables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(String uri, Map<String, ?> uriVariables) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uri, uriVariables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.uri(uriFunction);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec cookie(String name, String value) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.cookie(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.cookies(cookiesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec header(String headerName, String... headerValues) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.header(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec contentType(MediaType contentType) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.contentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHeadersSpec<?> body(Object body) {
|
||||
this.requestHeadersUriSpec.body(body);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec acceptCharset(Charset... acceptableCharsets) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.acceptCharset(acceptableCharsets);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.ifModifiedSince(ifModifiedSince);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec ifNoneMatch(String... ifNoneMatches) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.ifNoneMatch(ifNoneMatches);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec headers(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.headers(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attribute(String name, Object value) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.attribute(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
this.requestBodySpec = this.requestHeadersUriSpec.attributes(attributesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec exchange() {
|
||||
this.requestBodySpec = this.requestBodySpec.header(RESTTESTCLIENT_REQUEST_ID, this.requestId);
|
||||
ExchangeResult exchangeResult = this.requestBodySpec.exchange(
|
||||
(clientRequest, clientResponse) -> new ExchangeResult(clientResponse),
|
||||
false);
|
||||
return new DefaultResponseSpec(Objects.requireNonNull(exchangeResult));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
public DefaultResponseSpec(ExchangeResult exchangeResult) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAssertions expectStatus() {
|
||||
return new StatusAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec expectBody() {
|
||||
byte[] body = this.exchangeResult.getBody(byte[].class);
|
||||
return new DefaultBodyContentSpec( new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> BodySpec<B, ?> expectBody(Class<B> bodyType) {
|
||||
B body = this.exchangeResult.getBody(bodyType);
|
||||
return new DefaultBodySpec<>(new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType) {
|
||||
B body = this.exchangeResult.getBody(bodyType);
|
||||
return new DefaultBodySpec<>(new EntityExchangeResult<>(this.exchangeResult, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CookieAssertions expectCookie() {
|
||||
return new CookieAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAssertions expectHeader() {
|
||||
return new HeaderAssertions(this.exchangeResult, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec expectAll(ResponseSpecConsumer... consumers) {
|
||||
ExceptionCollector exceptionCollector = new ExceptionCollector();
|
||||
for (ResponseSpecConsumer consumer : consumers) {
|
||||
exceptionCollector.execute(() -> consumer.accept(this));
|
||||
}
|
||||
try {
|
||||
exceptionCollector.assertEmpty();
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// In theory, a ResponseSpecConsumer should never throw an Exception
|
||||
// that is not a RuntimeException, but since ExceptionCollector may
|
||||
// throw a checked Exception, we handle this to appease the compiler
|
||||
// and in case someone uses a "sneaky throws" technique.
|
||||
throw new AssertionError(ex.getMessage(), ex);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EntityExchangeResult<T> returnResult(Class<T> elementClass) {
|
||||
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EntityExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef) {
|
||||
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementTypeRef));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultBodyContentSpec implements BodyContentSpec {
|
||||
private final EntityExchangeResult<byte[]> result;
|
||||
|
||||
public DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<Void> isEmpty() {
|
||||
this.result.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertTrue("Expected empty body",
|
||||
this.result.getBody(byte[].class) == null));
|
||||
return new EntityExchangeResult<>(this.result, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec json(String expectedJson, JsonCompareMode compareMode) {
|
||||
return json(expectedJson, JsonAssert.comparator(compareMode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec json(String expectedJson, JsonComparator comparator) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
try {
|
||||
comparator.assertIsMatch(expectedJson, getBodyAsString());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("JSON parsing error", ex);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyContentSpec xml(String expectedXml) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
try {
|
||||
new XmlExpectationsHelper().assertXmlEqual(expectedXml, getBodyAsString());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonPathAssertions jsonPath(String expression) {
|
||||
return new JsonPathAssertions(this, getBodyAsString(), expression, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args) {
|
||||
return new XpathAssertions(this, expression, namespaces, args);
|
||||
}
|
||||
|
||||
private String getBodyAsString() {
|
||||
byte[] body = this.result.getResponseBody();
|
||||
if (body == null || body.length == 0) {
|
||||
return "";
|
||||
}
|
||||
Charset charset = Optional.ofNullable(this.result.getResponseHeaders().getContentType())
|
||||
.map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
|
||||
return new String(body, charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<byte[]> returnResult() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultBodySpec<B, S extends BodySpec<B, S>> implements BodySpec<B, S> {
|
||||
|
||||
private final EntityExchangeResult<B> result;
|
||||
|
||||
public DefaultBodySpec(@Nullable EntityExchangeResult<B> result) {
|
||||
this.result = Objects.requireNonNull(result, "exchangeResult must be non-null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityExchangeResult<B> returnResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T isEqualTo(B expected) {
|
||||
this.result.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody()));
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129
|
||||
public <T extends S, R> T value(Function<B, R> bodyMapper, Matcher<? super R> matcher) {
|
||||
this.result.assertWithDiagnostics(() -> {
|
||||
B body = this.result.getResponseBody();
|
||||
MatcherAssert.assertThat(bodyMapper.apply(body), matcher);
|
||||
});
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T value(Consumer<B> consumer) {
|
||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody()));
|
||||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer) {
|
||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result));
|
||||
return self();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends S> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link RestTestClient.Builder}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class DefaultRestTestClientBuilder implements RestTestClient.Builder {
|
||||
|
||||
private final RestClient.Builder restClientBuilder;
|
||||
|
||||
DefaultRestTestClientBuilder() {
|
||||
this.restClientBuilder = RestClient.builder();
|
||||
}
|
||||
|
||||
DefaultRestTestClientBuilder(RestClient.Builder restClientBuilder) {
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder apply(Consumer<RestTestClient.Builder> builderConsumer) {
|
||||
builderConsumer.accept(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder baseUrl(String baseUrl) {
|
||||
this.restClientBuilder.baseUrl(baseUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder defaultCookie(String cookieName, String... cookieValues) {
|
||||
this.restClientBuilder.defaultCookie(cookieName, cookieValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) {
|
||||
this.restClientBuilder.defaultCookies(cookiesConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder defaultHeader(String headerName, String... headerValues) {
|
||||
this.restClientBuilder.defaultHeader(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer) {
|
||||
this.restClientBuilder.defaultHeaders(headersConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient.Builder uriBuilderFactory(UriBuilderFactory uriFactory) {
|
||||
this.restClientBuilder.uriBuilderFactory(uriFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestTestClient build() {
|
||||
return new DefaultRestTestClient(this.restClientBuilder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* {@code ExchangeResult} sub-class that exposes the response body fully
|
||||
* extracted to a representation of type {@code <T>}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @param <T> the response body type
|
||||
*/
|
||||
public class EntityExchangeResult<T> extends ExchangeResult {
|
||||
|
||||
private final @Nullable T body;
|
||||
|
||||
|
||||
EntityExchangeResult(ExchangeResult result, @Nullable T body) {
|
||||
super(result);
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the entity extracted from the response body.
|
||||
*/
|
||||
public @Nullable T getResponseBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpCookie;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse;
|
||||
|
||||
/**
|
||||
* Container for request and response details for exchanges performed through
|
||||
* {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class ExchangeResult {
|
||||
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
|
||||
private static final Pattern PARTITIONED_PATTERN = Pattern.compile("(?i).*;\\s*Partitioned(\\s*;.*|\\s*)$");
|
||||
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ExchangeResult.class);
|
||||
|
||||
/** Ensure single logging; for example, for expectAll. */
|
||||
private boolean diagnosticsLogged;
|
||||
|
||||
private final ConvertibleClientHttpResponse clientResponse;
|
||||
|
||||
ExchangeResult(@Nullable ConvertibleClientHttpResponse clientResponse) {
|
||||
this.clientResponse = Objects.requireNonNull(clientResponse, "clientResponse must be non-null");
|
||||
}
|
||||
|
||||
ExchangeResult(ExchangeResult result) {
|
||||
this(result.clientResponse);
|
||||
this.diagnosticsLogged = result.diagnosticsLogged;
|
||||
}
|
||||
|
||||
public HttpStatusCode getStatus() {
|
||||
try {
|
||||
return this.clientResponse.getStatusCode();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new AssertionError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpHeaders getResponseHeaders() {
|
||||
return this.clientResponse.getHeaders();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T> T getBody(Class<T> bodyType) {
|
||||
return this.clientResponse.bodyTo(bodyType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T> T getBody(ParameterizedTypeReference<T> bodyType) {
|
||||
return this.clientResponse.bodyTo(bodyType);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the given Runnable, catch any {@link AssertionError}, log details
|
||||
* about the request and response at ERROR level under the class log
|
||||
* category, and after that re-throw the error.
|
||||
*/
|
||||
public void assertWithDiagnostics(Runnable assertion) {
|
||||
try {
|
||||
assertion.run();
|
||||
}
|
||||
catch (AssertionError ex) {
|
||||
if (!this.diagnosticsLogged && logger.isErrorEnabled()) {
|
||||
this.diagnosticsLogged = true;
|
||||
logger.error("Request details for assertion failure:\n" + this);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return response cookies received from the server.
|
||||
*/
|
||||
public MultiValueMap<String, ResponseCookie> getResponseCookies() {
|
||||
return Optional.ofNullable(this.clientResponse.getHeaders().get(HttpHeaders.SET_COOKIE)).orElse(List.of()).stream()
|
||||
.flatMap(header -> {
|
||||
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
|
||||
String sameSite = (matcher.matches() ? matcher.group(1) : null);
|
||||
boolean partitioned = PARTITIONED_PATTERN.matcher(header).matches();
|
||||
return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite, partitioned));
|
||||
})
|
||||
.collect(LinkedMultiValueMap::new,
|
||||
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
|
||||
LinkedMultiValueMap::addAll);
|
||||
}
|
||||
|
||||
private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite, boolean partitioned) {
|
||||
return ResponseCookie.from(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.maxAge(cookie.getMaxAge())
|
||||
.path(cookie.getPath())
|
||||
.secure(cookie.getSecure())
|
||||
.sameSite(sameSite)
|
||||
.partitioned(partitioned)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.assertNotNull;
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
|
||||
/**
|
||||
* Assertions on headers of the response.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @see RestTestClient.ResponseSpec#expectHeader()
|
||||
*/
|
||||
public class HeaderAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final RestTestClient.ResponseSpec responseSpec;
|
||||
|
||||
public HeaderAssertions(ExchangeResult exchangeResult, RestTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the specified values.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String headerName, String... values) {
|
||||
return assertHeader(headerName, Arrays.asList(values), getHeaders().getOrEmpty(headerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the given long value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEquals(String headerName, long value) {
|
||||
String actual = getHeaders().getFirst(headerName);
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
assertNotNull("Response does not contain header '" + headerName + "'", actual));
|
||||
return assertHeader(headerName, value, Long.parseLong(actual));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the specified long value
|
||||
* parsed into a date using the preferred date format described in RFC 7231.
|
||||
* <p>An {@link AssertionError} is thrown if the response does not contain
|
||||
* the specified header, or if the supplied {@code value} does not match the
|
||||
* primary header value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueEqualsDate(String headerName, long value) {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String headerValue = getHeaders().getFirst(headerName);
|
||||
assertNotNull("Response does not contain header '" + headerName + "'", headerValue);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setDate("expected", value);
|
||||
headers.set("actual", headerValue);
|
||||
|
||||
assertEquals(getMessage(headerName) + "='" + headerValue + "' " +
|
||||
"does not match expected value '" + headers.getFirst("expected") + "'",
|
||||
headers.getFirstDate("expected"), headers.getFirstDate("actual"));
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the first value of the response header with a regex.
|
||||
* @param name the header name
|
||||
* @param pattern the regex pattern
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valueMatches(String name, String pattern) {
|
||||
String value = getRequiredValue(name);
|
||||
String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> assertTrue(message, value.matches(pattern)));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match all values of the response header with the given regex
|
||||
* patterns which are applied to the values of the header in the
|
||||
* same order. Note that the number of patterns must match the
|
||||
* number of actual values.
|
||||
* @param name the header name
|
||||
* @param patterns one or more regex patterns, one per expected value
|
||||
*/
|
||||
public RestTestClient.ResponseSpec valuesMatch(String name, String... patterns) {
|
||||
List<String> values = getRequiredValues(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
assertTrue(
|
||||
getMessage(name) + " has fewer or more values " + values +
|
||||
" than number of patterns to match with " + Arrays.toString(patterns),
|
||||
values.size() == patterns.length);
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
String value = values.get(i);
|
||||
String pattern = patterns[i];
|
||||
assertTrue(
|
||||
getMessage(name) + "[" + i + "]='" + value + "' does not match '" + pattern + "'",
|
||||
value.matches(pattern));
|
||||
}
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the first value of the response header with a Hamcrest {@link Matcher}.
|
||||
* @param name the header name
|
||||
* @param matcher the matcher to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Matcher<? super String> matcher) {
|
||||
String value = getHeaders().getFirst(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertThat(message, value, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert all values of the response header with a Hamcrest {@link Matcher}.
|
||||
* @param name the header name
|
||||
* @param matcher the matcher to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec values(String name, Matcher<? super Iterable<String>> matcher) {
|
||||
List<String> values = getHeaders().get(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertThat(message, values, matcher);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the first value of the named response header.
|
||||
* @param name the header name
|
||||
* @param consumer the consumer to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(String name, Consumer<String> consumer) {
|
||||
String value = getRequiredValue(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume all values of the named response header.
|
||||
* @param name the header name
|
||||
* @param consumer the consumer to use
|
||||
*/
|
||||
public RestTestClient.ResponseSpec values(String name, Consumer<List<String>> consumer) {
|
||||
List<String> values = getRequiredValues(name);
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(values));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the header with the given name is present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec exists(String name) {
|
||||
if (!this.exchangeResult.getResponseHeaders().containsHeader(name)) {
|
||||
String message = getMessage(name) + " does not exist";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the header with the given name is not present.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec doesNotExist(String name) {
|
||||
if (getHeaders().containsHeader(name)) {
|
||||
String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
|
||||
}
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Cache-Control" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec cacheControl(CacheControl cacheControl) {
|
||||
return assertHeader("Cache-Control", cacheControl.getHeaderValue(), getHeaders().getCacheControl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Disposition" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentDisposition(ContentDisposition contentDisposition) {
|
||||
return assertHeader("Content-Disposition", contentDisposition, getHeaders().getContentDisposition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Length" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentLength(long contentLength) {
|
||||
return assertHeader("Content-Length", contentLength, getHeaders().getContentLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentType(MediaType mediaType) {
|
||||
return assertHeader("Content-Type", mediaType, getHeaders().getContentType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentType(String mediaType) {
|
||||
return contentType(MediaType.parseMediaType(mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header compatible with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentTypeCompatibleWith(MediaType mediaType) {
|
||||
MediaType actual = getHeaders().getContentType();
|
||||
String message = getMessage("Content-Type") + "=[" + actual + "] is not compatible with [" + mediaType + "]";
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType))));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Content-Type" header compatible with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec contentTypeCompatibleWith(String mediaType) {
|
||||
return contentTypeCompatibleWith(MediaType.parseMediaType(mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect an "Expires" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec expires(long expires) {
|
||||
return assertHeader("Expires", expires, getHeaders().getExpires());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Last-Modified" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec lastModified(long lastModified) {
|
||||
return assertHeader("Last-Modified", lastModified, getHeaders().getLastModified());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a "Location" header with the given value.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec location(String location) {
|
||||
return assertHeader("Location", URI.create(location), getHeaders().getLocation());
|
||||
}
|
||||
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
return this.exchangeResult.getResponseHeaders();
|
||||
}
|
||||
|
||||
private String getRequiredValue(String name) {
|
||||
return getRequiredValues(name).get(0);
|
||||
}
|
||||
|
||||
private List<String> getRequiredValues(String name) {
|
||||
List<String> values = getHeaders().get(name);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
return values;
|
||||
}
|
||||
else {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> fail(getMessage(name) + " not found"));
|
||||
}
|
||||
throw new IllegalStateException("This code path should not be reachable");
|
||||
}
|
||||
|
||||
private RestTestClient.ResponseSpec assertHeader(String name, @Nullable Object expected, @Nullable Object actual) {
|
||||
this.exchangeResult.assertWithDiagnostics(() -> {
|
||||
String message = getMessage(name);
|
||||
assertEquals(message, expected, actual);
|
||||
});
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private static String getMessage(String headerName) {
|
||||
return "Response header '" + headerName + "'";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.test.util.JsonPathExpectationsHelper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*
|
||||
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
|
||||
* @see JsonPathExpectationsHelper
|
||||
*/
|
||||
public class JsonPathAssertions {
|
||||
|
||||
private final RestTestClient.BodyContentSpec bodySpec;
|
||||
|
||||
private final String content;
|
||||
|
||||
private final JsonPathExpectationsHelper pathHelper;
|
||||
|
||||
|
||||
JsonPathAssertions(RestTestClient.BodyContentSpec spec, String content, String expression, @Nullable Configuration configuration) {
|
||||
Assert.hasText(expression, "expression must not be null or empty");
|
||||
this.bodySpec = spec;
|
||||
this.content = content;
|
||||
this.pathHelper = new JsonPathExpectationsHelper(expression, configuration);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValue(String, Object)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(Object expectedValue) {
|
||||
this.pathHelper.assertValue(this.content, expectedValue);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#exists(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec exists() {
|
||||
this.pathHelper.exists(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#doesNotExist(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotExist() {
|
||||
this.pathHelper.doesNotExist(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsEmpty(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEmpty() {
|
||||
this.pathHelper.assertValueIsEmpty(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNotEmpty(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isNotEmpty() {
|
||||
this.pathHelper.assertValueIsNotEmpty(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#hasJsonPath}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec hasJsonPath() {
|
||||
this.pathHelper.hasJsonPath(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#doesNotHaveJsonPath}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotHaveJsonPath() {
|
||||
this.pathHelper.doesNotHaveJsonPath(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsBoolean(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isBoolean() {
|
||||
this.pathHelper.assertValueIsBoolean(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNumber(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isNumber() {
|
||||
this.pathHelper.assertValueIsNumber(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsArray(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isArray() {
|
||||
this.pathHelper.assertValueIsArray(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsMap(String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isMap() {
|
||||
this.pathHelper.assertValueIsMap(this.content);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Class<T> targetType, Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher, targetType);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Matcher<? super T> matcher) {
|
||||
this.pathHelper.assertValue(this.content, matcher, targetType);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> RestTestClient.BodyContentSpec value(Consumer<T> consumer) {
|
||||
Object value = this.pathHelper.evaluateJsonPath(this.content);
|
||||
consumer.accept((T) value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation and provide a target class.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(Class<T> targetType, Consumer<T> consumer) {
|
||||
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
|
||||
consumer.accept(value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the JSONPath evaluation and provide a parameterized type.
|
||||
*/
|
||||
public <T> RestTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Consumer<T> consumer) {
|
||||
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
|
||||
consumer.accept(value);
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
throw new AssertionError("Object#equals is disabled " +
|
||||
"to avoid being used in error instead of JsonPathAssertions#isEqualTo(String).");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,945 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.test.json.JsonComparator;
|
||||
import org.springframework.test.json.JsonCompareMode;
|
||||
import org.springframework.test.json.JsonComparison;
|
||||
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
|
||||
import org.springframework.test.web.servlet.DispatcherServletCustomizer;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.servlet.FlashMapManager;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Client for testing web servers.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public interface RestTestClient {
|
||||
|
||||
/**
|
||||
* The name of a request header used to assign a unique id to every request
|
||||
* performed through the {@code RestTestClient}. This can be useful for
|
||||
* storing contextual information at all phases of request processing (for example,
|
||||
* from a server-side component) under that id and later to look up
|
||||
* that information once an {@link ExchangeResult} is available.
|
||||
*/
|
||||
String RESTTESTCLIENT_REQUEST_ID = "RestTestClient-Request-Id";
|
||||
|
||||
/**
|
||||
* Prepare an HTTP GET request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> get();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP HEAD request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> head();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP POST request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec post();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PUT request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec put();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PATCH request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec patch();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP DELETE request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> delete();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP OPTIONS request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestHeadersUriSpec<?> options();
|
||||
|
||||
/**
|
||||
* Prepare a request for the specified {@code HttpMethod}.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
RequestBodyUriSpec method(HttpMethod method);
|
||||
|
||||
/**
|
||||
* Return a builder to mutate properties of this test client.
|
||||
*/
|
||||
Builder mutate();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing the {@code @Controller}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#standaloneSetup(Object...)}
|
||||
* to initialize {@link MockMvc}.
|
||||
*/
|
||||
static ControllerSpec bindToController(Object... controllers) {
|
||||
return new StandaloneMockMvcSpec(controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing the {@link RouterFunction}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#routerFunctions(RouterFunction[])}
|
||||
* to initialize {@link MockMvc}.
|
||||
*/
|
||||
static RouterFunctionSpec bindToRouterFunction(RouterFunction<?>... routerFunctions) {
|
||||
return new RouterFunctionMockMvcSpec(routerFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing a
|
||||
* {@link WebApplicationContext} with Spring MVC infrastructure and
|
||||
* controllers.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#webAppContextSetup(WebApplicationContext)}
|
||||
* to initialize {@code MockMvc}.
|
||||
*/
|
||||
static MockMvcServerSpec<?> bindToApplicationContext(WebApplicationContext context) {
|
||||
return new ApplicationContextMockMvcSpec(context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Begin creating a {@link RestTestClient} by providing an already
|
||||
* initialized {@link MockMvc} instance to use as the server.
|
||||
*/
|
||||
static RestTestClient.Builder bindTo(MockMvc mockMvc) {
|
||||
ClientHttpRequestFactory requestFactory = new MockMvcClientHttpRequestFactory(mockMvc);
|
||||
return RestTestClient.bindToServer(requestFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* This server setup option allows you to connect to a live server through
|
||||
* a client connector.
|
||||
* <p><pre class="code">
|
||||
* RestTestClient client = RestTestClient.bindToServer()
|
||||
* .baseUrl("http://localhost:8080")
|
||||
* .build();
|
||||
* </pre>
|
||||
* @return chained API to customize client config
|
||||
*/
|
||||
static Builder bindToServer() {
|
||||
return new DefaultRestTestClientBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link #bindToServer()} with a pre-configured request factory.
|
||||
* @return chained API to customize client config
|
||||
*/
|
||||
static Builder bindToServer(ClientHttpRequestFactory requestFactory) {
|
||||
return new DefaultRestTestClientBuilder(RestClient.builder().requestFactory(requestFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for customizing controller configuration.
|
||||
*/
|
||||
interface ControllerSpec extends MockMvcServerSpec<ControllerSpec> {
|
||||
/**
|
||||
* Register {@link org.springframework.web.bind.annotation.ControllerAdvice}
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setControllerAdvice(Object...)}.
|
||||
*/
|
||||
ControllerSpec controllerAdvice(Object... controllerAdvice);
|
||||
|
||||
/**
|
||||
* Set the message converters to use.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setMessageConverters(HttpMessageConverter[])}.
|
||||
*/
|
||||
ControllerSpec messageConverters(HttpMessageConverter<?>... messageConverters);
|
||||
|
||||
/**
|
||||
* Provide a custom {@link Validator}.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setValidator(Validator)}.
|
||||
*/
|
||||
ControllerSpec validator(Validator validator);
|
||||
|
||||
/**
|
||||
* Provide a conversion service.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setConversionService(FormattingConversionService)}.
|
||||
*/
|
||||
ControllerSpec conversionService(FormattingConversionService conversionService);
|
||||
|
||||
/**
|
||||
* Add global interceptors.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#addInterceptors(HandlerInterceptor...)}.
|
||||
*/
|
||||
ControllerSpec interceptors(HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Add interceptors for specific patterns.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}.
|
||||
*/
|
||||
ControllerSpec mappedInterceptors(
|
||||
String @Nullable [] pathPatterns, HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Set a ContentNegotiationManager.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setContentNegotiationManager(ContentNegotiationManager)}.
|
||||
*/
|
||||
ControllerSpec contentNegotiationManager(ContentNegotiationManager manager);
|
||||
|
||||
/**
|
||||
* Specify the timeout value for async execution.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setAsyncRequestTimeout(long)}.
|
||||
*/
|
||||
ControllerSpec asyncRequestTimeout(long timeout);
|
||||
|
||||
/**
|
||||
* Provide custom argument resolvers.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setCustomArgumentResolvers(HandlerMethodArgumentResolver...)}.
|
||||
*/
|
||||
ControllerSpec customArgumentResolvers(HandlerMethodArgumentResolver... argumentResolvers);
|
||||
|
||||
/**
|
||||
* Provide custom return value handlers.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setCustomReturnValueHandlers(HandlerMethodReturnValueHandler...)}.
|
||||
*/
|
||||
ControllerSpec customReturnValueHandlers(HandlerMethodReturnValueHandler... handlers);
|
||||
|
||||
/**
|
||||
* Set the HandlerExceptionResolver types to use.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setHandlerExceptionResolvers(HandlerExceptionResolver...)}.
|
||||
*/
|
||||
ControllerSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers);
|
||||
|
||||
/**
|
||||
* Set up view resolution.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setViewResolvers(ViewResolver...)}.
|
||||
*/
|
||||
ControllerSpec viewResolvers(ViewResolver... resolvers);
|
||||
|
||||
/**
|
||||
* Set up a single {@link ViewResolver} with a fixed view.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setSingleView(View)}.
|
||||
*/
|
||||
ControllerSpec singleView(View view);
|
||||
|
||||
/**
|
||||
* Provide the LocaleResolver to use.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setLocaleResolver(LocaleResolver)}.
|
||||
*/
|
||||
ControllerSpec localeResolver(LocaleResolver localeResolver);
|
||||
|
||||
/**
|
||||
* Provide a custom FlashMapManager.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setFlashMapManager(FlashMapManager)}.
|
||||
*/
|
||||
ControllerSpec flashMapManager(FlashMapManager flashMapManager);
|
||||
|
||||
/**
|
||||
* Enable URL path matching with parsed
|
||||
* {@link org.springframework.web.util.pattern.PathPattern PathPatterns}.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setPatternParser(PathPatternParser)}.
|
||||
*/
|
||||
ControllerSpec patternParser(PathPatternParser parser);
|
||||
|
||||
/**
|
||||
* Configure placeholder values to use.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#addPlaceholderValue(String, String)}.
|
||||
*/
|
||||
ControllerSpec placeholderValue(String name, String value);
|
||||
|
||||
/**
|
||||
* Configure factory for a custom {@link RequestMappingHandlerMapping}.
|
||||
* <p>This is delegated to
|
||||
* {@link StandaloneMockMvcBuilder#setCustomHandlerMapping(Supplier)}.
|
||||
*/
|
||||
ControllerSpec customHandlerMapping(Supplier<RequestMappingHandlerMapping> factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for configuring {@link MockMvc} to test one or more
|
||||
* {@linkplain RouterFunction router functions}
|
||||
* directly, and a simple facade around {@link RouterFunctionMockMvcBuilder}.
|
||||
*/
|
||||
interface RouterFunctionSpec extends MockMvcServerSpec<RouterFunctionSpec> {
|
||||
|
||||
/**
|
||||
* Set the message converters to use.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setMessageConverters(HttpMessageConverter[])}.
|
||||
*/
|
||||
RouterFunctionSpec messageConverters(HttpMessageConverter<?>... messageConverters);
|
||||
|
||||
/**
|
||||
* Add global interceptors.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#addInterceptors(HandlerInterceptor...)}.
|
||||
*/
|
||||
RouterFunctionSpec interceptors(HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Add interceptors for specific patterns.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}.
|
||||
*/
|
||||
RouterFunctionSpec mappedInterceptors(
|
||||
String @Nullable [] pathPatterns, HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Specify the timeout value for async execution.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setAsyncRequestTimeout(long)}.
|
||||
*/
|
||||
RouterFunctionSpec asyncRequestTimeout(long timeout);
|
||||
|
||||
/**
|
||||
* Set the HandlerExceptionResolver types to use.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setHandlerExceptionResolvers(HandlerExceptionResolver...)}.
|
||||
*/
|
||||
RouterFunctionSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers);
|
||||
|
||||
/**
|
||||
* Set up view resolution.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setViewResolvers(ViewResolver...)}.
|
||||
*/
|
||||
RouterFunctionSpec viewResolvers(ViewResolver... resolvers);
|
||||
|
||||
/**
|
||||
* Set up a single {@link ViewResolver} with a fixed view.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setSingleView(View)}.
|
||||
*/
|
||||
RouterFunctionSpec singleView(View view);
|
||||
|
||||
/**
|
||||
* Enable URL path matching with parsed
|
||||
* {@link org.springframework.web.util.pattern.PathPattern PathPatterns}.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setPatternParser(PathPatternParser)}.
|
||||
*/
|
||||
RouterFunctionSpec patternParser(PathPatternParser parser);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base specification for configuring {@link MockMvc}, and a simple facade
|
||||
* around {@link ConfigurableMockMvcBuilder}.
|
||||
*
|
||||
* @param <B> a self reference to the builder type
|
||||
*/
|
||||
interface MockMvcServerSpec<B extends MockMvcServerSpec<B>> {
|
||||
/**
|
||||
* Add a global filter.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#addFilters(Filter...)}.
|
||||
*/
|
||||
<T extends B> T filters(Filter... filters);
|
||||
|
||||
/**
|
||||
* Add a filter for specific URL patterns.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#addFilter(Filter, String...)}.
|
||||
*/
|
||||
<T extends B> T filter(Filter filter, String... urlPatterns);
|
||||
|
||||
/**
|
||||
* Define default request properties that should be merged into all
|
||||
* performed requests such that input from the client request override
|
||||
* the default properties defined here.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#defaultRequest(RequestBuilder)}.
|
||||
*/
|
||||
<T extends B> T defaultRequest(RequestBuilder requestBuilder);
|
||||
|
||||
/**
|
||||
* Define a global expectation that should <em>always</em> be applied to
|
||||
* every response.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#alwaysExpect(ResultMatcher)}.
|
||||
*/
|
||||
<T extends B> T alwaysExpect(ResultMatcher resultMatcher);
|
||||
|
||||
/**
|
||||
* Whether to handle HTTP OPTIONS requests.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#dispatchOptions(boolean)}.
|
||||
*/
|
||||
<T extends B> T dispatchOptions(boolean dispatchOptions);
|
||||
|
||||
/**
|
||||
* Allow customization of {@code DispatcherServlet}.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#addDispatcherServletCustomizer(DispatcherServletCustomizer)}.
|
||||
*/
|
||||
<T extends B> T dispatcherServletCustomizer(DispatcherServletCustomizer customizer);
|
||||
|
||||
/**
|
||||
* Add a {@code MockMvcConfigurer} that automates MockMvc setup.
|
||||
* <p>This is delegated to
|
||||
* {@link ConfigurableMockMvcBuilder#apply(MockMvcConfigurer)}.
|
||||
*/
|
||||
<T extends B> T apply(MockMvcConfigurer configurer);
|
||||
|
||||
|
||||
/**
|
||||
* Proceed to configure and build the test client.
|
||||
*/
|
||||
Builder configureClient();
|
||||
|
||||
/**
|
||||
* Shortcut to build the test client.
|
||||
*/
|
||||
RestTestClient build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing request headers and the URI of a request.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S>, RequestHeadersSpec<S> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing the body and the URI of a request.
|
||||
*/
|
||||
interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Chained API for applying assertions to a response.
|
||||
*/
|
||||
interface ResponseSpec {
|
||||
/**
|
||||
* Assertions on the response status.
|
||||
*/
|
||||
StatusAssertions expectStatus();
|
||||
|
||||
/**
|
||||
* Consume and decode the response body to {@code byte[]} and then apply
|
||||
* assertions on the raw content (for example, isEmpty, JSONPath, etc.).
|
||||
*/
|
||||
BodyContentSpec expectBody();
|
||||
|
||||
/**
|
||||
* Consume and decode the response body to a single object of type
|
||||
* {@code <B>} and then apply assertions.
|
||||
* @param bodyType the expected body type
|
||||
*/
|
||||
<B> BodySpec<B, ?> expectBody(Class<B> bodyType);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #expectBody(Class)} that accepts information
|
||||
* about a target type with generics.
|
||||
*/
|
||||
<B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType);
|
||||
|
||||
/**
|
||||
* Assertions on the cookies of the response.
|
||||
*/
|
||||
CookieAssertions expectCookie();
|
||||
|
||||
/**
|
||||
* Assertions on the headers of the response.
|
||||
*/
|
||||
HeaderAssertions expectHeader();
|
||||
|
||||
/**
|
||||
* Apply multiple assertions to a response with the given
|
||||
* {@linkplain RestTestClient.ResponseSpec.ResponseSpecConsumer consumers}, with the guarantee that
|
||||
* all assertions will be applied even if one or more assertions fails
|
||||
* with an exception.
|
||||
* <p>If a single {@link Error} or {@link RuntimeException} is thrown,
|
||||
* it will be rethrown.
|
||||
* <p>If multiple exceptions are thrown, this method will throw an
|
||||
* {@link AssertionError} whose error message is a summary of all the
|
||||
* exceptions. In addition, each exception will be added as a
|
||||
* {@linkplain Throwable#addSuppressed(Throwable) suppressed exception} to
|
||||
* the {@code AssertionError}.
|
||||
* <p>This feature is similar to the {@code SoftAssertions} support in
|
||||
* AssertJ and the {@code assertAll()} support in JUnit Jupiter.
|
||||
*
|
||||
* <h4>Example</h4>
|
||||
* <pre class="code">
|
||||
* restTestClient.get().uri("/hello").exchange()
|
||||
* .expectAll(
|
||||
* responseSpec -> responseSpec.expectStatus().isOk(),
|
||||
* responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
|
||||
* );
|
||||
* </pre>
|
||||
* @param consumers the list of {@code ResponseSpec} consumers
|
||||
*/
|
||||
ResponseSpec expectAll(ResponseSpecConsumer... consumers);
|
||||
|
||||
/**
|
||||
* Exit the chained flow in order to consume the response body
|
||||
* externally.
|
||||
*/
|
||||
<T> EntityExchangeResult<T> returnResult(Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #returnResult(Class)} that accepts information
|
||||
* about a target type with generics.
|
||||
*/
|
||||
<T> EntityExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/**
|
||||
* {@link Consumer} of a {@link RestTestClient.ResponseSpec}.
|
||||
* @see RestTestClient.ResponseSpec#expectAll(RestTestClient.ResponseSpec.ResponseSpecConsumer...)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ResponseSpecConsumer extends Consumer<ResponseSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec for expectations on the response body content.
|
||||
*/
|
||||
interface BodyContentSpec {
|
||||
/**
|
||||
* Assert the response body is empty and return the exchange result.
|
||||
*/
|
||||
EntityExchangeResult<Void> isEmpty();
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison verifying that they contain the same attribute-value pairs
|
||||
* regardless of formatting with <em>lenient</em> checking (extensible
|
||||
* and non-strict array ordering).
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://jsonassert.skyscreamer.org/">JSONassert</a> library
|
||||
* to be on the classpath.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @see #json(String, JsonCompareMode)
|
||||
*/
|
||||
default BodyContentSpec json(String expectedJson) {
|
||||
return json(expectedJson, JsonCompareMode.LENIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison using the given {@linkplain JsonCompareMode mode}. If the
|
||||
* comparison failed, throws an {@link AssertionError} with the message
|
||||
* of the {@link JsonComparison}.
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://jsonassert.skyscreamer.org/">JSONassert</a> library
|
||||
* to be on the classpath.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @param compareMode the compare mode
|
||||
* @see #json(String)
|
||||
*/
|
||||
BodyContentSpec json(String expectedJson, JsonCompareMode compareMode);
|
||||
|
||||
/**
|
||||
* Parse the expected and actual response content as JSON and perform a
|
||||
* comparison using the given {@link JsonComparator}. If the comparison
|
||||
* failed, throws an {@link AssertionError} with the message of the
|
||||
* {@link JsonComparison}.
|
||||
* @param expectedJson the expected JSON content
|
||||
* @param comparator the comparator to use
|
||||
*/
|
||||
BodyContentSpec json(String expectedJson, JsonComparator comparator);
|
||||
|
||||
/**
|
||||
* Parse expected and actual response content as XML and assert that
|
||||
* the two are "similar", i.e. they contain the same elements and
|
||||
* attributes regardless of order.
|
||||
* <p>Use of this method requires the
|
||||
* <a href="https://github.com/xmlunit/xmlunit">XMLUnit</a> library on
|
||||
* the classpath.
|
||||
* @param expectedXml the expected XML content.
|
||||
* @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String)
|
||||
*/
|
||||
BodyContentSpec xml(String expectedXml);
|
||||
|
||||
/**
|
||||
* Access to response body assertions using an XPath expression to
|
||||
* inspect a specific subset of the body.
|
||||
* <p>The XPath expression can be a parameterized string using
|
||||
* formatting specifiers as defined in {@link String#format}.
|
||||
* @param expression the XPath expression
|
||||
* @param args arguments to parameterize the expression
|
||||
* @see #xpath(String, Map, Object...)
|
||||
*/
|
||||
default XpathAssertions xpath(String expression, Object... args) {
|
||||
return xpath(expression, null, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access to response body assertions with specific namespaces using an
|
||||
* XPath expression to inspect a specific subset of the body.
|
||||
* <p>The XPath expression can be a parameterized string using
|
||||
* formatting specifiers as defined in {@link String#format}.
|
||||
* @param expression the XPath expression
|
||||
* @param namespaces the namespaces to use
|
||||
* @param args arguments to parameterize the expression
|
||||
*/
|
||||
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
|
||||
|
||||
/**
|
||||
* Access to response body assertions using a
|
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
|
||||
* to inspect a specific subset of the body.
|
||||
* @param expression the JsonPath expression
|
||||
*/
|
||||
JsonPathAssertions jsonPath(String expression);
|
||||
|
||||
/**
|
||||
* Exit the chained API and return an {@code ExchangeResult} with the
|
||||
* raw response content.
|
||||
*/
|
||||
EntityExchangeResult<byte[]> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec for expectations on the response body decoded to a single Object.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
* @param <B> the body type
|
||||
*/
|
||||
interface BodySpec<B, S extends BodySpec<B, S>> {
|
||||
/**
|
||||
* Transform the extracted the body with a function, for example, extracting a
|
||||
* property, and assert the mapped value with a {@link Matcher}.
|
||||
*/
|
||||
<T extends S, R> T value(Function<B, R> bodyMapper, Matcher<? super R> matcher);
|
||||
|
||||
/**
|
||||
* Assert the extracted body with a {@link Consumer}.
|
||||
*/
|
||||
<T extends S> T value(Consumer<B> consumer);
|
||||
|
||||
/**
|
||||
* Assert the exchange result with the given {@link Consumer}.
|
||||
*/
|
||||
<T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer);
|
||||
|
||||
/**
|
||||
* Exit the chained API and return an {@code EntityExchangeResult} with the
|
||||
* decoded response content.
|
||||
*/
|
||||
EntityExchangeResult<B> returnResult();
|
||||
|
||||
/**
|
||||
* Assert the extracted body is equal to the given value.
|
||||
*/
|
||||
<T extends S> T isEqualTo(B expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing the URI of a request.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface UriSpec<S extends RequestHeadersSpec<?>> {
|
||||
/**
|
||||
* Specify the URI using an absolute, fully constructed {@link java.net.URI}.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client with
|
||||
* a base URI, that base URI will <strong>not</strong> be applied to the
|
||||
* supplied {@code java.net.URI}. If you wish to have a base URI applied to a
|
||||
* {@code java.net.URI} you must invoke either {@link #uri(String, Object...)}
|
||||
* or {@link #uri(String, Map)} — for example, {@code uri(myUri.toString())}.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(URI uri);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client (for example,
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(String uri, Object... uriVariables);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* <p>If a {@link UriBuilderFactory} was configured for the client (for example,
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(String uri, Map<String, ?> uriVariables);
|
||||
|
||||
/**
|
||||
* Build the URI for the request with a {@link UriBuilder} obtained
|
||||
* through the {@link UriBuilderFactory} configured for this client.
|
||||
* @return spec to add headers or perform the exchange
|
||||
*/
|
||||
S uri(Function<UriBuilder, URI> uriFunction);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Specification for adding request headers and performing an exchange.
|
||||
*
|
||||
* @param <S> a self reference to the spec type
|
||||
*/
|
||||
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain MediaType media types}, as
|
||||
* specified by the {@code Accept} header.
|
||||
* @param acceptableMediaTypes the acceptable media types
|
||||
* @return the same instance
|
||||
*/
|
||||
S accept(MediaType... acceptableMediaTypes);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified
|
||||
* by the {@code Accept-Charset} header.
|
||||
* @param acceptableCharsets the acceptable charsets
|
||||
* @return the same instance
|
||||
*/
|
||||
S acceptCharset(Charset... acceptableCharsets);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return the same instance
|
||||
*/
|
||||
S cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Manipulate this request's cookies with the given consumer. The
|
||||
* map provided to the consumer is "live", so that the consumer can be used to
|
||||
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
|
||||
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
|
||||
* {@link MultiValueMap} methods.
|
||||
* @param cookiesConsumer a function that consumes the cookies map
|
||||
* @return this builder
|
||||
*/
|
||||
S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
|
||||
|
||||
/**
|
||||
* Set the value of the {@code If-Modified-Since} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
* @param ifModifiedSince the new value of the header
|
||||
* @return the same instance
|
||||
*/
|
||||
S ifModifiedSince(ZonedDateTime ifModifiedSince);
|
||||
|
||||
/**
|
||||
* Set the values of the {@code If-None-Match} header.
|
||||
* @param ifNoneMatches the new value of the header
|
||||
* @return the same instance
|
||||
*/
|
||||
S ifNoneMatch(String... ifNoneMatches);
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return the same instance
|
||||
*/
|
||||
S header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Manipulate the request's headers with the given consumer. The
|
||||
* headers provided to the consumer are "live", so that the consumer can be used to
|
||||
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
|
||||
* {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other
|
||||
* {@link HttpHeaders} methods.
|
||||
* @param headersConsumer a function that consumes the {@code HttpHeaders}
|
||||
* @return this builder
|
||||
*/
|
||||
S headers(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Set the attribute with the given name to the given value.
|
||||
* @param name the name of the attribute to add
|
||||
* @param value the value of the attribute to add
|
||||
* @return this builder
|
||||
*/
|
||||
S attribute(String name, Object value);
|
||||
|
||||
/**
|
||||
* Manipulate the request attributes with the given consumer. The attributes provided to
|
||||
* the consumer are "live", so that the consumer can be used to inspect attributes,
|
||||
* remove attributes, or use any of the other map-provided methods.
|
||||
* @param attributesConsumer a function that consumes the attributes
|
||||
* @return this builder
|
||||
*/
|
||||
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||
|
||||
/**
|
||||
* Perform the exchange without a request body.
|
||||
* @return spec for decoding the response
|
||||
*/
|
||||
ResponseSpec exchange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for providing body of a request.
|
||||
*/
|
||||
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
* @param contentType the content type
|
||||
* @return the same instance
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
RequestBodySpec contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Set the body to the given {@code Object} value. This method invokes the
|
||||
* {@link org.springframework.web.client.RestClient.RequestBodySpec#body(Object)} (Object)
|
||||
* bodyValue} method on the underlying {@code RestClient}.
|
||||
* @param body the value to write to the request body
|
||||
* @return spec for further declaration of the request
|
||||
*/
|
||||
RequestHeadersSpec<?> body(Object body);
|
||||
}
|
||||
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Apply the given {@code Consumer} to this builder instance.
|
||||
* <p>This can be useful for applying pre-packaged customizations.
|
||||
* @param builderConsumer the consumer to apply
|
||||
*/
|
||||
Builder apply(Consumer<Builder> builderConsumer);
|
||||
|
||||
/**
|
||||
* Add the given cookie to all requests.
|
||||
* @param cookieName the cookie name
|
||||
* @param cookieValues the cookie values
|
||||
*/
|
||||
Builder defaultCookie(String cookieName, String... cookieValues);
|
||||
|
||||
/**
|
||||
* Manipulate the default cookies with the given consumer. The
|
||||
* map provided to the consumer is "live", so that the consumer can be used to
|
||||
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
|
||||
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
|
||||
* {@link MultiValueMap} methods.
|
||||
* @param cookiesConsumer a function that consumes the cookies map
|
||||
* @return this builder
|
||||
*/
|
||||
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header values
|
||||
*/
|
||||
Builder defaultHeader(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Manipulate the default headers with the given consumer. The
|
||||
* headers provided to the consumer are "live", so that the consumer can be used to
|
||||
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
|
||||
* {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other
|
||||
* {@link HttpHeaders} methods.
|
||||
* @param headersConsumer a function that consumes the {@code HttpHeaders}
|
||||
* @return this builder
|
||||
*/
|
||||
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
|
||||
|
||||
/**
|
||||
* Provide a pre-configured {@link UriBuilderFactory} instance as an
|
||||
* alternative to and effectively overriding {@link #baseUrl(String)}.
|
||||
*/
|
||||
Builder uriBuilderFactory(UriBuilderFactory uriFactory);
|
||||
|
||||
/**
|
||||
* Build the {@link RestTestClient} instance.
|
||||
*/
|
||||
RestTestClient build();
|
||||
|
||||
/**
|
||||
* Configure a base URI as described in
|
||||
* {@link RestClient#create(String)
|
||||
* WebClient.create(String)}.
|
||||
*/
|
||||
Builder baseUrl(String baseUrl);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.test.web.server.RestTestClient.RouterFunctionSpec;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a {@link RouterFunctionMockMvcBuilder} that implements
|
||||
* {@link RouterFunctionSpec}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class RouterFunctionMockMvcSpec extends AbstractMockMvcServerSpec<RouterFunctionSpec>
|
||||
implements RouterFunctionSpec {
|
||||
|
||||
private final RouterFunctionMockMvcBuilder mockMvcBuilder;
|
||||
|
||||
RouterFunctionMockMvcSpec(RouterFunction<?>... routerFunctions) {
|
||||
this.mockMvcBuilder = MockMvcBuilders.routerFunctions(routerFunctions);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec messageConverters(HttpMessageConverter<?>... messageConverters) {
|
||||
this.mockMvcBuilder.setMessageConverters(messageConverters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec interceptors(HandlerInterceptor... interceptors) {
|
||||
mappedInterceptors(null, interceptors);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec mappedInterceptors(String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) {
|
||||
this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec asyncRequestTimeout(long timeout) {
|
||||
this.mockMvcBuilder.setAsyncRequestTimeout(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) {
|
||||
this.mockMvcBuilder.setHandlerExceptionResolvers(exceptionResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec viewResolvers(ViewResolver... resolvers) {
|
||||
this.mockMvcBuilder.setViewResolvers(resolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec singleView(View view) {
|
||||
this.mockMvcBuilder.setSingleView(view);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunctionSpec patternParser(PathPatternParser parser) {
|
||||
this.mockMvcBuilder.setPatternParser(parser);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigurableMockMvcBuilder<?> getMockMvcBuilder() {
|
||||
return this.mockMvcBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.servlet.FlashMapManager;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a {@link StandaloneMockMvcBuilder} that implements
|
||||
* {@link RestTestClient.ControllerSpec}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class StandaloneMockMvcSpec extends AbstractMockMvcServerSpec<RestTestClient.ControllerSpec>
|
||||
implements RestTestClient.ControllerSpec {
|
||||
|
||||
private final StandaloneMockMvcBuilder mockMvcBuilder;
|
||||
|
||||
StandaloneMockMvcSpec(Object... controllers) {
|
||||
this.mockMvcBuilder = MockMvcBuilders.standaloneSetup(controllers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec controllerAdvice(Object... controllerAdvice) {
|
||||
this.mockMvcBuilder.setControllerAdvice(controllerAdvice);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec messageConverters(HttpMessageConverter<?>... messageConverters) {
|
||||
this.mockMvcBuilder.setMessageConverters(messageConverters);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec validator(Validator validator) {
|
||||
this.mockMvcBuilder.setValidator(validator);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec conversionService(FormattingConversionService conversionService) {
|
||||
this.mockMvcBuilder.setConversionService(conversionService);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec interceptors(HandlerInterceptor... interceptors) {
|
||||
mappedInterceptors(null, interceptors);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec mappedInterceptors(
|
||||
String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) {
|
||||
|
||||
this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec contentNegotiationManager(ContentNegotiationManager manager) {
|
||||
this.mockMvcBuilder.setContentNegotiationManager(manager);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec asyncRequestTimeout(long timeout) {
|
||||
this.mockMvcBuilder.setAsyncRequestTimeout(timeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec customArgumentResolvers(HandlerMethodArgumentResolver... argumentResolvers) {
|
||||
this.mockMvcBuilder.setCustomArgumentResolvers(argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec customReturnValueHandlers(HandlerMethodReturnValueHandler... handlers) {
|
||||
this.mockMvcBuilder.setCustomReturnValueHandlers(handlers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) {
|
||||
this.mockMvcBuilder.setHandlerExceptionResolvers(exceptionResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec viewResolvers(ViewResolver... resolvers) {
|
||||
this.mockMvcBuilder.setViewResolvers(resolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec singleView(View view) {
|
||||
this.mockMvcBuilder.setSingleView(view);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec localeResolver(LocaleResolver localeResolver) {
|
||||
this.mockMvcBuilder.setLocaleResolver(localeResolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec flashMapManager(FlashMapManager flashMapManager) {
|
||||
this.mockMvcBuilder.setFlashMapManager(flashMapManager);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec patternParser(PathPatternParser parser) {
|
||||
this.mockMvcBuilder.setPatternParser(parser);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec placeholderValue(String name, String value) {
|
||||
this.mockMvcBuilder.addPlaceholderValue(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandaloneMockMvcSpec customHandlerMapping(Supplier<RequestMappingHandlerMapping> factory) {
|
||||
this.mockMvcBuilder.setCustomHandlerMapping(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigurableMockMvcBuilder<?> getMockMvcBuilder() {
|
||||
return this.mockMvcBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
import org.springframework.test.web.server.RestTestClient.ResponseSpec;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertNotNull;
|
||||
|
||||
/**
|
||||
* Assertions on the response status.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*
|
||||
* @see ResponseSpec#expectStatus()
|
||||
*/
|
||||
public class StatusAssertions {
|
||||
|
||||
private final ExchangeResult exchangeResult;
|
||||
|
||||
private final ResponseSpec responseSpec;
|
||||
|
||||
public StatusAssertions(ExchangeResult exchangeResult, ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response status as an {@link HttpStatusCode}.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isEqualTo(HttpStatusCode status) {
|
||||
HttpStatusCode actual = this.exchangeResult.getStatus();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", status, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status as an integer.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isEqualTo(int status) {
|
||||
return isEqualTo(HttpStatusCode.valueOf(status));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.OK} (200).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isOk() {
|
||||
return assertStatusAndReturn(HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CREATED} (201).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isCreated() {
|
||||
return assertStatusAndReturn(HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isAccepted() {
|
||||
return assertStatusAndReturn(HttpStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNoContent() {
|
||||
return assertStatusAndReturn(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FOUND} (302).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isFound() {
|
||||
return assertStatusAndReturn(HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isSeeOther() {
|
||||
return assertStatusAndReturn(HttpStatus.SEE_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNotModified() {
|
||||
return assertStatusAndReturn(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isTemporaryRedirect() {
|
||||
return assertStatusAndReturn(HttpStatus.TEMPORARY_REDIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isPermanentRedirect() {
|
||||
return assertStatusAndReturn(HttpStatus.PERMANENT_REDIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isBadRequest() {
|
||||
return assertStatusAndReturn(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isUnauthorized() {
|
||||
return assertStatusAndReturn(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403).
|
||||
* @since 5.0.2
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isForbidden() {
|
||||
return assertStatusAndReturn(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
|
||||
*/
|
||||
public RestTestClient.ResponseSpec isNotFound() {
|
||||
return assertStatusAndReturn(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response error message.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec reasonEquals(String reason) {
|
||||
String actual = getReasonPhrase(this.exchangeResult.getStatus());
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Response status reason", reason, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private static String getReasonPhrase(HttpStatusCode statusCode) {
|
||||
if (statusCode instanceof HttpStatus status) {
|
||||
return status.getReasonPhrase();
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 1xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is1xxInformational() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.INFORMATIONAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 2xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is2xxSuccessful() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.SUCCESSFUL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 3xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is3xxRedirection() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.REDIRECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 4xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is4xxClientError() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.CLIENT_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 5xx range.
|
||||
*/
|
||||
public RestTestClient.ResponseSpec is5xxServerError() {
|
||||
return assertSeriesAndReturn(HttpStatus.Series.SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the response status value with a Hamcrest matcher.
|
||||
* @param matcher the matcher to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(Matcher<? super Integer> matcher) {
|
||||
int actual = this.exchangeResult.getStatus().value();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat("Response status", actual, matcher));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the response status value as an integer.
|
||||
* @param consumer the consumer to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public RestTestClient.ResponseSpec value(Consumer<Integer> consumer) {
|
||||
int actual = this.exchangeResult.getStatus().value();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
||||
private ResponseSpec assertStatusAndReturn(HttpStatus expected) {
|
||||
assertNotNull("exchangeResult unexpectedly null", this.exchangeResult);
|
||||
HttpStatusCode actual = this.exchangeResult.getStatus();
|
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", expected, actual));
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
private RestTestClient.ResponseSpec assertSeriesAndReturn(HttpStatus.Series expected) {
|
||||
HttpStatusCode status = this.exchangeResult.getStatus();
|
||||
HttpStatus.Series series = HttpStatus.Series.resolve(status.value());
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.assertEquals("Range for response status value " + status, expected, series));
|
||||
return this.responseSpec;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.util.XpathExpectationsHelper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* XPath assertions for the {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class XpathAssertions {
|
||||
|
||||
private final RestTestClient.BodyContentSpec bodySpec;
|
||||
|
||||
private final XpathExpectationsHelper xpathHelper;
|
||||
|
||||
|
||||
XpathAssertions(RestTestClient.BodyContentSpec spec,
|
||||
String expression, @Nullable Map<String, String> namespaces, Object... args) {
|
||||
|
||||
this.bodySpec = spec;
|
||||
this.xpathHelper = initXpathHelper(expression, namespaces, args);
|
||||
}
|
||||
|
||||
private static XpathExpectationsHelper initXpathHelper(
|
||||
String expression, @Nullable Map<String, String> namespaces, Object[] args) {
|
||||
|
||||
try {
|
||||
return new XpathExpectationsHelper(expression, namespaces, args);
|
||||
}
|
||||
catch (XPathExpressionException ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(String expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(Double expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec isEqualTo(boolean expectedValue) {
|
||||
return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec exists() {
|
||||
return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec doesNotExist() {
|
||||
return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, int)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(int expectedCount) {
|
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec string(Matcher<? super String> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec number(Matcher<? super Double> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(Matcher<? super Integer> matcher){
|
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the XPath evaluation as a String.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec string(Consumer<String> consumer){
|
||||
return assertWith(() -> {
|
||||
String value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), String.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the result of the XPath evaluation as a Double.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec number(Consumer<Double> consumer){
|
||||
return assertWith(() -> {
|
||||
Double value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Double.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the count of nodes as result of the XPath evaluation.
|
||||
*/
|
||||
public RestTestClient.BodyContentSpec nodeCount(Consumer<Integer> consumer){
|
||||
return assertWith(() -> {
|
||||
Integer value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Integer.class);
|
||||
consumer.accept(value);
|
||||
});
|
||||
}
|
||||
|
||||
private RestTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) {
|
||||
try {
|
||||
task.run();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("XML parsing error", ex);
|
||||
}
|
||||
return this.bodySpec;
|
||||
}
|
||||
|
||||
private byte[] getContent() {
|
||||
byte[] body = this.bodySpec.returnResult().getResponseBody();
|
||||
Assert.notNull(body, "Expected body content");
|
||||
return body;
|
||||
}
|
||||
|
||||
private String getCharset() {
|
||||
return Optional.of(this.bodySpec.returnResult())
|
||||
.map(EntityExchangeResult::getResponseHeaders)
|
||||
.map(HttpHeaders::getContentType)
|
||||
.map(MimeType::getCharset)
|
||||
.orElse(StandardCharsets.UTF_8)
|
||||
.name();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
throw new AssertionError("Object#equals is disabled " +
|
||||
"to avoid being used in error instead of XPathAssertions#isEqualTo(String).");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lets us be able to use lambda expressions that could throw checked exceptions, since
|
||||
* {@link XpathExpectationsHelper} throws {@link Exception} on its methods.
|
||||
*/
|
||||
private interface CheckedExceptionTask {
|
||||
|
||||
void run() throws Exception;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Support for testing Spring Web server endpoints via
|
||||
* {@link org.springframework.test.web.server.RestTestClient}.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link CookieAssertions}
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class CookieAssertionsTests {
|
||||
|
||||
private final ResponseCookie cookie = ResponseCookie.from("foo", "bar")
|
||||
.maxAge(Duration.ofMinutes(30))
|
||||
.domain("foo.com")
|
||||
.path("/foo")
|
||||
.secure(true)
|
||||
.httpOnly(true)
|
||||
.partitioned(true)
|
||||
.sameSite("Lax")
|
||||
.build();
|
||||
|
||||
private final CookieAssertions assertions = cookieAssertions(cookie);
|
||||
|
||||
|
||||
@Test
|
||||
void valueEquals() {
|
||||
assertions.valueEquals("foo", "bar");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("what?!", "bar"));
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("foo", "what?!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void value() {
|
||||
assertions.value("foo", equalTo("bar"));
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.value("foo", equalTo("what?!")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueConsumer() {
|
||||
assertions.value("foo", input -> assertThat(input).isEqualTo("bar"));
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.value("foo", input -> assertThat(input).isEqualTo("what?!")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exists() {
|
||||
assertions.exists("foo");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.exists("what?!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotExist() {
|
||||
assertions.doesNotExist("what?!");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.doesNotExist("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAge() {
|
||||
assertions.maxAge("foo", Duration.ofMinutes(30));
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.maxAge("foo", Duration.ofMinutes(29)));
|
||||
|
||||
assertions.maxAge("foo", equalTo(Duration.ofMinutes(30).getSeconds()));
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.maxAge("foo", equalTo(Duration.ofMinutes(29).getSeconds())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void domain() {
|
||||
assertions.domain("foo", "foo.com");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", "what.com"));
|
||||
|
||||
assertions.domain("foo", equalTo("foo.com"));
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", equalTo("what.com")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void path() {
|
||||
assertions.path("foo", "/foo");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", "/what"));
|
||||
|
||||
assertions.path("foo", equalTo("/foo"));
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", equalTo("/what")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void secure() {
|
||||
assertions.secure("foo", true);
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.secure("foo", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpOnly() {
|
||||
assertions.httpOnly("foo", true);
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.httpOnly("foo", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void partitioned() {
|
||||
assertions.partitioned("foo", true);
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.partitioned("foo", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSite() {
|
||||
assertions.sameSite("foo", "Lax");
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.sameSite("foo", "Strict"));
|
||||
}
|
||||
|
||||
|
||||
private CookieAssertions cookieAssertions(ResponseCookie cookie) {
|
||||
RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock();
|
||||
var headers = new HttpHeaders();
|
||||
headers.set(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
when(response.getHeaders()).thenReturn(headers);
|
||||
ExchangeResult result = new ExchangeResult(response);
|
||||
return new CookieAssertions(result, mock());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link HeaderAssertions}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class HeaderAssertionTests {
|
||||
|
||||
@Test
|
||||
void valueEquals() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("foo", "bar");
|
||||
headers.add("age", "22");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.valueEquals("foo", "bar");
|
||||
assertions.value("foo", s -> assertThat(s).isEqualTo("bar"));
|
||||
assertions.values("foo", strings -> assertThat(strings).containsExactly("bar"));
|
||||
assertions.valueEquals("age", 22);
|
||||
|
||||
// Missing header
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEquals("what?!", "bar"));
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEquals("foo", "what?!"));
|
||||
|
||||
// Wrong # of values
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEquals("foo", "bar", "what?!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueEqualsWithMultipleValues() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("foo", "bar");
|
||||
headers.add("foo", "baz");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.valueEquals("foo", "bar", "baz");
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEquals("foo", "bar", "what?!"));
|
||||
|
||||
// Too few values
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEquals("foo", "bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueMatches() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8"));
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.valueMatches("Content-Type", ".*UTF-8.*");
|
||||
|
||||
// Wrong pattern
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.valueMatches("Content-Type", ".*ISO-8859-1.*"))
|
||||
.satisfies(ex -> assertThat(ex).hasMessage("Response header " +
|
||||
"'Content-Type'=[application/json;charset=UTF-8] does not match " +
|
||||
"[.*ISO-8859-1.*]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueMatchesWithNonexistentHeader() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8"));
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.valueMatches("Content-XYZ", ".*ISO-8859-1.*"))
|
||||
.withMessage("Response header 'Content-XYZ' not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void valuesMatch() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("foo", "value1");
|
||||
headers.add("foo", "value2");
|
||||
headers.add("foo", "value3");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
assertions.valuesMatch("foo", "val.*1", "val.*2", "val.*3");
|
||||
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.valuesMatch("foo", ".*", "val.*5"))
|
||||
.satisfies(ex -> assertThat(ex).hasMessage(
|
||||
"Response header 'foo' has fewer or more values [value1, value2, value3] " +
|
||||
"than number of patterns to match with [.*, val.*5]"));
|
||||
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.valuesMatch("foo", ".*", "val.*5", ".*"))
|
||||
.satisfies(ex -> assertThat(ex).hasMessage(
|
||||
"Response header 'foo'[1]='value2' does not match 'val.*5'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valueMatcher() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("foo", "bar");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
assertions.value("foo", containsString("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void valuesMatcher() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("foo", "bar");
|
||||
headers.add("foo", "baz");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
assertions.values("foo", hasItems("bar", "baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exists() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.exists("Content-Type");
|
||||
|
||||
// Header should not exist
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.exists("Framework"))
|
||||
.satisfies(ex -> assertThat(ex).hasMessage("Response header 'Framework' does not exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotExist() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8"));
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.doesNotExist("Framework");
|
||||
|
||||
// Existing header
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.doesNotExist("Content-Type"))
|
||||
.satisfies(ex -> assertThat(ex).hasMessage("Response header " +
|
||||
"'Content-Type' exists with value=[application/json;charset=UTF-8]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentTypeCompatibleWith() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.contentTypeCompatibleWith(MediaType.parseMediaType("application/*"));
|
||||
assertions.contentTypeCompatibleWith("application/*");
|
||||
|
||||
// MediaTypes not compatible
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertions.contentTypeCompatibleWith(MediaType.TEXT_XML))
|
||||
.withMessage("Response header 'Content-Type'=[application/xml] is not compatible with [text/xml]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void location() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setLocation(URI.create("http://localhost:8080/"));
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.location("http://localhost:8080/");
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.location("http://localhost:8081/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheControl() {
|
||||
CacheControl control = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setCacheControl(control.getHeaderValue());
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
|
||||
// Success
|
||||
assertions.cacheControl(control);
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.cacheControl(CacheControl.noStore()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentDisposition() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDispositionFormData("foo", "bar");
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.contentDisposition(ContentDisposition.formData().name("foo").filename("bar").build());
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.contentDisposition(ContentDisposition.attachment().build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentLength() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentLength(100);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.contentLength(100);
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.contentLength(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentType() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.contentType(MediaType.APPLICATION_JSON);
|
||||
assertions.contentType("application/json");
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.contentType(MediaType.APPLICATION_XML));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void expires() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
ZonedDateTime expires = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"));
|
||||
headers.setExpires(expires);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.expires(expires.toInstant().toEpochMilli());
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.expires(expires.toInstant().toEpochMilli() + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastModified() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
ZonedDateTime lastModified = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"));
|
||||
headers.setLastModified(lastModified.toInstant().toEpochMilli());
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.lastModified(lastModified.toInstant().toEpochMilli());
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.lastModified(lastModified.toInstant().toEpochMilli() + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsDate() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setDate("foo", 1000);
|
||||
HeaderAssertions assertions = headerAssertions(headers);
|
||||
assertions.valueEqualsDate("foo", 1000);
|
||||
|
||||
// Wrong value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.valueEqualsDate("foo", 2000));
|
||||
}
|
||||
|
||||
private HeaderAssertions headerAssertions(HttpHeaders responseHeaders) {
|
||||
RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock();
|
||||
when(response.getHeaders()).thenReturn(responseHeaders);
|
||||
ExchangeResult result = new ExchangeResult(response);
|
||||
return new HeaderAssertions(result, mock());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.Person;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.in;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests JSON Path assertions with {@link RestTestClient}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class JsonPathAssertionTests {
|
||||
|
||||
private final RestTestClient client =
|
||||
RestTestClient.bindToController(new MusicController())
|
||||
.alwaysExpect(status().isOk())
|
||||
.alwaysExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.configureClient()
|
||||
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||||
.build();
|
||||
|
||||
|
||||
@Test
|
||||
void exists() {
|
||||
String composerByName = "$.composers[?(@.name == '%s')]";
|
||||
String performerByName = "$.performers[?(@.name == '%s')]";
|
||||
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath(composerByName.formatted("Johann Sebastian Bach")).exists()
|
||||
.jsonPath(composerByName.formatted("Johannes Brahms")).exists()
|
||||
.jsonPath(composerByName.formatted("Edvard Grieg")).exists()
|
||||
.jsonPath(composerByName.formatted("Robert Schumann")).exists()
|
||||
.jsonPath(performerByName.formatted("Vladimir Ashkenazy")).exists()
|
||||
.jsonPath(performerByName.formatted("Yehudi Menuhin")).exists()
|
||||
.jsonPath("$.composers[0]").exists()
|
||||
.jsonPath("$.composers[1]").exists()
|
||||
.jsonPath("$.composers[2]").exists()
|
||||
.jsonPath("$.composers[3]").exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotExist() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[?(@.name == 'Edvard Grieeeeeeg')]").doesNotExist()
|
||||
.jsonPath("$.composers[?(@.name == 'Robert Schuuuuuuman')]").doesNotExist()
|
||||
.jsonPath("$.composers[4]").doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void equality() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].name").isEqualTo("Johann Sebastian Bach")
|
||||
.jsonPath("$.performers[1].name").isEqualTo("Yehudi Menuhin");
|
||||
|
||||
// Hamcrest matchers...
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].name").value(equalTo("Johann Sebastian Bach"))
|
||||
.jsonPath("$.performers[1].name").value(equalTo("Yehudi Menuhin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hamcrestMatcher() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].name").value(startsWith("Johann"))
|
||||
.jsonPath("$.performers[0].name").value(endsWith("Ashkenazy"))
|
||||
.jsonPath("$.performers[1].name").value(containsString("di Me"))
|
||||
.jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hamcrestMatcherWithParameterizedJsonPath() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].name").value(String.class, startsWith("Johann"))
|
||||
.jsonPath("$.composers[0].name").value(String.class, s -> assertThat(s).startsWith("Johann"))
|
||||
.jsonPath("$.composers[0].name").value(o -> assertThat((String) o).startsWith("Johann"))
|
||||
.jsonPath("$.performers[1].name").value(containsString("di Me"))
|
||||
.jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEmpty() {
|
||||
client.get().uri("/music/instruments")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.clarinets").isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotEmpty() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers").isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasJsonPath() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers").hasJsonPath();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHaveJsonPath() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.audience").doesNotHaveJsonPath();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isBoolean() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].someBoolean").isBoolean();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNumber() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers[0].someDouble").isNumber();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isMap() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$").isMap();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isArray() {
|
||||
client.get().uri("/music/people")
|
||||
.exchange()
|
||||
.expectBody()
|
||||
.jsonPath("$.composers").isArray();
|
||||
}
|
||||
|
||||
@RestController
|
||||
private static class MusicController {
|
||||
@GetMapping("/music/instruments")
|
||||
public Map<String, Object> getInstruments() {
|
||||
return Map.of("clarinets", List.of());
|
||||
}
|
||||
|
||||
@GetMapping("/music/people")
|
||||
public MultiValueMap<String, Person> get() {
|
||||
MultiValueMap<String, Person> map = new LinkedMultiValueMap<>();
|
||||
|
||||
map.add("composers", new Person("Johann Sebastian Bach"));
|
||||
map.add("composers", new Person("Johannes Brahms"));
|
||||
map.add("composers", new Person("Edvard Grieg"));
|
||||
map.add("composers", new Person("Robert Schumann"));
|
||||
|
||||
map.add("performers", new Person("Vladimir Ashkenazy"));
|
||||
map.add("performers", new Person("Yehudi Menuhin"));
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Tests for {@link StandaloneMockMvcSpec}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
public class StandaloneMockMvcSpecTests {
|
||||
|
||||
@Test
|
||||
public void controller() {
|
||||
new StandaloneMockMvcSpec(new MyController()).build()
|
||||
.get().uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("Success");
|
||||
|
||||
new StandaloneMockMvcSpec(new MyController()).build()
|
||||
.get().uri("")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("Success");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerAdvice() {
|
||||
new StandaloneMockMvcSpec(new MyController())
|
||||
.controllerAdvice(new MyControllerAdvice())
|
||||
.build()
|
||||
.get().uri("/exception")
|
||||
.exchange()
|
||||
.expectStatus().isBadRequest()
|
||||
.expectBody(String.class).isEqualTo("Handled exception");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerAdviceWithClassArgument() {
|
||||
new StandaloneMockMvcSpec(MyController.class)
|
||||
.controllerAdvice(MyControllerAdvice.class)
|
||||
.build()
|
||||
.get().uri("/exception")
|
||||
.exchange()
|
||||
.expectStatus().isBadRequest()
|
||||
.expectBody(String.class).isEqualTo("Handled exception");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RestController
|
||||
private static class MyController {
|
||||
|
||||
@GetMapping
|
||||
public String handleRootPath() {
|
||||
return "Success";
|
||||
}
|
||||
|
||||
@GetMapping("/exception")
|
||||
public void handleWithError() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ControllerAdvice
|
||||
private static class MyControllerAdvice {
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<String> handle(IllegalStateException ex) {
|
||||
return ResponseEntity.status(400).body("Handled exception");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestConsumer<T> implements Consumer<T> {
|
||||
|
||||
private T value;
|
||||
|
||||
public T getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(T t) {
|
||||
this.value = t;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link StatusAssertions}.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class StatusAssertionTests {
|
||||
|
||||
@Test
|
||||
void isEqualTo() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.CONFLICT);
|
||||
|
||||
// Success
|
||||
assertions.isEqualTo(HttpStatus.CONFLICT);
|
||||
assertions.isEqualTo(409);
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.isEqualTo(HttpStatus.REQUEST_TIMEOUT));
|
||||
|
||||
// Wrong status value
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.isEqualTo(408));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEqualToWithCustomStatus() {
|
||||
StatusAssertions assertions = statusAssertions(600);
|
||||
|
||||
// Success
|
||||
assertions.isEqualTo(600);
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(601).isEqualTo(600));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void reasonEquals() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.CONFLICT);
|
||||
|
||||
// Success
|
||||
assertions.reasonEquals("Conflict");
|
||||
|
||||
// Wrong reason
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).reasonEquals("Conflict"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusSeries1xx() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.CONTINUE);
|
||||
|
||||
// Success
|
||||
assertions.is1xxInformational();
|
||||
|
||||
// Wrong series
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.OK).is1xxInformational());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusSeries2xx() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.OK);
|
||||
|
||||
// Success
|
||||
assertions.is2xxSuccessful();
|
||||
|
||||
// Wrong series
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).is2xxSuccessful());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusSeries3xx() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.PERMANENT_REDIRECT);
|
||||
|
||||
// Success
|
||||
assertions.is3xxRedirection();
|
||||
|
||||
// Wrong series
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusSeries4xx() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.BAD_REQUEST);
|
||||
|
||||
// Success
|
||||
assertions.is4xxClientError();
|
||||
|
||||
// Wrong series
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).is4xxClientError());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusSeries5xx() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
// Success
|
||||
assertions.is5xxServerError();
|
||||
|
||||
// Wrong series
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
statusAssertions(HttpStatus.OK).is5xxServerError());
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesStatusValue() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.CONFLICT);
|
||||
|
||||
// Success
|
||||
assertions.value(equalTo(409));
|
||||
assertions.value(greaterThan(400));
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||
assertions.value(equalTo(200)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesCustomStatusValue() {
|
||||
statusAssertions(600).value(equalTo(600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumesStatusValue() {
|
||||
StatusAssertions assertions = statusAssertions(HttpStatus.CONFLICT);
|
||||
|
||||
// Success
|
||||
assertions.value((Integer value) -> assertThat(value).isEqualTo(409));
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsAccepted() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.ACCEPTED).isAccepted();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isAccepted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsNoContent() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.NO_CONTENT).isNoContent();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsFound() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.FOUND).isFound();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsSeeOther() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.SEE_OTHER).isSeeOther();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isSeeOther());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsNotModified() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.NOT_MODIFIED).isNotModified();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isNotModified());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsTemporaryRedirect() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.TEMPORARY_REDIRECT).isTemporaryRedirect();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isTemporaryRedirect());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsPermanentRedirect() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.PERMANENT_REDIRECT).isPermanentRedirect();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isPermanentRedirect());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsUnauthorized() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.UNAUTHORIZED).isUnauthorized();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsForbidden() {
|
||||
// Success
|
||||
statusAssertions(HttpStatus.FORBIDDEN).isForbidden();
|
||||
|
||||
// Wrong status
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> statusAssertions(HttpStatus.INTERNAL_SERVER_ERROR).isForbidden());
|
||||
}
|
||||
|
||||
private StatusAssertions statusAssertions(HttpStatus status) {
|
||||
return statusAssertions(status.value());
|
||||
}
|
||||
|
||||
private StatusAssertions statusAssertions(int status) {
|
||||
try {
|
||||
RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse response = mock();
|
||||
when(response.getStatusCode()).thenReturn(HttpStatusCode.valueOf(status));
|
||||
ExchangeResult result = new ExchangeResult(response);
|
||||
return new StatusAssertions(result, mock());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new AssertionError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Tests with error status codes or error conditions.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class ErrorTests {
|
||||
|
||||
private final RestTestClient client = RestTestClient.bindToController(new TestController()).build();
|
||||
|
||||
|
||||
@Test
|
||||
void notFound(){
|
||||
this.client.get().uri("/invalid")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound();
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverException() {
|
||||
this.client.get().uri("/server-error")
|
||||
.exchange()
|
||||
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("/server-error")
|
||||
void handleAndThrowException() {
|
||||
throw new IllegalStateException("server error");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Tests with headers and cookies.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class HeaderAndCookieTests {
|
||||
|
||||
private final RestTestClient client = RestTestClient.bindToController(new TestController()).build();
|
||||
|
||||
@Test
|
||||
void requestResponseHeaderPair() {
|
||||
this.client.get().uri("/header-echo")
|
||||
.header("h1", "in")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals("h1", "in-out");
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerMultipleValues() {
|
||||
this.client.get().uri("/header-multi-value")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals("h1", "v1", "v2", "v3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setCookies() {
|
||||
this.client.get().uri("/cookie-echo")
|
||||
.cookies(cookies -> cookies.add("k1", "v1"))
|
||||
.exchange()
|
||||
.expectHeader().valueMatches("Set-Cookie", "k1=v1");
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("header-echo")
|
||||
ResponseEntity<Void> handleHeader(@RequestHeader("h1") String myHeader) {
|
||||
String value = myHeader + "-out";
|
||||
return ResponseEntity.ok().header("h1", value).build();
|
||||
}
|
||||
|
||||
@GetMapping("header-multi-value")
|
||||
ResponseEntity<Void> multiValue() {
|
||||
return ResponseEntity.ok().header("h1", "v1", "v2", "v3").build();
|
||||
}
|
||||
|
||||
@GetMapping("cookie-echo")
|
||||
ResponseEntity<Void> handleCookie(@CookieValue("k1") String cookieValue) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Set-Cookie", "k1=" + cookieValue);
|
||||
return new ResponseEntity<>(headers, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.json.JsonCompareMode;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
/**
|
||||
* Samples of tests using {@link RestTestClient} with serialized JSON content.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class JsonContentTests {
|
||||
|
||||
private final RestTestClient client = RestTestClient.bindToController(new PersonController()).build();
|
||||
|
||||
|
||||
@Test
|
||||
void jsonContentWithDefaultLenientMode() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().json("""
|
||||
[
|
||||
{"firstName":"Jane"},
|
||||
{"firstName":"Jason"},
|
||||
{"firstName":"John"}
|
||||
]
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonContentWithStrictMode() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().json("""
|
||||
[
|
||||
{"firstName":"Jane", "lastName":"Williams"},
|
||||
{"firstName":"Jason","lastName":"Johnson"},
|
||||
{"firstName":"John", "lastName":"Smith"}
|
||||
]
|
||||
""",
|
||||
JsonCompareMode.STRICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonContentWithStrictModeAndMissingAttributes() {
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectBody().json("""
|
||||
[
|
||||
{"firstName":"Jane"},
|
||||
{"firstName":"Jason"},
|
||||
{"firstName":"John"}
|
||||
]
|
||||
""",
|
||||
JsonCompareMode.STRICT)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonPathIsEqualTo() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].firstName").isEqualTo("Jane")
|
||||
.jsonPath("$[1].firstName").isEqualTo("Jason")
|
||||
.jsonPath("$[2].firstName").isEqualTo("John");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonPathMatches() {
|
||||
this.client.get().uri("/persons/John/Smith")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$.firstName").value(containsString("oh"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postJsonContent() {
|
||||
this.client.post().uri("/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("""
|
||||
{"firstName":"John", "lastName":"Smith"}
|
||||
""")
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/persons")
|
||||
static class PersonController {
|
||||
|
||||
@GetMapping
|
||||
List<Person> getPersons() {
|
||||
return List.of(new Person("Jane", "Williams"), new Person("Jason", "Johnson"), new Person("John", "Smith"));
|
||||
}
|
||||
|
||||
@GetMapping("/{firstName}/{lastName}")
|
||||
Person getPerson(@PathVariable String firstName, @PathVariable String lastName) {
|
||||
return new Person(firstName, lastName);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
ResponseEntity<String> savePerson(@RequestBody Person person) {
|
||||
return ResponseEntity.created(URI.create(String.format("/persons/%s/%s", person.getFirstName(), person.getLastName()))).build();
|
||||
}
|
||||
}
|
||||
|
||||
static class Person {
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
public Person() {
|
||||
}
|
||||
|
||||
public Person(String firstName, String lastName) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return this.firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return this.lastName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.xml.bind.annotation.XmlRootElement;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@XmlRootElement
|
||||
class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
|
||||
// No-arg constructor for XML
|
||||
public Person() {
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public Person(@JsonProperty("name") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Person person = (Person) other;
|
||||
return getName().equals(person.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getName().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Person[name='" + name + "']";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
/**
|
||||
* Annotated controllers accepting and returning typed Objects.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class ResponseEntityTests {
|
||||
private final RestTestClient client = RestTestClient.bindToController(new PersonController())
|
||||
.configureClient()
|
||||
.baseUrl("/persons")
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void entity() {
|
||||
this.client.get().uri("/John")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(Person.class).isEqualTo(new Person("John"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityMatcher() {
|
||||
this.client.get().uri("/John")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(Person.class).value(Person::getName, startsWith("Joh"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityWithConsumer() {
|
||||
this.client.get().uri("/John")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> assertThat(result.getResponseBody()).isEqualTo(new Person("John")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityList() {
|
||||
List<Person> expected = List.of(
|
||||
new Person("Jane"), new Person("Jason"), new Person("John"));
|
||||
|
||||
this.client.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(new ParameterizedTypeReference<List<Person>>() {}).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityListWithConsumer() {
|
||||
this.client.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
.expectBody(new ParameterizedTypeReference<List<Person>>() {})
|
||||
.value(people ->
|
||||
assertThat(people).contains(new Person("Jason"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityMap() {
|
||||
Map<String, Person> map = new LinkedHashMap<>();
|
||||
map.put("Jane", new Person("Jane"));
|
||||
map.put("Jason", new Person("Jason"));
|
||||
map.put("John", new Person("John"));
|
||||
|
||||
this.client.get().uri("?map=true")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(new ParameterizedTypeReference<Map<String, Person>>() {}).isEqualTo(map);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postEntity() {
|
||||
this.client.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(new Person("John"))
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectHeader().valueEquals("location", "/persons/John")
|
||||
.expectBody().isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/persons")
|
||||
static class PersonController {
|
||||
|
||||
@GetMapping(path = "/{name}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
Person getPerson(@PathVariable String name) {
|
||||
return new Person(name);
|
||||
}
|
||||
|
||||
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
List<Person> getPersons() {
|
||||
return List.of(new Person("Jane"), new Person("Jason"), new Person("John"));
|
||||
}
|
||||
|
||||
@GetMapping(params = "map", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
Map<String, Person> getPersonsAsMap() {
|
||||
Map<String, Person> map = new LinkedHashMap<>();
|
||||
map.put("Jane", new Person("Jane"));
|
||||
map.put("Jason", new Person("Jason"));
|
||||
map.put("John", new Person("John"));
|
||||
return map;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<String> savePerson(@RequestBody Person person) {
|
||||
return ResponseEntity.created(URI.create("/persons/" + person.getName())).build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests using the {@link RestTestClient} API.
|
||||
*/
|
||||
class RestTestClientTests {
|
||||
|
||||
private RestTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = RestTestClient.bindToController(new TestController()).build();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class HttpMethods {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"})
|
||||
void testMethod(String method) {
|
||||
RestTestClientTests.this.client.method(HttpMethod.valueOf(method)).uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGet() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("GET");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPost() {
|
||||
RestTestClientTests.this.client.post().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("POST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPut() {
|
||||
RestTestClientTests.this.client.put().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("PUT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDelete() {
|
||||
RestTestClientTests.this.client.delete().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("DELETE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPatch() {
|
||||
RestTestClientTests.this.client.patch().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("PATCH");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHead() {
|
||||
RestTestClientTests.this.client.head().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.method").isEqualTo("HEAD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptions() {
|
||||
RestTestClientTests.this.client.options().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS")
|
||||
.expectBody().isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Mutation {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
RestTestClientTests.this.client.mutate()
|
||||
.apply(builder -> builder.defaultHeader("foo", "bar"))
|
||||
.uriBuilderFactory(new DefaultUriBuilderFactory("/test"))
|
||||
.defaultCookie("foo", "bar")
|
||||
.defaultCookies(cookies -> cookies.add("a", "b"))
|
||||
.defaultHeaders(headers -> headers.set("a", "b"))
|
||||
.build().get()
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$.uri").isEqualTo("/test")
|
||||
.jsonPath("$.headers.Cookie").isEqualTo("foo=bar; a=b")
|
||||
.jsonPath("$.headers.foo").isEqualTo("bar")
|
||||
.jsonPath("$.headers.a").isEqualTo("b");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Uris {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.uri").isEqualTo("/test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithPathVariables() {
|
||||
RestTestClientTests.this.client.get().uri("/test/{id}", 1)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.uri").isEqualTo("/test/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithParameterMap() {
|
||||
RestTestClientTests.this.client.get().uri("/test/{id}", Map.of("id", 1))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.uri").isEqualTo("/test/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithUrlBuilder() {
|
||||
RestTestClientTests.this.client.get().uri(builder -> builder.path("/test/{id}").build(1))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.uri").isEqualTo("/test/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testURI() {
|
||||
RestTestClientTests.this.client.get().uri(URI.create("/test"))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.uri").isEqualTo("/test");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Cookies {
|
||||
@Test
|
||||
void testCookie() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.cookie("foo", "bar")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.Cookie").isEqualTo("foo=bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCookies() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.cookies(cookies -> cookies.add("foo", "bar"))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.Cookie").isEqualTo("foo=bar");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Headers {
|
||||
@Test
|
||||
void testHeader() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.header("foo", "bar")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.foo").isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHeaders() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.headers(headers -> headers.set("foo", "bar"))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.foo").isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContentType() {
|
||||
RestTestClientTests.this.client.post().uri("/test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.Content-Type").isEqualTo("application/json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcceptCharset() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.acceptCharset(StandardCharsets.UTF_8)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.Accept-Charset").isEqualTo("utf-8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIfModifiedSince() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.ifModifiedSince(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneId.of("GMT")))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.If-Modified-Since").isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIfNoneMatch() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.ifNoneMatch("foo")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().jsonPath("$.headers.If-None-Match").isEqualTo("foo");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Expectations {
|
||||
@Test
|
||||
void testExpectCookie() {
|
||||
RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectCookie().value("session", Matchers.equalTo("abc"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReturnResults {
|
||||
@Test
|
||||
void testBodyReturnResult() {
|
||||
var result = RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Map.class).returnResult();
|
||||
assertThat(result.getResponseBody().get("uri")).isEqualTo("/test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReturnResultClass() {
|
||||
var result = RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult(Map.class);
|
||||
assertThat(result.getResponseBody().get("uri")).isEqualTo("/test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReturnResultParameterizedTypeReference() {
|
||||
var result = RestTestClientTests.this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
});
|
||||
assertThat(result.getResponseBody().get("uri")).isEqualTo("/test");
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@RequestMapping(path = {"/test", "/test/*"}, produces = "application/json")
|
||||
public Map<String, Object> handle(
|
||||
@RequestHeader HttpHeaders headers,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
response.addCookie(new Cookie("session", "abc"));
|
||||
return Map.of(
|
||||
"method", request.getMethod(),
|
||||
"uri", request.getRequestURI(),
|
||||
"headers", headers.toSingleValueMap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link RestTestClient} with soft assertions.
|
||||
*
|
||||
*/
|
||||
class SoftAssertionTests {
|
||||
|
||||
private final RestTestClient restTestClient = RestTestClient.bindToController(new TestController()).build();
|
||||
|
||||
|
||||
@Test
|
||||
void expectAll() {
|
||||
this.restTestClient.get().uri("/test").exchange()
|
||||
.expectAll(
|
||||
responseSpec -> responseSpec.expectStatus().isOk(),
|
||||
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("hello")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void expectAllWithMultipleFailures() {
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() ->
|
||||
this.restTestClient.get().uri("/test").exchange()
|
||||
.expectAll(
|
||||
responseSpec -> responseSpec.expectStatus().isBadRequest(),
|
||||
responseSpec -> responseSpec.expectStatus().isOk(),
|
||||
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("bogus")
|
||||
)
|
||||
)
|
||||
.withMessage("""
|
||||
Multiple Exceptions (2):
|
||||
Status expected:<400 BAD_REQUEST> but was:<200 OK>
|
||||
Response body expected:<bogus> but was:<hello>""");
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("/test")
|
||||
String handle() {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlAccessType;
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||
import jakarta.xml.bind.annotation.XmlElement;
|
||||
import jakarta.xml.bind.annotation.XmlRootElement;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
|
||||
/**
|
||||
* Samples of tests using {@link RestTestClient} with XML content.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class XmlContentTests {
|
||||
|
||||
private static final String persons_XML = """
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<persons>
|
||||
<person><name>Jane</name></person>
|
||||
<person><name>Jason</name></person>
|
||||
<person><name>John</name></person>
|
||||
</persons>
|
||||
""";
|
||||
|
||||
|
||||
private final RestTestClient client = RestTestClient.bindToController(new PersonController()).build();
|
||||
|
||||
|
||||
@Test
|
||||
void xmlContent() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().xml(persons_XML);
|
||||
}
|
||||
|
||||
@Test
|
||||
void xpathIsEqualTo() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.xpath("/").exists()
|
||||
.xpath("/persons").exists()
|
||||
.xpath("/persons/person").exists()
|
||||
.xpath("/persons/person").nodeCount(3)
|
||||
.xpath("/persons/person[1]/name").isEqualTo("Jane")
|
||||
.xpath("/persons/person[2]/name").isEqualTo("Jason")
|
||||
.xpath("/persons/person[3]/name").isEqualTo("John");
|
||||
}
|
||||
|
||||
@Test
|
||||
void xpathDoesNotExist() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.xpath("/persons/person[4]").doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void xpathNodeCount() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.xpath("/persons/person").nodeCount(3)
|
||||
.xpath("/persons/person").nodeCount(equalTo(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void xpathMatches() {
|
||||
this.client.get().uri("/persons")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.xpath("//person/name").string(startsWith("J"))
|
||||
.xpath("//person/name").string(s -> {
|
||||
if (!s.startsWith("J")) {
|
||||
throw new AssertionError("Name does not start with J: " + s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void xpathContainsSubstringViaRegex() {
|
||||
this.client.get().uri("/persons/John")
|
||||
.accept(MediaType.APPLICATION_XML)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.xpath("//name[contains(text(), 'oh')]").exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void postXmlContent() {
|
||||
String content =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
|
||||
"<person><name>John</name></person>";
|
||||
|
||||
this.client.post().uri("/persons")
|
||||
.contentType(MediaType.APPLICATION_XML)
|
||||
.body(content)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John")
|
||||
.expectBody().isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@XmlRootElement(name="persons")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class PersonsWrapper {
|
||||
|
||||
@XmlElement(name="person")
|
||||
private final List<Person> persons = new ArrayList<>();
|
||||
|
||||
public PersonsWrapper() {
|
||||
}
|
||||
|
||||
public PersonsWrapper(List<Person> persons) {
|
||||
this.persons.addAll(persons);
|
||||
}
|
||||
|
||||
public PersonsWrapper(Person... persons) {
|
||||
this.persons.addAll(Arrays.asList(persons));
|
||||
}
|
||||
|
||||
public List<Person> getpersons() {
|
||||
return this.persons;
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/persons")
|
||||
static class PersonController {
|
||||
|
||||
@GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
|
||||
PersonsWrapper getPersons() {
|
||||
return new PersonsWrapper(new Person("Jane"), new Person("Jason"), new Person("John"));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{name}", produces = MediaType.APPLICATION_XML_VALUE)
|
||||
Person getPerson(@PathVariable String name) {
|
||||
return new Person(name);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.APPLICATION_XML_VALUE)
|
||||
ResponseEntity<Object> savepersons(@RequestBody Person person) {
|
||||
URI location = URI.create(String.format("/persons/%s", person.getName()));
|
||||
return ResponseEntity.created(location).build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples.bind;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Sample tests demonstrating "mock" server tests binding to server infrastructure
|
||||
* declared in a Spring ApplicationContext.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
@SpringJUnitWebConfig(ApplicationContextTests.WebConfig.class)
|
||||
class ApplicationContextTests {
|
||||
|
||||
private RestTestClient client;
|
||||
private final WebApplicationContext context;
|
||||
|
||||
public ApplicationContextTests(WebApplicationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = RestTestClient.bindToApplicationContext(context).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("It works!");
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class WebConfig {
|
||||
|
||||
@Bean
|
||||
public TestController controller() {
|
||||
return new TestController();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("/test")
|
||||
public String handle() {
|
||||
return "It works!";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples.bind;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Sample tests demonstrating "mock" server tests binding to an annotated
|
||||
* controller.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class ControllerTests {
|
||||
|
||||
private RestTestClient client;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = RestTestClient.bindToController(new TestController()).build();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("It works!");
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@GetMapping("/test")
|
||||
public String handle() {
|
||||
return "It works!";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples.bind;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
import static org.springframework.http.HttpStatus.I_AM_A_TEAPOT;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for a {@link Filter}.
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class FilterTests {
|
||||
|
||||
@Test
|
||||
void filter() {
|
||||
|
||||
Filter filter = new HttpFilter() {
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
|
||||
res.getWriter().write("It works!");
|
||||
}
|
||||
};
|
||||
|
||||
RestTestClient client = RestTestClient.bindToRouterFunction(
|
||||
request -> Optional.of(req -> ServerResponse.status(I_AM_A_TEAPOT).build()))
|
||||
.filter(filter)
|
||||
.build();
|
||||
|
||||
client.get().uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("It works!");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples.bind;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* Sample tests demonstrating live server integration tests.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class HttpServerTests {
|
||||
|
||||
private ReactorHttpServer server;
|
||||
|
||||
private RestTestClient client;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void start() throws Exception {
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler(
|
||||
route(GET("/test"), request -> ServerResponse.ok().bodyValue("It works!")));
|
||||
|
||||
this.server = new ReactorHttpServer();
|
||||
this.server.setHandler(httpHandler);
|
||||
this.server.afterPropertiesSet();
|
||||
this.server.start();
|
||||
|
||||
this.client = RestTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:" + this.server.getPort())
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void stop() {
|
||||
this.server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
this.client.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("It works!");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.web.server.samples.bind;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.test.web.server.RestTestClient;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.GET;
|
||||
import static org.springframework.web.servlet.function.RouterFunctions.route;
|
||||
|
||||
/**
|
||||
* Sample tests demonstrating "mock" server tests binding to a RouterFunction.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
*/
|
||||
class RouterFunctionTests {
|
||||
|
||||
private RestTestClient testClient;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
|
||||
RouterFunction<?> route = route(GET("/test"), request ->
|
||||
ServerResponse.ok().body("It works!"));
|
||||
|
||||
this.testClient = RestTestClient.bindToRouterFunction(route).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() throws Exception {
|
||||
this.testClient.get().uri("/test")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).isEqualTo("It works!");
|
||||
}
|
||||
|
||||
}
|
|
@ -94,13 +94,14 @@
|
|||
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]util[\\/].+Helper" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]client[\\/]match[\\/].+Matchers" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]reactive[\\/]server[\\/].+" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]server[\\/].+" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]servlet[\\/]result[\\/].+Matchers" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<!-- spring-test - test -->
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/].+TestNGTests" checks="IllegalImport" id="bannedTestNGImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/]aot[\\/]samples[\\/]web[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/]junit[\\/]jupiter[\\/]web[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]util[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/](client|reactive|servlet)[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/](client|server|reactive|servlet)[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
|
||||
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/](aot|junit4)" checks="SpringJUnit5"/>
|
||||
<suppress files="AutowiredConfigurationErrorsIntegrationTests" checks="SpringJUnit5" message="Lifecycle method .+ should not be private"/>
|
||||
<suppress files="org[\\/]springframework[\\/]test[\\/]context[\\/].+[\\/](ExpectedExceptionSpringRunnerTests|StandardJUnit4FeaturesTests|ProgrammaticTxMgmtTestNGTests)" checks="RegexpSinglelineJava" id="expectedExceptionAnnotation"/>
|
||||
|
|
Loading…
Reference in New Issue