35 changed files with 5742 additions and 1 deletions
@ -0,0 +1,440 @@
@@ -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") |
||||
---- |
||||
====== |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,236 @@
@@ -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.servlet.client; |
||||
|
||||
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,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* 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.servlet.client; |
||||
|
||||
import java.util.function.Consumer; |
||||
|
||||
import org.springframework.test.web.servlet.MockMvcBuilder; |
||||
|
||||
/** |
||||
* Default implementation of {@link RestTestClient.MockServerBuilder}. |
||||
* @author Rob Worsnop |
||||
* @param <M> the type of the {@link MockMvcBuilder} to use for building the mock server |
||||
*/ |
||||
class DefaultMockServerBuilder<M extends MockMvcBuilder> |
||||
extends DefaultRestTestClientBuilder<RestTestClient.MockServerBuilder<M>> |
||||
implements RestTestClient.MockServerBuilder<M> { |
||||
|
||||
private final M builder; |
||||
|
||||
public DefaultMockServerBuilder(M builder) { |
||||
this.builder = builder; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.MockServerBuilder<M> configureServer(Consumer<M> consumer) { |
||||
consumer.accept(this.builder); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient build() { |
||||
this.restClientBuilder.requestFactory(new MockMvcClientHttpRequestFactory(this.builder.build())); |
||||
return super.build(); |
||||
} |
||||
} |
||||
@ -0,0 +1,429 @@
@@ -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.servlet.client; |
||||
|
||||
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 <B extends Builder<B>> Builder<B> 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 @@
@@ -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.servlet.client; |
||||
|
||||
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 |
||||
* @param <B> the type of the builder |
||||
*/ |
||||
class DefaultRestTestClientBuilder<B extends RestTestClient.Builder<B>> implements RestTestClient.Builder<B> { |
||||
|
||||
protected final RestClient.Builder restClientBuilder; |
||||
|
||||
DefaultRestTestClientBuilder() { |
||||
this.restClientBuilder = RestClient.builder(); |
||||
} |
||||
|
||||
DefaultRestTestClientBuilder(RestClient.Builder restClientBuilder) { |
||||
this.restClientBuilder = restClientBuilder; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> apply(Consumer<RestTestClient.Builder<B>> builderConsumer) { |
||||
builderConsumer.accept(this); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> baseUrl(String baseUrl) { |
||||
this.restClientBuilder.baseUrl(baseUrl); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> defaultCookie(String cookieName, String... cookieValues) { |
||||
this.restClientBuilder.defaultCookie(cookieName, cookieValues); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) { |
||||
this.restClientBuilder.defaultCookies(cookiesConsumer); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> defaultHeader(String headerName, String... headerValues) { |
||||
this.restClientBuilder.defaultHeader(headerName, headerValues); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> defaultHeaders(Consumer<HttpHeaders> headersConsumer) { |
||||
this.restClientBuilder.defaultHeaders(headersConsumer); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient.Builder<B> uriBuilderFactory(UriBuilderFactory uriFactory) { |
||||
this.restClientBuilder.uriBuilderFactory(uriFactory); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RestTestClient build() { |
||||
return new DefaultRestTestClient(this.restClientBuilder); |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -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.servlet.client; |
||||
|
||||
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 @@
@@ -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.servlet.client; |
||||
|
||||
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 @@
@@ -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.servlet.client; |
||||
|
||||
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 @@
@@ -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.servlet.client; |
||||
|
||||
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,133 @@
@@ -0,0 +1,133 @@
|
||||
/* |
||||
* 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.servlet.client; |
||||
|
||||
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; |
||||
import org.springframework.http.HttpStatusCode; |
||||
import org.springframework.http.client.ClientHttpRequest; |
||||
import org.springframework.http.client.ClientHttpRequestFactory; |
||||
import org.springframework.http.client.ClientHttpResponse; |
||||
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; |
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; |
||||
|
||||
/** |
||||
* A {@link ClientHttpRequestFactory} for requests executed via {@link MockMvc}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @author Rob Worsnop |
||||
* @since 7.0 |
||||
*/ |
||||
class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory { |
||||
|
||||
private final MockMvc mockMvc; |
||||
|
||||
|
||||
MockMvcClientHttpRequestFactory(MockMvc mockMvc) { |
||||
Assert.notNull(mockMvc, "MockMvc must not be null"); |
||||
this.mockMvc = mockMvc; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { |
||||
return new MockClientHttpRequest(httpMethod, uri) { |
||||
@Override |
||||
public ClientHttpResponse executeInternal() { |
||||
return getClientHttpResponse(httpMethod, uri, getHeaders(), getBodyAsBytes()); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
private ClientHttpResponse getClientHttpResponse( |
||||
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(requestBuilder) |
||||
.andReturn() |
||||
.getResponse(); |
||||
|
||||
HttpStatusCode status = HttpStatusCode.valueOf(servletResponse.getStatus()); |
||||
byte[] body = servletResponse.getContentAsByteArray(); |
||||
if (body.length == 0) { |
||||
String error = servletResponse.getErrorMessage(); |
||||
if (StringUtils.hasLength(error)) { |
||||
// sendError message as default body
|
||||
body = error.getBytes(StandardCharsets.UTF_8); |
||||
} |
||||
} |
||||
|
||||
MockClientHttpResponse clientResponse = new MockClientHttpResponse(body, status); |
||||
clientResponse.getHeaders().putAll(getResponseHeaders(servletResponse)); |
||||
return clientResponse; |
||||
} |
||||
catch (Exception ex) { |
||||
byte[] body = ex.toString().getBytes(StandardCharsets.UTF_8); |
||||
return new MockClientHttpResponse(body, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
} |
||||
} |
||||
|
||||
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()) { |
||||
List<String> values = response.getHeaders(name); |
||||
for (String value : values) { |
||||
headers.add(name, value); |
||||
} |
||||
} |
||||
return headers; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,656 @@
@@ -0,0 +1,656 @@
|
||||
/* |
||||
* 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.servlet.client; |
||||
|
||||
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 org.hamcrest.Matcher; |
||||
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.http.client.ClientHttpRequestFactory; |
||||
import org.springframework.test.json.JsonComparator; |
||||
import org.springframework.test.json.JsonCompareMode; |
||||
import org.springframework.test.json.JsonComparison; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
import org.springframework.test.web.servlet.MockMvcBuilder; |
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; |
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; |
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder; |
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.client.RestClient; |
||||
import org.springframework.web.context.WebApplicationContext; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.util.UriBuilder; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
/** |
||||
* 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. |
||||
*/ |
||||
<B extends Builder<B>> Builder<B> 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 MockServerBuilder<StandaloneMockMvcBuilder> standaloneSetup(Object... controllers) { |
||||
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers); |
||||
return new DefaultMockServerBuilder<>(builder); |
||||
} |
||||
|
||||
/** |
||||
* 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 MockServerBuilder<RouterFunctionMockMvcBuilder> bindToRouterFunction(RouterFunction<?>... routerFunctions) { |
||||
RouterFunctionMockMvcBuilder builder = MockMvcBuilders.routerFunctions(routerFunctions); |
||||
return new DefaultMockServerBuilder<>(builder); |
||||
} |
||||
|
||||
/** |
||||
* 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 MockServerBuilder<DefaultMockMvcBuilder> bindToApplicationContext(WebApplicationContext context) { |
||||
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context); |
||||
return new DefaultMockServerBuilder<>(builder); |
||||
} |
||||
|
||||
/** |
||||
* Begin creating a {@link RestTestClient} by providing an already |
||||
* initialized {@link MockMvc} instance to use as the server. |
||||
*/ |
||||
static <B extends Builder<B>> Builder<B> 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 <B extends Builder<B>> Builder<B> bindToServer() { |
||||
return new DefaultRestTestClientBuilder<>(); |
||||
} |
||||
|
||||
/** |
||||
* A variant of {@link #bindToServer()} with a pre-configured request factory. |
||||
* @return chained API to customize client config |
||||
*/ |
||||
static <B extends Builder<B>> Builder<B> bindToServer(ClientHttpRequestFactory requestFactory) { |
||||
return new DefaultRestTestClientBuilder<>(RestClient.builder().requestFactory(requestFactory)); |
||||
} |
||||
|
||||
/** |
||||
* 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<B extends Builder<B>> { |
||||
/** |
||||
* 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<B> apply(Consumer<Builder<B>> builderConsumer); |
||||
|
||||
/** |
||||
* Add the given cookie to all requests. |
||||
* @param cookieName the cookie name |
||||
* @param cookieValues the cookie values |
||||
*/ |
||||
Builder<B> 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<B> 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<B> 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<B> defaultHeaders(Consumer<HttpHeaders> headersConsumer); |
||||
|
||||
/** |
||||
* Provide a pre-configured {@link UriBuilderFactory} instance as an |
||||
* alternative to and effectively overriding {@link #baseUrl(String)}. |
||||
*/ |
||||
Builder<B> 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<B> baseUrl(String baseUrl); |
||||
} |
||||
|
||||
interface MockServerBuilder<M extends MockMvcBuilder> extends Builder<MockServerBuilder<M>> { |
||||
MockServerBuilder<M> configureServer(Consumer<M> consumer); |
||||
} |
||||
} |
||||
@ -0,0 +1,250 @@
@@ -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.servlet.client; |
||||
|
||||
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.servlet.client.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 @@
@@ -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.servlet.client; |
||||
|
||||
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,147 @@
@@ -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.servlet.client; |
||||
|
||||
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 @@
@@ -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.servlet.client; |
||||
|
||||
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,218 @@
@@ -0,0 +1,218 @@
|
||||
/* |
||||
* 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.servlet.client; |
||||
|
||||
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.standaloneSetup(new MusicController()) |
||||
.configureServer(builder -> |
||||
builder.alwaysExpect(status().isOk()) |
||||
.alwaysExpect(content().contentType(MediaType.APPLICATION_JSON)) |
||||
) |
||||
.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,103 @@
@@ -0,0 +1,103 @@
|
||||
/* |
||||
* 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.servlet.client; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import jakarta.servlet.http.Cookie; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; |
||||
import org.springframework.web.bind.annotation.CookieValue; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* Tests that use a {@link RestTestClient} configured with a |
||||
* {@link MockMvcClientHttpRequestFactory} that is in turn configured with a |
||||
* {@link MockMvc} instance that uses a standalone controller |
||||
* |
||||
* @author Rob Worsnop |
||||
*/ |
||||
@ExtendWith(SpringExtension.class) |
||||
public class MockMvcClientHttpRequestFactoryTests { |
||||
|
||||
private RestTestClient client; |
||||
|
||||
@BeforeEach |
||||
public void setup() { |
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()).build(); |
||||
this.client = RestTestClient.bindTo(mockMvc).build(); |
||||
} |
||||
|
||||
@Test |
||||
public void withResult() { |
||||
client.get() |
||||
.uri("/foo") |
||||
.cookie("session", "12345") |
||||
.exchange() |
||||
.expectCookie().valueEquals("session", "12345") |
||||
.expectBody(String.class) |
||||
.isEqualTo("bar"); |
||||
} |
||||
|
||||
@Test |
||||
public void withError() { |
||||
client.get() |
||||
.uri("/error") |
||||
.exchange() |
||||
.expectStatus().isBadRequest() |
||||
.expectBody().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void withErrorAndBody() { |
||||
client.get().uri("/errorbody") |
||||
.exchange() |
||||
.expectStatus().isBadRequest() |
||||
.expectBody(String.class) |
||||
.isEqualTo("some really bad request"); |
||||
} |
||||
|
||||
@RestController |
||||
static class TestController { |
||||
|
||||
@GetMapping(value = "/foo") |
||||
public void foo(@CookieValue("session") String session, HttpServletResponse response) throws IOException { |
||||
response.getWriter().write("bar"); |
||||
response.addCookie(new Cookie("session", session)); |
||||
} |
||||
|
||||
@GetMapping(value = "/error") |
||||
public void handleError(HttpServletResponse response) throws Exception { |
||||
response.sendError(400); |
||||
} |
||||
|
||||
@GetMapping(value = "/errorbody") |
||||
public void handleErrorWithBody(HttpServletResponse response) throws Exception { |
||||
response.sendError(400); |
||||
response.getWriter().write("some really bad request"); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,266 @@
@@ -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.servlet.client; |
||||
|
||||
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 @@
@@ -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.servlet.client.samples; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.test.web.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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,156 @@
@@ -0,0 +1,156 @@
|
||||
/* |
||||
* 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.servlet.client.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.servlet.client.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.standaloneSetup(new PersonController()) |
||||
.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 @@
@@ -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.servlet.client.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.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.samples; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.test.web.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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.servlet.client.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 @@
@@ -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.servlet.client.samples.bind; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.test.web.servlet.client.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.standaloneSetup(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 @@
@@ -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.servlet.client.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.servlet.client.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())) |
||||
.configureServer(builder -> builder.addFilters(filter)) |
||||
.build(); |
||||
|
||||
client.get().uri("/") |
||||
.exchange() |
||||
.expectStatus().isOk() |
||||
.expectBody(String.class).isEqualTo("It works!"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@
@@ -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.servlet.client.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.servlet.client.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 @@
@@ -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.servlet.client.samples.bind; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.test.web.servlet.client.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!"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue