Browse Source
Includes: - FilterFunctions - HandlerFunctions - TestRestClient Fixes gh-36mvc-server
19 changed files with 4232 additions and 0 deletions
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!-- |
||||
~ Copyright 2023 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. |
||||
--> |
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<parent> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-gateway</artifactId> |
||||
<version>4.1.0-SNAPSHOT</version> |
||||
<relativePath>..</relativePath> <!-- lookup parent from repository --> |
||||
</parent> |
||||
<artifactId>spring-cloud-gateway-server-mvc</artifactId> |
||||
<packaging>jar</packaging> |
||||
<name>Spring Cloud Gateway Server MVC</name> |
||||
<description>Spring Cloud Gateway Server MVC</description> |
||||
<properties> |
||||
<main.basedir>${basedir}/..</main.basedir> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-web</artifactId> |
||||
<optional>true</optional> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-test</artifactId> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc; |
||||
|
||||
import java.net.URI; |
||||
|
||||
import org.springframework.web.servlet.function.HandlerFilterFunction; |
||||
import org.springframework.web.servlet.function.ServerRequest; |
||||
import org.springframework.web.servlet.function.ServerResponse; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
public class FilterFunctions { |
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> addRequestHeader(String name, |
||||
String... values) { |
||||
return (request, next) -> { |
||||
ServerRequest modified = new GatewayServerRequestBuilder(request).header(name, values).build(); |
||||
return next.handle(modified); |
||||
}; |
||||
} |
||||
|
||||
public static HandlerFilterFunction<ServerResponse, ServerResponse> prefixPath(String prefix) { |
||||
return (request, next) -> { |
||||
//TODO: template vars
|
||||
String newPath = prefix + request.uri().getRawPath(); |
||||
|
||||
URI prefixedUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(newPath).build().toUri(); |
||||
ServerRequest modified = new GatewayServerRequestBuilder(request).uri(prefixedUri).build(); |
||||
return next.handle(modified); |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,530 @@
@@ -0,0 +1,530 @@
|
||||
/* |
||||
* Copyright 2002-2023 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.cloud.gateway.server.mvc; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.lang.reflect.ParameterizedType; |
||||
import java.lang.reflect.Type; |
||||
import java.net.InetSocketAddress; |
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.security.Principal; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.OptionalLong; |
||||
import java.util.function.Consumer; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import jakarta.servlet.ReadListener; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.ServletInputStream; |
||||
import jakarta.servlet.http.Cookie; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpSession; |
||||
import jakarta.servlet.http.Part; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpInputMessage; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpRange; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.HttpMediaTypeNotSupportedException; |
||||
import org.springframework.web.servlet.function.RouterFunctions; |
||||
import org.springframework.web.servlet.function.ServerRequest; |
||||
import org.springframework.web.util.UriBuilder; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
/** |
||||
* Default {@link ServerRequest.Builder} implementation. |
||||
* |
||||
* @author Arjen Poutsma |
||||
* @since 5.2 |
||||
*/ |
||||
class GatewayServerRequestBuilder implements ServerRequest.Builder { |
||||
|
||||
private final HttpServletRequest servletRequest; |
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters; |
||||
|
||||
private HttpMethod method; |
||||
|
||||
private URI uri; |
||||
|
||||
private final HttpHeaders headers = new HttpHeaders(); |
||||
|
||||
private final MultiValueMap<String, Cookie> cookies = new LinkedMultiValueMap<>(); |
||||
|
||||
private final Map<String, Object> attributes = new LinkedHashMap<>(); |
||||
|
||||
private final MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); |
||||
|
||||
@Nullable |
||||
private InetSocketAddress remoteAddress; |
||||
|
||||
private byte[] body = new byte[0]; |
||||
|
||||
|
||||
public GatewayServerRequestBuilder(ServerRequest other) { |
||||
Assert.notNull(other, "ServerRequest must not be null"); |
||||
this.servletRequest = other.servletRequest(); |
||||
this.messageConverters = new ArrayList<>(other.messageConverters()); |
||||
this.method = other.method(); |
||||
this.uri = other.uri(); |
||||
headers(headers -> headers.addAll(other.headers().asHttpHeaders())); |
||||
cookies(cookies -> cookies.addAll(other.cookies())); |
||||
attributes(attributes -> attributes.putAll(other.attributes())); |
||||
params(params -> params.addAll(other.params())); |
||||
this.remoteAddress = other.remoteAddress().orElse(null); |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder method(HttpMethod method) { |
||||
Assert.notNull(method, "HttpMethod must not be null"); |
||||
this.method = method; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder uri(URI uri) { |
||||
Assert.notNull(uri, "URI must not be null"); |
||||
this.uri = uri; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder header(String headerName, String... headerValues) { |
||||
Assert.notNull(headerName, "Header name must not be null"); |
||||
for (String headerValue : headerValues) { |
||||
this.headers.add(headerName, headerValue); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder headers(Consumer<HttpHeaders> headersConsumer) { |
||||
Assert.notNull(headersConsumer, "Headers consumer must not be null"); |
||||
headersConsumer.accept(this.headers); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder cookie(String name, String... values) { |
||||
Assert.notNull(name, "Cookie name must not be null"); |
||||
for (String value : values) { |
||||
this.cookies.add(name, new Cookie(name, value)); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder cookies(Consumer<MultiValueMap<String, Cookie>> cookiesConsumer) { |
||||
Assert.notNull(cookiesConsumer, "Cookies consumer must not be null"); |
||||
cookiesConsumer.accept(this.cookies); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder body(byte[] body) { |
||||
Assert.notNull(body, "Body must not be null"); |
||||
this.body = body; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder body(String body) { |
||||
Assert.notNull(body, "Body must not be null"); |
||||
return body(body.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder attribute(String name, Object value) { |
||||
Assert.notNull(name, "Name must not be null"); |
||||
this.attributes.put(name, value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder attributes(Consumer<Map<String, Object>> attributesConsumer) { |
||||
Assert.notNull(attributesConsumer, "Attributes consumer must not be null"); |
||||
attributesConsumer.accept(this.attributes); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder param(String name, String... values) { |
||||
Assert.notNull(name, "Name must not be null"); |
||||
for (String value : values) { |
||||
this.params.add(name, value); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder params(Consumer<MultiValueMap<String, String>> paramsConsumer) { |
||||
Assert.notNull(paramsConsumer, "Parameters consumer must not be null"); |
||||
paramsConsumer.accept(this.params); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ServerRequest.Builder remoteAddress(@Nullable InetSocketAddress remoteAddress) { |
||||
this.remoteAddress = remoteAddress; |
||||
return this; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public ServerRequest build() { |
||||
return new BuiltServerRequest(this.servletRequest, this.method, this.uri, this.headers, this.cookies, |
||||
this.attributes, this.params, this.remoteAddress, this.body, this.messageConverters); |
||||
} |
||||
|
||||
static Class<?> bodyClass(Type type) { |
||||
if (type instanceof Class<?> clazz) { |
||||
return clazz; |
||||
} |
||||
if (type instanceof ParameterizedType parameterizedType && |
||||
parameterizedType.getRawType() instanceof Class<?> rawType) { |
||||
return rawType; |
||||
} |
||||
return Object.class; |
||||
} |
||||
|
||||
private static class BuiltServerRequest implements ServerRequest { |
||||
|
||||
private final HttpMethod method; |
||||
|
||||
private final URI uri; |
||||
|
||||
private final HttpHeaders headers; |
||||
|
||||
private final HttpServletRequest servletRequest; |
||||
|
||||
private final MultiValueMap<String, Cookie> cookies; |
||||
|
||||
private final Map<String, Object> attributes; |
||||
|
||||
private final byte[] body; |
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters; |
||||
|
||||
private final MultiValueMap<String, String> params; |
||||
|
||||
@Nullable |
||||
private final InetSocketAddress remoteAddress; |
||||
|
||||
public BuiltServerRequest(HttpServletRequest servletRequest, HttpMethod method, URI uri, |
||||
HttpHeaders headers, MultiValueMap<String, Cookie> cookies, |
||||
Map<String, Object> attributes, MultiValueMap<String, String> params, |
||||
@Nullable InetSocketAddress remoteAddress, byte[] body, List<HttpMessageConverter<?>> messageConverters) { |
||||
|
||||
this.servletRequest = servletRequest; |
||||
this.method = method; |
||||
this.uri = uri; |
||||
this.headers = new HttpHeaders(headers); |
||||
this.cookies = new LinkedMultiValueMap<>(cookies); |
||||
this.attributes = new LinkedHashMap<>(attributes); |
||||
this.params = new LinkedMultiValueMap<>(params); |
||||
this.remoteAddress = remoteAddress; |
||||
this.body = body; |
||||
this.messageConverters = messageConverters; |
||||
} |
||||
|
||||
@Override |
||||
public HttpMethod method() { |
||||
return this.method; |
||||
} |
||||
|
||||
@Override |
||||
@Deprecated |
||||
public String methodName() { |
||||
return this.method.name(); |
||||
} |
||||
|
||||
@Override |
||||
public MultiValueMap<String, Part> multipartData() throws IOException, ServletException { |
||||
return servletRequest().getParts().stream() |
||||
.collect(Collectors.groupingBy(Part::getName, |
||||
LinkedMultiValueMap::new, |
||||
Collectors.toList())); |
||||
} |
||||
|
||||
@Override |
||||
public URI uri() { |
||||
return this.uri; |
||||
} |
||||
|
||||
@Override |
||||
public UriBuilder uriBuilder() { |
||||
return UriComponentsBuilder.fromUri(this.uri); |
||||
} |
||||
|
||||
@Override |
||||
public Headers headers() { |
||||
return new DefaultRequestHeaders(this.headers); |
||||
} |
||||
|
||||
@Override |
||||
public MultiValueMap<String, Cookie> cookies() { |
||||
return this.cookies; |
||||
} |
||||
|
||||
@Override |
||||
public Optional<InetSocketAddress> remoteAddress() { |
||||
return Optional.ofNullable(this.remoteAddress); |
||||
} |
||||
|
||||
@Override |
||||
public List<HttpMessageConverter<?>> messageConverters() { |
||||
return this.messageConverters; |
||||
} |
||||
|
||||
@Override |
||||
public <T> T body(Class<T> bodyType) throws IOException, ServletException { |
||||
return bodyInternal(bodyType, bodyType); |
||||
} |
||||
|
||||
@Override |
||||
public <T> T body(ParameterizedTypeReference<T> bodyType) throws IOException, ServletException { |
||||
Type type = bodyType.getType(); |
||||
return bodyInternal(type, bodyClass(type)); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T> T bodyInternal(Type bodyType, Class<?> bodyClass) throws ServletException, IOException { |
||||
HttpInputMessage inputMessage = new BuiltInputMessage(); |
||||
MediaType contentType = headers().contentType().orElse(MediaType.APPLICATION_OCTET_STREAM); |
||||
|
||||
for (HttpMessageConverter<?> messageConverter : this.messageConverters) { |
||||
if (messageConverter instanceof GenericHttpMessageConverter<?> genericMessageConverter) { |
||||
if (genericMessageConverter.canRead(bodyType, bodyClass, contentType)) { |
||||
return (T) genericMessageConverter.read(bodyType, bodyClass, inputMessage); |
||||
} |
||||
} |
||||
if (messageConverter.canRead(bodyClass, contentType)) { |
||||
HttpMessageConverter<T> theConverter = |
||||
(HttpMessageConverter<T>) messageConverter; |
||||
Class<? extends T> clazz = (Class<? extends T>) bodyClass; |
||||
return theConverter.read(clazz, inputMessage); |
||||
} |
||||
} |
||||
throw new HttpMediaTypeNotSupportedException(contentType, Collections.emptyList(), method()); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, Object> attributes() { |
||||
return this.attributes; |
||||
} |
||||
|
||||
@Override |
||||
public MultiValueMap<String, String> params() { |
||||
return this.params; |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, String> pathVariables() { |
||||
@SuppressWarnings("unchecked") |
||||
Map<String, String> pathVariables = (Map<String, String>) attributes() |
||||
.get(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE); |
||||
if (pathVariables != null) { |
||||
return pathVariables; |
||||
} |
||||
else { |
||||
return Collections.emptyMap(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public HttpSession session() { |
||||
return this.servletRequest.getSession(); |
||||
} |
||||
|
||||
@Override |
||||
public Optional<Principal> principal() { |
||||
return Optional.ofNullable(this.servletRequest.getUserPrincipal()); |
||||
} |
||||
|
||||
@Override |
||||
public HttpServletRequest servletRequest() { |
||||
return this.servletRequest; |
||||
} |
||||
|
||||
|
||||
private class BuiltInputMessage implements HttpInputMessage { |
||||
|
||||
@Override |
||||
public InputStream getBody() throws IOException { |
||||
return new BodyInputStream(body); |
||||
} |
||||
|
||||
@Override |
||||
public HttpHeaders getHeaders() { |
||||
return headers; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class BodyInputStream extends ServletInputStream { |
||||
|
||||
private final InputStream delegate; |
||||
|
||||
public BodyInputStream(byte[] body) { |
||||
this.delegate = new ByteArrayInputStream(body); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isFinished() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReady() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void setReadListener(ReadListener readListener) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public int read() throws IOException { |
||||
return this.delegate.read(); |
||||
} |
||||
|
||||
@Override |
||||
public int read(byte[] b, int off, int len) throws IOException { |
||||
return this.delegate.read(b, off, len); |
||||
} |
||||
|
||||
@Override |
||||
public int read(byte[] b) throws IOException { |
||||
return this.delegate.read(b); |
||||
} |
||||
|
||||
@Override |
||||
public long skip(long n) throws IOException { |
||||
return this.delegate.skip(n); |
||||
} |
||||
|
||||
@Override |
||||
public int available() throws IOException { |
||||
return this.delegate.available(); |
||||
} |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
this.delegate.close(); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void mark(int readlimit) { |
||||
this.delegate.mark(readlimit); |
||||
} |
||||
|
||||
@Override |
||||
public synchronized void reset() throws IOException { |
||||
this.delegate.reset(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean markSupported() { |
||||
return this.delegate.markSupported(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Default implementation of {@link ServerRequest.Headers}. |
||||
*/ |
||||
static class DefaultRequestHeaders implements ServerRequest.Headers { |
||||
|
||||
private final HttpHeaders httpHeaders; |
||||
|
||||
public DefaultRequestHeaders(HttpHeaders httpHeaders) { |
||||
this.httpHeaders = HttpHeaders.readOnlyHttpHeaders(httpHeaders); |
||||
} |
||||
|
||||
@Override |
||||
public List<MediaType> accept() { |
||||
return this.httpHeaders.getAccept(); |
||||
} |
||||
|
||||
@Override |
||||
public List<Charset> acceptCharset() { |
||||
return this.httpHeaders.getAcceptCharset(); |
||||
} |
||||
|
||||
@Override |
||||
public List<Locale.LanguageRange> acceptLanguage() { |
||||
return this.httpHeaders.getAcceptLanguage(); |
||||
} |
||||
|
||||
@Override |
||||
public OptionalLong contentLength() { |
||||
long value = this.httpHeaders.getContentLength(); |
||||
return (value != -1 ? OptionalLong.of(value) : OptionalLong.empty()); |
||||
} |
||||
|
||||
@Override |
||||
public Optional<MediaType> contentType() { |
||||
return Optional.ofNullable(this.httpHeaders.getContentType()); |
||||
} |
||||
|
||||
@Override |
||||
public InetSocketAddress host() { |
||||
return this.httpHeaders.getHost(); |
||||
} |
||||
|
||||
@Override |
||||
public List<HttpRange> range() { |
||||
return this.httpHeaders.getRange(); |
||||
} |
||||
|
||||
@Override |
||||
public List<String> header(String headerName) { |
||||
List<String> headerValues = this.httpHeaders.get(headerName); |
||||
return (headerValues != null ? headerValues : Collections.emptyList()); |
||||
} |
||||
|
||||
@Override |
||||
public HttpHeaders asHttpHeaders() { |
||||
return this.httpHeaders; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.httpHeaders.toString(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc; |
||||
|
||||
import java.net.URI; |
||||
import java.util.Optional; |
||||
import java.util.function.Function; |
||||
|
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.RequestEntity; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.web.client.RestTemplate; |
||||
import org.springframework.web.servlet.DispatcherServlet; |
||||
import org.springframework.web.servlet.function.HandlerFunction; |
||||
import org.springframework.web.servlet.function.ServerRequest; |
||||
import org.springframework.web.servlet.function.ServerResponse; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
public class HandlerFunctions { |
||||
public static HandlerFunction<ServerResponse> http(String uri) { |
||||
return http(URI.create(uri)); |
||||
} |
||||
|
||||
public static HandlerFunction<ServerResponse> http(URI uri) { |
||||
return new ProxyHandlerFunction(req -> uri); |
||||
} |
||||
|
||||
public static HandlerFunction<ServerResponse> http(URIResolver uriResolver) { |
||||
return new ProxyHandlerFunction(uriResolver); |
||||
} |
||||
|
||||
interface URIResolver extends Function<ServerRequest, URI> { |
||||
|
||||
} |
||||
|
||||
static class ProxyHandlerFunction implements HandlerFunction<ServerResponse> { |
||||
|
||||
private RestTemplate restTemplate; |
||||
|
||||
private final URIResolver uriResolver; |
||||
|
||||
ProxyHandlerFunction(URIResolver uriResolver) { |
||||
this.uriResolver = uriResolver; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public ServerResponse handle(ServerRequest request) { |
||||
RestTemplate restTemplate = getRestTemplate(request); |
||||
if (restTemplate != null) { |
||||
URI uri = uriResolver.apply(request); |
||||
boolean encoded = containsEncodedQuery(request.uri()); |
||||
URI url = UriComponentsBuilder.fromUri(request.uri()) |
||||
// .uri(routeUri)
|
||||
.scheme(uri.getScheme()).host(uri.getHost()).port(uri.getPort()).build(encoded).toUri(); |
||||
|
||||
RequestEntity<Void> entity = RequestEntity.method(request.method(), url) |
||||
.headers(request.headers().asHttpHeaders()) |
||||
.build(); |
||||
ResponseEntity<Object> response = restTemplate.exchange(entity, Object.class); |
||||
return ServerResponse.status(response.getStatusCode()) |
||||
.headers(httpHeaders -> httpHeaders.putAll(response.getHeaders())) |
||||
.body(response.getBody()); |
||||
} |
||||
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); |
||||
} |
||||
|
||||
private RestTemplate getRestTemplate(ServerRequest request) { |
||||
if (this.restTemplate == null) { |
||||
this.restTemplate = getBean(request, RestTemplate.class); |
||||
} |
||||
return this.restTemplate; |
||||
} |
||||
|
||||
} |
||||
|
||||
static ApplicationContext getApplicationContext(ServerRequest request) { |
||||
Optional<Object> contextAttr = request.attribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); |
||||
if (contextAttr.isEmpty()) { |
||||
throw new IllegalStateException("No Application Context in request attributes"); |
||||
} |
||||
return (ApplicationContext) contextAttr.get(); |
||||
} |
||||
|
||||
static <T> T getBean(ServerRequest request, Class<T> type) { |
||||
return getApplicationContext(request).getBean(type); |
||||
} |
||||
|
||||
static boolean containsEncodedQuery(URI uri) { |
||||
boolean encoded = (uri.getRawQuery() != null && uri.getRawQuery().contains("%")) |
||||
|| (uri.getRawPath() != null && uri.getRawPath().contains("%")); |
||||
|
||||
// Verify if it is really fully encoded. Treat partial encoded as unencoded.
|
||||
if (encoded) { |
||||
try { |
||||
UriComponentsBuilder.fromUri(uri).build(true); |
||||
return true; |
||||
} |
||||
catch (IllegalArgumentException ignored) { |
||||
/*if (log.isTraceEnabled()) { |
||||
log.trace("Error in containsEncodedParts", ignored); |
||||
}*/ |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
return encoded; |
||||
} |
||||
} |
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc; |
||||
|
||||
import java.net.URI; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; |
||||
import org.springframework.boot.test.web.client.TestRestTemplate; |
||||
import org.springframework.boot.test.web.server.LocalServerPort; |
||||
import org.springframework.boot.web.client.RestTemplateBuilder; |
||||
import org.springframework.cloud.gateway.server.mvc.test.DefaultTestRestClient; |
||||
import org.springframework.cloud.gateway.server.mvc.test.HttpBinCompatibleController; |
||||
import org.springframework.cloud.gateway.server.mvc.test.LocalHostUriBuilderFactory; |
||||
import org.springframework.cloud.gateway.server.mvc.test.TestRestClient; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.web.client.RestTemplate; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.servlet.function.ServerRequest; |
||||
import org.springframework.web.servlet.function.ServerResponse; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.springframework.cloud.gateway.server.mvc.FilterFunctions.addRequestHeader; |
||||
import static org.springframework.cloud.gateway.server.mvc.FilterFunctions.prefixPath; |
||||
import static org.springframework.cloud.gateway.server.mvc.HandlerFunctions.http; |
||||
import static org.springframework.web.servlet.function.RequestPredicates.GET; |
||||
import static org.springframework.web.servlet.function.RouterFunctions.route; |
||||
|
||||
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT) |
||||
public class ServerMvcIntegrationTests { |
||||
|
||||
@LocalServerPort |
||||
int port; |
||||
|
||||
@Autowired |
||||
TestRestTemplate restTemplate; |
||||
|
||||
@Autowired |
||||
TestRestClient restClient; |
||||
|
||||
@Test |
||||
public void nonGatewayRouterFunctionWorks() { |
||||
restClient.get().uri("/hello").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello"); |
||||
} |
||||
|
||||
@Test |
||||
public void getGatewayRouterFunctionWorks() { |
||||
ResponseEntity<Map> response = restTemplate.getForEntity("/get", Map.class); |
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); |
||||
Map<String, Object> map0 = response.getBody(); |
||||
assertThat(map0).isNotEmpty().containsKey("headers"); |
||||
Map<String, Object> headers0 = (Map<String, Object>) map0.get("headers"); |
||||
// TODO: assert headers case insensitive
|
||||
assertThat(headers0).containsEntry("x-foo", "Bar"); |
||||
|
||||
restClient.get().uri("/get").exchange().expectStatus().isOk().expectBody(Map.class).consumeWith(res -> { |
||||
Map<String, Object> map = res.getResponseBody(); |
||||
assertThat(map).isNotEmpty().containsKey("headers"); |
||||
Map<String, Object> headers = (Map<String, Object>) map.get("headers"); |
||||
assertThat(headers).containsEntry("x-foo", "Bar"); |
||||
}); |
||||
} |
||||
|
||||
@SpringBootConfiguration |
||||
@EnableAutoConfiguration |
||||
protected static class TestConfiguration { |
||||
|
||||
@Bean |
||||
public DefaultTestRestClient testRestClient(TestRestTemplate testRestTemplate, Environment env) { |
||||
return new DefaultTestRestClient(testRestTemplate, new LocalHostUriBuilderFactory(env), result -> {}); |
||||
} |
||||
|
||||
@Bean |
||||
public HttpBinCompatibleController httpBinCompatibleController() { |
||||
return new HttpBinCompatibleController(); |
||||
} |
||||
|
||||
@Bean |
||||
public RestTemplate gatewayRestTemplate(RestTemplateBuilder builder) { |
||||
return builder.build(); |
||||
} |
||||
|
||||
@Bean |
||||
TestHandler testHandler() { |
||||
return new TestHandler(); |
||||
} |
||||
|
||||
@Bean |
||||
public RouterFunction<ServerResponse> nonGatewayRouterFunctions(TestHandler testHandler) { |
||||
return route(GET("/hello"), testHandler::hello); |
||||
} |
||||
|
||||
@Bean |
||||
public RouterFunction<ServerResponse> gatewayRouterFunctions() { |
||||
return route(GET("/get"), http(new LocalServerPortUriResolver())) |
||||
.filter(addRequestHeader("X-Foo", "Bar")) |
||||
.filter(prefixPath("/httpbin")); |
||||
} |
||||
} |
||||
|
||||
protected static class TestHandler { |
||||
|
||||
public ServerResponse hello(ServerRequest request) { |
||||
return ServerResponse.ok().body("Hello"); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class LocalServerPortUriResolver implements HandlerFunctions.URIResolver { |
||||
@Override |
||||
public URI apply(ServerRequest request) { |
||||
ApplicationContext context = HandlerFunctions.getApplicationContext(request); |
||||
Integer port = context.getEnvironment().getProperty("local.server.port", Integer.class); |
||||
return URI.create("http://localhost:" + port); } |
||||
} |
||||
} |
@ -0,0 +1,224 @@
@@ -0,0 +1,224 @@
|
||||
/* |
||||
* Copyright 2002-2021 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.Objects; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.hamcrest.Matcher; |
||||
import org.hamcrest.MatcherAssert; |
||||
|
||||
import org.springframework.http.ResponseCookie; |
||||
import org.springframework.test.util.AssertionErrors; |
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat; |
||||
|
||||
/** |
||||
* Assertions on cookies of the response. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.3 |
||||
*/ |
||||
public class CookieAssertions { |
||||
|
||||
private final ExchangeResult exchangeResult; |
||||
|
||||
private final TestRestClient.ResponseSpec responseSpec; |
||||
|
||||
|
||||
public CookieAssertions(ExchangeResult exchangeResult, TestRestClient.ResponseSpec responseSpec) { |
||||
this.exchangeResult = exchangeResult; |
||||
this.responseSpec = responseSpec; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Expect a header with the given name to match the specified values. |
||||
*/ |
||||
public TestRestClient.ResponseSpec valueEquals(String name, String value) { |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name); |
||||
AssertionErrors.assertEquals(message, value, getCookie(name).getValue()); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert the first value of the response cookie with a Hamcrest {@link Matcher}. |
||||
*/ |
||||
public TestRestClient.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. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec exists(String name) { |
||||
getCookie(name); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Expect that the cookie with the given name is not present. |
||||
*/ |
||||
public TestRestClient.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(() -> AssertionErrors.fail(message)); |
||||
} |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's maxAge attribute. |
||||
*/ |
||||
public TestRestClient.ResponseSpec maxAge(String name, Duration expected) { |
||||
Duration maxAge = getCookie(name).getMaxAge(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " maxAge"; |
||||
AssertionErrors.assertEquals(message, expected, maxAge); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's maxAge attribute with a Hamcrest {@link Matcher}. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec path(String name, String expected) { |
||||
String path = getCookie(name).getPath(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " path"; |
||||
AssertionErrors.assertEquals(message, expected, path); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's path attribute with a Hamcrest {@link Matcher}. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec domain(String name, String expected) { |
||||
String path = getCookie(name).getDomain(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " domain"; |
||||
AssertionErrors.assertEquals(message, expected, path); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's domain attribute with a Hamcrest {@link Matcher}. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec secure(String name, boolean expected) { |
||||
boolean isSecure = getCookie(name).isSecure(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " secure"; |
||||
AssertionErrors.assertEquals(message, expected, isSecure); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's httpOnly attribute. |
||||
*/ |
||||
public TestRestClient.ResponseSpec httpOnly(String name, boolean expected) { |
||||
boolean isHttpOnly = getCookie(name).isHttpOnly(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " httpOnly"; |
||||
AssertionErrors.assertEquals(message, expected, isHttpOnly); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Assert a cookie's sameSite attribute. |
||||
*/ |
||||
public TestRestClient.ResponseSpec sameSite(String name, String expected) { |
||||
String sameSite = getCookie(name).getSameSite(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name) + " sameSite"; |
||||
AssertionErrors.assertEquals(message, expected, sameSite); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
|
||||
private ResponseCookie getCookie(String name) { |
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); |
||||
if (cookie == null) { |
||||
this.exchangeResult.assertWithDiagnostics(() -> |
||||
AssertionErrors.fail("No cookie with name '" + name + "'")); |
||||
} |
||||
return Objects.requireNonNull(cookie); |
||||
} |
||||
|
||||
private String getMessage(String cookie) { |
||||
return "Response cookie '" + cookie + "'"; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,566 @@
@@ -0,0 +1,566 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.time.ZonedDateTime; |
||||
import java.util.Arrays; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
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.springframework.boot.test.web.client.TestRestTemplate; |
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.convert.support.DefaultConversionService; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.RequestEntity; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.http.client.reactive.ClientHttpRequest; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.mock.http.client.MockClientHttpResponse; |
||||
import org.springframework.test.util.AssertionErrors; |
||||
import org.springframework.test.util.ExceptionCollector; |
||||
import org.springframework.test.util.JsonExpectationsHelper; |
||||
import org.springframework.test.util.XmlExpectationsHelper; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MimeType; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.client.HttpMessageConverterExtractor; |
||||
import org.springframework.web.util.UriBuilder; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
public class DefaultTestRestClient implements TestRestClient { |
||||
|
||||
private final TestRestTemplate testRestTemplate; |
||||
|
||||
private final UriBuilderFactory uriBuilderFactory; |
||||
|
||||
private final Consumer<EntityExchangeResult<?>> entityResultConsumer; |
||||
|
||||
|
||||
private final AtomicLong requestIndex = new AtomicLong(); |
||||
|
||||
public DefaultTestRestClient(TestRestTemplate testRestTemplate, UriBuilderFactory uriBuilderFactory, Consumer<EntityExchangeResult<?>> entityResultConsumer) { |
||||
this.uriBuilderFactory = uriBuilderFactory; |
||||
this.testRestTemplate = testRestTemplate; |
||||
this.entityResultConsumer = entityResultConsumer; |
||||
} |
||||
|
||||
@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); |
||||
} |
||||
|
||||
private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) { |
||||
return new DefaultRequestBodyUriSpec(httpMethod); |
||||
} |
||||
|
||||
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { |
||||
private final HttpMethod httpMethod; |
||||
|
||||
@Nullable |
||||
private URI uri; |
||||
|
||||
private final HttpHeaders headers; |
||||
|
||||
@Nullable |
||||
private MultiValueMap<String, String> cookies; |
||||
|
||||
private final Map<String, Object> attributes = new LinkedHashMap<>(4); |
||||
|
||||
@Nullable |
||||
private Consumer<ClientHttpRequest> httpRequestConsumer; |
||||
|
||||
@Nullable |
||||
private String uriTemplate; |
||||
|
||||
private final String requestId; |
||||
private Object body; |
||||
|
||||
DefaultRequestBodyUriSpec(HttpMethod httpMethod) { |
||||
this.httpMethod = httpMethod; |
||||
this.requestId = String.valueOf(requestIndex.incrementAndGet()); |
||||
this.headers = new HttpHeaders(); |
||||
this.headers.add(TESTRESTCLIENT_REQUEST_ID, this.requestId); |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) { |
||||
this.uriTemplate = uriTemplate; |
||||
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) { |
||||
this.uriTemplate = uriTemplate; |
||||
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) { |
||||
this.uriTemplate = null; |
||||
return uri(uriFunction.apply(uriBuilderFactory.builder())); |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec uri(URI uri) { |
||||
this.uri = uri; |
||||
return this; |
||||
} |
||||
|
||||
private HttpHeaders getHeaders() { |
||||
return this.headers; |
||||
} |
||||
|
||||
private MultiValueMap<String, String> getCookies() { |
||||
if (this.cookies == null) { |
||||
this.cookies = new LinkedMultiValueMap<>(3); |
||||
} |
||||
return this.cookies; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec header(String headerName, String... headerValues) { |
||||
for (String headerValue : headerValues) { |
||||
getHeaders().add(headerName, headerValue); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec headers(Consumer<HttpHeaders> headersConsumer) { |
||||
headersConsumer.accept(getHeaders()); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec attribute(String name, Object value) { |
||||
this.attributes.put(name, value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) { |
||||
attributesConsumer.accept(this.attributes); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec accept(MediaType... acceptableMediaTypes) { |
||||
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec acceptCharset(Charset... acceptableCharsets) { |
||||
getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec contentType(MediaType contentType) { |
||||
getHeaders().setContentType(contentType); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec contentLength(long contentLength) { |
||||
getHeaders().setContentLength(contentLength); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec cookie(String name, String value) { |
||||
getCookies().add(name, value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer) { |
||||
cookiesConsumer.accept(getCookies()); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) { |
||||
getHeaders().setIfModifiedSince(ifModifiedSince); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestBodySpec ifNoneMatch(String... ifNoneMatches) { |
||||
getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestHeadersSpec<?> bodyValue(Object body) { |
||||
this.body = body; |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestHeadersSpec<?> body(Object producer, Class<?> elementClass) { |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementTypeRef) { |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public ResponseSpec exchange() { |
||||
RequestEntity<Object> request = RequestEntity.method(httpMethod, uri).headers(getHeaders()).body(body); |
||||
ResponseEntity<byte[]> response = testRestTemplate.exchange(request, byte[].class); |
||||
ExchangeResult exchangeResult = new ExchangeResult(request, response); |
||||
return new DefaultResponseSpec(exchangeResult, response, DefaultTestRestClient.this.entityResultConsumer); |
||||
} |
||||
|
||||
} |
||||
|
||||
private class DefaultResponseSpec implements ResponseSpec { |
||||
|
||||
private final ExchangeResult exchangeResult; |
||||
|
||||
private final ResponseEntity<byte[]> responseEntity; |
||||
private final Consumer<EntityExchangeResult<?>> entityResultConsumer; |
||||
|
||||
DefaultResponseSpec(ExchangeResult exchangeResult, |
||||
ResponseEntity<byte[]> responseEntity, Consumer<EntityExchangeResult<?>> entityResultConsumer) { |
||||
this.exchangeResult = exchangeResult; |
||||
this.responseEntity = responseEntity; |
||||
this.entityResultConsumer = entityResultConsumer; |
||||
} |
||||
|
||||
@Override |
||||
public StatusAssertions expectStatus() { |
||||
return new StatusAssertions(this.exchangeResult, this); |
||||
} |
||||
|
||||
@Override |
||||
public HeaderAssertions expectHeader() { |
||||
return new HeaderAssertions(this.exchangeResult, this); |
||||
} |
||||
|
||||
@Override |
||||
public CookieAssertions expectCookie() { |
||||
return new CookieAssertions(this.exchangeResult, this); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public <B> BodySpec<B, ?> expectBody(Class<B> bodyType) { |
||||
HttpMessageConverterExtractor<B> httpMessageConverterExtractor = new HttpMessageConverterExtractor<>(bodyType, DefaultTestRestClient.this.testRestTemplate.getRestTemplate().getMessageConverters()); |
||||
try { |
||||
MockClientHttpResponse mockResponse = new MockClientHttpResponse(this.responseEntity.getBody(), this.responseEntity.getStatusCode()); |
||||
mockResponse.getHeaders().putAll(this.responseEntity.getHeaders()); |
||||
B body = httpMessageConverterExtractor.extractData(mockResponse); |
||||
EntityExchangeResult<B> entityResult = initEntityExchangeResult(body); |
||||
return new DefaultBodySpec<>(entityResult); |
||||
} catch (IOException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType) { |
||||
throw new UnsupportedOperationException("expectBody(ParameterizedTypeReference<B> bodyType)"); |
||||
/*B body = DefaultConversionService.getSharedInstance().convert(this.responseEntity.getBody(), bodyType); |
||||
EntityExchangeResult<B> entityResult = initEntityExchangeResult(body); |
||||
return new DefaultBodySpec<>(entityResult);*/ |
||||
} |
||||
|
||||
private <B> EntityExchangeResult<B> initEntityExchangeResult(@Nullable B body) { |
||||
EntityExchangeResult<B> result = new EntityExchangeResult<>(this.exchangeResult, body); |
||||
result.assertWithDiagnostics(() -> this.entityResultConsumer.accept(result)); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public <E> ListBodySpec<E> expectBodyList(Class<E> elementType) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public <E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public BodyContentSpec expectBody() { |
||||
return new DefaultBodyContentSpec(null); |
||||
} |
||||
|
||||
/*@Override |
||||
public <T> FluxExchangeResult<T> returnResult(Class<T> elementClass) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public <T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef) { |
||||
return null; |
||||
}*/ |
||||
|
||||
@Override |
||||
public TestRestClient.ResponseSpec expectAll(TestRestClient.ResponseSpec.ResponseSpecConsumer... consumers) { |
||||
ExceptionCollector exceptionCollector = new ExceptionCollector(); |
||||
for (TestRestClient.ResponseSpec.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.
|
||||
AssertionError assertionError = new AssertionError(ex.getMessage()); |
||||
assertionError.initCause(ex); |
||||
throw assertionError; |
||||
} |
||||
return this; |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class DefaultBodySpec<B, S extends TestRestClient.BodySpec<B, S>> implements TestRestClient.BodySpec<B, S> { |
||||
|
||||
private final EntityExchangeResult<B> result; |
||||
|
||||
DefaultBodySpec(EntityExchangeResult<B> result) { |
||||
this.result = result; |
||||
} |
||||
|
||||
protected EntityExchangeResult<B> getResult() { |
||||
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 |
||||
public <T extends S> T value(Matcher<? super B> matcher) { |
||||
this.result.assertWithDiagnostics(() -> MatcherAssert.assertThat(this.result.getResponseBody(), matcher)); |
||||
return self(); |
||||
} |
||||
|
||||
@Override |
||||
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; |
||||
} |
||||
|
||||
@Override |
||||
public EntityExchangeResult<B> returnResult() { |
||||
return this.result; |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class DefaultListBodySpec<E> extends DefaultTestRestClient.DefaultBodySpec<List<E>, TestRestClient.ListBodySpec<E>> |
||||
implements TestRestClient.ListBodySpec<E> { |
||||
|
||||
DefaultListBodySpec(EntityExchangeResult<List<E>> result) { |
||||
super(result); |
||||
} |
||||
|
||||
@Override |
||||
public TestRestClient.ListBodySpec<E> hasSize(int size) { |
||||
List<E> actual = getResult().getResponseBody(); |
||||
String message = "Response body does not contain " + size + " elements"; |
||||
getResult().assertWithDiagnostics(() -> |
||||
AssertionErrors.assertEquals(message, size, (actual != null ? actual.size() : 0))); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TestRestClient.ListBodySpec<E> contains(E... elements) { |
||||
List<E> expected = Arrays.asList(elements); |
||||
List<E> actual = getResult().getResponseBody(); |
||||
String message = "Response body does not contain " + expected; |
||||
getResult().assertWithDiagnostics(() -> |
||||
AssertionErrors.assertTrue(message, (actual != null && actual.containsAll(expected)))); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TestRestClient.ListBodySpec<E> doesNotContain(E... elements) { |
||||
List<E> expected = Arrays.asList(elements); |
||||
List<E> actual = getResult().getResponseBody(); |
||||
String message = "Response body should not have contained " + expected; |
||||
getResult().assertWithDiagnostics(() -> |
||||
AssertionErrors.assertTrue(message, (actual == null || !actual.containsAll(expected)))); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public EntityExchangeResult<List<E>> returnResult() { |
||||
return getResult(); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class DefaultBodyContentSpec implements TestRestClient.BodyContentSpec { |
||||
|
||||
private final EntityExchangeResult<byte[]> result; |
||||
|
||||
private final boolean isEmpty; |
||||
|
||||
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) { |
||||
this.result = result; |
||||
this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0); |
||||
} |
||||
|
||||
@Override |
||||
public EntityExchangeResult<Void> isEmpty() { |
||||
this.result.assertWithDiagnostics(() -> |
||||
AssertionErrors.assertTrue("Expected empty body", this.isEmpty)); |
||||
return new EntityExchangeResult<>(this.result, null); |
||||
} |
||||
|
||||
@Override |
||||
public TestRestClient.BodyContentSpec json(String json, boolean strict) { |
||||
this.result.assertWithDiagnostics(() -> { |
||||
try { |
||||
new JsonExpectationsHelper().assertJsonEqual(json, getBodyAsString(), strict); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new AssertionError("JSON parsing error", ex); |
||||
} |
||||
}); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public TestRestClient.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, Object... args) { |
||||
return new JsonPathAssertions(this, getBodyAsString(), expression, args); |
||||
} |
||||
|
||||
@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 TestRestClient.BodyContentSpec consumeWith(Consumer<EntityExchangeResult<byte[]>> consumer) { |
||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public EntityExchangeResult<byte[]> returnResult() { |
||||
return this.result; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright 2002-2017 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* {@code ExchangeResult} sub-class that exposes the response body fully |
||||
* extracted to a representation of type {@code <T>}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
* @param <T> the response body type |
||||
* @see FluxExchangeResult |
||||
*/ |
||||
public class EntityExchangeResult<T> extends ExchangeResult { |
||||
|
||||
@Nullable |
||||
private final T body; |
||||
|
||||
|
||||
EntityExchangeResult(ExchangeResult result, @Nullable T body) { |
||||
super(result); |
||||
this.body = body; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the entity extracted from the response body. |
||||
*/ |
||||
@Nullable |
||||
public T getResponseBody() { |
||||
return this.body; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,309 @@
@@ -0,0 +1,309 @@
|
||||
/* |
||||
* Copyright 2002-2022 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.time.Duration; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.HttpStatusCode; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.RequestEntity; |
||||
import org.springframework.http.ResponseCookie; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.http.client.reactive.ClientHttpRequest; |
||||
import org.springframework.http.client.reactive.ClientHttpResponse; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* Container for request and response details for exchanges performed through |
||||
* {@link WebTestClient}. |
||||
* |
||||
* <p>Note that a decoded response body is not exposed at this level since the |
||||
* body may not have been decoded and consumed yet. Subtypes |
||||
* {@link EntityExchangeResult} and {@link FluxExchangeResult} provide access |
||||
* to a decoded response entity and a decoded (but not consumed) response body |
||||
* respectively. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @author Sam Brannen |
||||
* @since 5.0 |
||||
* @see EntityExchangeResult |
||||
* @see FluxExchangeResult |
||||
*/ |
||||
public class ExchangeResult { |
||||
|
||||
private static final Log logger = LogFactory.getLog(ExchangeResult.class); |
||||
|
||||
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = List.of( |
||||
MediaType.parseMediaType("application/*+json"), MediaType.APPLICATION_XML, |
||||
MediaType.parseMediaType("text/*"), MediaType.APPLICATION_FORM_URLENCODED); |
||||
|
||||
|
||||
private final RequestEntity request; |
||||
|
||||
private final ResponseEntity response; |
||||
|
||||
private final byte[] requestBody; |
||||
|
||||
private final byte[] responseBody; |
||||
|
||||
private final Duration timeout; |
||||
|
||||
@Nullable |
||||
private final String uriTemplate; |
||||
|
||||
@Nullable |
||||
private final Object mockServerResult; |
||||
|
||||
/** Ensure single logging, e.g. for expectAll. */ |
||||
private boolean diagnosticsLogged; |
||||
|
||||
/** |
||||
* Create an instance with an HTTP request and response along with promises |
||||
* for the serialized request and response body content. |
||||
* |
||||
* @param request the HTTP request |
||||
* @param response the HTTP response |
||||
*/ |
||||
ExchangeResult(RequestEntity request, ResponseEntity response) { |
||||
this(request, response, new byte[0], new byte[0], null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* Create an instance with an HTTP request and response along with promises |
||||
* for the serialized request and response body content. |
||||
* |
||||
* @param request the HTTP request |
||||
* @param response the HTTP response |
||||
* @param requestBody capture of serialized request body content |
||||
* @param responseBody capture of serialized response body content |
||||
* @param timeout how long to wait for content to materialize |
||||
* @param uriTemplate the URI template used to set up the request, if any |
||||
* @param serverResult the result of a mock server exchange if applicable. |
||||
*/ |
||||
ExchangeResult(RequestEntity request, ResponseEntity response, |
||||
byte[] requestBody, byte[] responseBody, Duration timeout, @Nullable String uriTemplate, |
||||
@Nullable Object serverResult) { |
||||
|
||||
Assert.notNull(request, "ClientHttpRequest is required"); |
||||
Assert.notNull(response, "ClientHttpResponse is required"); |
||||
Assert.notNull(requestBody, "'requestBody' is required"); |
||||
Assert.notNull(responseBody, "'responseBody' is required"); |
||||
|
||||
this.request = request; |
||||
this.response = response; |
||||
this.requestBody = requestBody; |
||||
this.responseBody = responseBody; |
||||
this.timeout = timeout; |
||||
this.uriTemplate = uriTemplate; |
||||
this.mockServerResult = serverResult; |
||||
} |
||||
|
||||
/** |
||||
* Copy constructor to use after body is decoded and/or consumed. |
||||
*/ |
||||
ExchangeResult(ExchangeResult other) { |
||||
this.request = other.request; |
||||
this.response = other.response; |
||||
this.requestBody = other.requestBody; |
||||
this.responseBody = other.responseBody; |
||||
this.timeout = other.timeout; |
||||
this.uriTemplate = other.uriTemplate; |
||||
this.mockServerResult = other.mockServerResult; |
||||
this.diagnosticsLogged = other.diagnosticsLogged; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the method of the request. |
||||
*/ |
||||
public HttpMethod getMethod() { |
||||
return this.request.getMethod(); |
||||
} |
||||
|
||||
/** |
||||
* Return the URI of the request. |
||||
*/ |
||||
public URI getUrl() { |
||||
return this.request.getUrl(); |
||||
} |
||||
|
||||
/** |
||||
* Return the original URI template used to prepare the request, if any. |
||||
*/ |
||||
@Nullable |
||||
public String getUriTemplate() { |
||||
return this.uriTemplate; |
||||
} |
||||
|
||||
/** |
||||
* Return the request headers sent to the server. |
||||
*/ |
||||
public HttpHeaders getRequestHeaders() { |
||||
return this.request.getHeaders(); |
||||
} |
||||
|
||||
/** |
||||
* Return the raw request body content written through the request. |
||||
* <p><strong>Note:</strong> If the request content has not been consumed |
||||
* for any reason yet, use of this method will trigger consumption. |
||||
* @throws IllegalStateException if the request body has not been fully written. |
||||
*/ |
||||
@Nullable |
||||
public byte[] getRequestBodyContent() { |
||||
return this.requestBody; |
||||
} |
||||
|
||||
/** |
||||
* Return the HTTP status code as an {@link HttpStatusCode} value. |
||||
*/ |
||||
public HttpStatusCode getStatus() { |
||||
return this.response.getStatusCode(); |
||||
} |
||||
|
||||
/** |
||||
* Return the HTTP status code as an integer. |
||||
* @since 5.1.10 |
||||
* @deprecated as of 6.0, in favor of {@link #getStatus()} |
||||
*/ |
||||
@Deprecated(since = "6.0", forRemoval = true) |
||||
public int getRawStatusCode() { |
||||
return getStatus().value(); |
||||
} |
||||
|
||||
/** |
||||
* Return the response headers received from the server. |
||||
*/ |
||||
public HttpHeaders getResponseHeaders() { |
||||
return this.response.getHeaders(); |
||||
} |
||||
|
||||
/** |
||||
* Return response cookies received from the server. |
||||
*/ |
||||
public MultiValueMap<String, ResponseCookie> getResponseCookies() { |
||||
return new LinkedMultiValueMap<>(); // TODO: getResponseCookies
|
||||
} |
||||
|
||||
/** |
||||
* Return the raw request body content written to the response. |
||||
* <p><strong>Note:</strong> If the response content has not been consumed |
||||
* yet, use of this method will trigger consumption. |
||||
* @throws IllegalStateException if the response has not been fully read. |
||||
*/ |
||||
@Nullable |
||||
public byte[] getResponseBodyContent() { |
||||
return this.responseBody; |
||||
} |
||||
|
||||
/** |
||||
* Return the result from the mock server exchange, if applicable, for |
||||
* further assertions on the state of the server response. |
||||
* @since 5.3 |
||||
* @see org.springframework.test.web.servlet.client.MockMvcWebTestClient#resultActionsFor(ExchangeResult) |
||||
*/ |
||||
@Nullable |
||||
public Object getMockServerResult() { |
||||
return this.mockServerResult; |
||||
} |
||||
|
||||
/** |
||||
* 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; |
||||
} |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "\n" + |
||||
"> " + getMethod() + " " + getUrl() + "\n" + |
||||
"> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" + |
||||
"\n" + |
||||
formatBody(getRequestHeaders().getContentType(), this.requestBody) + "\n" + |
||||
"\n" + |
||||
"< " + formatStatus(getStatus()) + "\n" + |
||||
"< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" + |
||||
"\n" + |
||||
formatBody(getResponseHeaders().getContentType(), this.responseBody) +"\n" + |
||||
formatMockServerResult(); |
||||
} |
||||
|
||||
private String formatStatus(HttpStatusCode statusCode) { |
||||
String result = statusCode.toString(); |
||||
if (statusCode instanceof HttpStatus status) { |
||||
result += " " + status.getReasonPhrase(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private String formatHeaders(HttpHeaders headers, String delimiter) { |
||||
return headers.entrySet().stream() |
||||
.map(entry -> entry.getKey() + ": " + entry.getValue()) |
||||
.collect(Collectors.joining(delimiter)); |
||||
} |
||||
|
||||
@Nullable |
||||
private String formatBody(@Nullable MediaType contentType, byte[] bytes) { |
||||
if (bytes == null) { |
||||
return "No content"; |
||||
} |
||||
if (contentType == null) { |
||||
return bytes.length + " bytes of content (unknown content-type)."; |
||||
} |
||||
Charset charset = contentType.getCharset(); |
||||
if (charset != null) { |
||||
return new String(bytes, charset); |
||||
} |
||||
if (PRINTABLE_MEDIA_TYPES.stream().anyMatch(contentType::isCompatibleWith)) { |
||||
return new String(bytes, StandardCharsets.UTF_8); |
||||
} |
||||
return bytes.length + " bytes of content."; |
||||
} |
||||
|
||||
private String formatMockServerResult() { |
||||
return (this.mockServerResult != null ? |
||||
"\n====================== MockMvc (Server) ===============================\n" + |
||||
this.mockServerResult + "\n" : ""); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
/* |
||||
* Copyright 2002-2021 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.net.URI; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.hamcrest.Matcher; |
||||
import org.hamcrest.MatcherAssert; |
||||
|
||||
import org.springframework.http.CacheControl; |
||||
import org.springframework.http.ContentDisposition; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.test.util.AssertionErrors; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals; |
||||
import static org.springframework.test.util.AssertionErrors.assertNotNull; |
||||
import static org.springframework.test.util.AssertionErrors.assertTrue; |
||||
|
||||
/** |
||||
* Assertions on headers of the response. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @author Brian Clozel |
||||
* @author Sam Brannen |
||||
* @since 5.0 |
||||
* @see TestRestClient.ResponseSpec#expectHeader() |
||||
*/ |
||||
public class HeaderAssertions { |
||||
|
||||
private final ExchangeResult exchangeResult; |
||||
|
||||
private final TestRestClient.ResponseSpec responseSpec; |
||||
|
||||
|
||||
HeaderAssertions(ExchangeResult result, TestRestClient.ResponseSpec spec) { |
||||
this.exchangeResult = result; |
||||
this.responseSpec = spec; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Expect a header with the given name to match the specified values. |
||||
*/ |
||||
public TestRestClient.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. |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec valueEquals(String headerName, long value) { |
||||
String actual = getHeaders().getFirst(headerName); |
||||
this.exchangeResult.assertWithDiagnostics(() -> |
||||
assertTrue("Response does not contain header '" + headerName + "'", actual != null)); |
||||
return assertHeader(headerName, value, Long.parseLong(Objects.requireNonNull(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. |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.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("Response header '" + 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 TestRestClient.ResponseSpec valueMatches(String name, String pattern) { |
||||
String value = getRequiredValue(name); |
||||
String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]"; |
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.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 |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec valuesMatch(String name, String... patterns) { |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
List<String> values = getRequiredValues(name); |
||||
AssertionErrors.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]; |
||||
AssertionErrors.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 |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.ResponseSpec value(String name, Matcher<? super String> matcher) { |
||||
String value = getHeaders().getFirst(name); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name); |
||||
MatcherAssert.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 |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec values(String name, Matcher<? super Iterable<String>> matcher) { |
||||
List<String> values = getHeaders().get(name); |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name); |
||||
MatcherAssert.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 |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.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 |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec values(String name, Consumer<List<String>> consumer) { |
||||
List<String> values = getRequiredValues(name); |
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(values)); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
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)) { |
||||
this.exchangeResult.assertWithDiagnostics(() -> |
||||
AssertionErrors.fail(getMessage(name) + " not found")); |
||||
} |
||||
return Objects.requireNonNull(values); |
||||
} |
||||
|
||||
/** |
||||
* Expect that the header with the given name is present. |
||||
* @since 5.0.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec exists(String name) { |
||||
if (!getHeaders().containsKey(name)) { |
||||
String message = getMessage(name) + " does not exist"; |
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); |
||||
} |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Expect that the header with the given name is not present. |
||||
*/ |
||||
public TestRestClient.ResponseSpec doesNotExist(String name) { |
||||
if (getHeaders().containsKey(name)) { |
||||
String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]"; |
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); |
||||
} |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Cache-Control" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec cacheControl(CacheControl cacheControl) { |
||||
return assertHeader("Cache-Control", cacheControl.getHeaderValue(), getHeaders().getCacheControl()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Disposition" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentDisposition(ContentDisposition contentDisposition) { |
||||
return assertHeader("Content-Disposition", contentDisposition, getHeaders().getContentDisposition()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Length" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentLength(long contentLength) { |
||||
return assertHeader("Content-Length", contentLength, getHeaders().getContentLength()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Type" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentType(MediaType mediaType) { |
||||
return assertHeader("Content-Type", mediaType, getHeaders().getContentType()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Type" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentType(String mediaType) { |
||||
return contentType(MediaType.parseMediaType(mediaType)); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Type" header compatible with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentTypeCompatibleWith(MediaType mediaType) { |
||||
MediaType actual = getHeaders().getContentType(); |
||||
String message = getMessage("Content-Type") + "=[" + actual + "] is not compatible with [" + mediaType + "]"; |
||||
this.exchangeResult.assertWithDiagnostics(() -> |
||||
AssertionErrors.assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType)))); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Content-Type" header compatible with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec contentTypeCompatibleWith(String mediaType) { |
||||
return contentTypeCompatibleWith(MediaType.parseMediaType(mediaType)); |
||||
} |
||||
|
||||
/** |
||||
* Expect an "Expires" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec expires(long expires) { |
||||
return assertHeader("Expires", expires, getHeaders().getExpires()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Last-Modified" header with the given value. |
||||
*/ |
||||
public TestRestClient.ResponseSpec lastModified(long lastModified) { |
||||
return assertHeader("Last-Modified", lastModified, getHeaders().getLastModified()); |
||||
} |
||||
|
||||
/** |
||||
* Expect a "Location" header with the given value. |
||||
* @since 5.3 |
||||
*/ |
||||
public TestRestClient.ResponseSpec location(String location) { |
||||
return assertHeader("Location", URI.create(location), getHeaders().getLocation()); |
||||
} |
||||
|
||||
|
||||
private HttpHeaders getHeaders() { |
||||
return this.exchangeResult.getResponseHeaders(); |
||||
} |
||||
|
||||
private String getMessage(String headerName) { |
||||
return "Response header '" + headerName + "'"; |
||||
} |
||||
|
||||
private TestRestClient.ResponseSpec assertHeader(String name, @Nullable Object expected, @Nullable Object actual) { |
||||
this.exchangeResult.assertWithDiagnostics(() -> { |
||||
String message = getMessage(name); |
||||
AssertionErrors.assertEquals(message, expected, actual); |
||||
}); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Enumeration; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
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.RequestHeader; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RequestMethod; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
@RestController |
||||
@RequestMapping("/httpbin") |
||||
public class HttpBinCompatibleController { |
||||
|
||||
private static final Log log = LogFactory.getLog(HttpBinCompatibleController.class); |
||||
|
||||
private static final String HEADER_REQ_VARY = "X-Request-Vary"; |
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
||||
|
||||
@GetMapping("/") |
||||
public String home() { |
||||
return "httpbin compatible home"; |
||||
} |
||||
|
||||
@RequestMapping(path = "/headers", method = { RequestMethod.GET, RequestMethod.POST }, |
||||
produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Map<String, Object> headers(HttpServletRequest request) { |
||||
Map<String, Object> result = new HashMap<>(); |
||||
result.put("headers", getHeaders(request)); |
||||
return result; |
||||
} |
||||
|
||||
/*@PatchMapping("/headers") |
||||
public ResponseEntity<Map<String, Object>> headersPatch(ServerWebExchange exchange, |
||||
@RequestBody Map<String, String> headersToAdd) { |
||||
Map<String, Object> result = new HashMap<>(); |
||||
result.put("headers", getHeaders(exchange)); |
||||
ResponseEntity.BodyBuilder responseEntity = ResponseEntity.status(HttpStatus.OK); |
||||
headersToAdd.forEach(responseEntity::header); |
||||
|
||||
return responseEntity.body(result); |
||||
}*/ |
||||
|
||||
@RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET, RequestMethod.POST }, |
||||
produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Map<String, Object> multiValueHeaders(ServerWebExchange exchange) { |
||||
Map<String, Object> result = new HashMap<>(); |
||||
result.put("headers", exchange.getRequest().getHeaders()); |
||||
return result; |
||||
} |
||||
|
||||
/*@GetMapping(path = "/delay/{sec}/**", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Mono<Map<String, Object>> delay(ServerWebExchange exchange, @PathVariable int sec) |
||||
throws InterruptedException { |
||||
int delay = Math.min(sec, 10); |
||||
return Mono.just(get(exchange)).delayElement(Duration.ofSeconds(delay)); |
||||
}*/ |
||||
|
||||
@GetMapping(path = "/anything/{anything}", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Map<String, Object> anything(HttpServletRequest request, @PathVariable(required = false) String anything) { |
||||
return get(request); |
||||
} |
||||
|
||||
@GetMapping(path = "/get", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Map<String, Object> get(HttpServletRequest request) { |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("httpbin /get"); |
||||
} |
||||
HashMap<String, Object> result = new HashMap<>(); |
||||
Map<String, String[]> params = request.getParameterMap(); |
||||
result.put("args", params); |
||||
result.put("headers", getHeaders(request)); |
||||
return result; |
||||
} |
||||
|
||||
/*@PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, |
||||
produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Mono<Map<String, Object>> postFormData(@RequestBody Mono<MultiValueMap<String, Part>> parts) { |
||||
// StringDecoder decoder = StringDecoder.allMimeTypes(true);
|
||||
return parts.flux().flatMap(map -> Flux.fromIterable(map.values())).flatMap(Flux::fromIterable) |
||||
.filter(part -> part instanceof FilePart).reduce(new HashMap<String, Object>(), (files, part) -> { |
||||
MediaType contentType = part.headers().getContentType(); |
||||
long contentLength = part.headers().getContentLength(); |
||||
// TODO: get part data
|
||||
files.put(part.name(), "data:" + contentType + ";base64," + contentLength); |
||||
return files; |
||||
}).map(files -> Collections.singletonMap("files", files)); |
||||
}*/ |
||||
|
||||
/*@PostMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, |
||||
produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Mono<Map<String, Object>> postUrlEncoded(ServerWebExchange exchange) throws IOException { |
||||
return post(exchange, null); |
||||
}*/ |
||||
|
||||
/*@PostMapping(path = "/post", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Mono<Map<String, Object>> post(ServerWebExchange exchange, @RequestBody(required = false) String body) |
||||
throws IOException { |
||||
HashMap<String, Object> ret = new HashMap<>(); |
||||
ret.put("headers", getHeaders(exchange)); |
||||
ret.put("data", body); |
||||
HashMap<String, Object> form = new HashMap<>(); |
||||
ret.put("form", form); |
||||
|
||||
return exchange.getFormData().flatMap(map -> { |
||||
for (Map.Entry<String, List<String>> entry : map.entrySet()) { |
||||
for (String value : entry.getValue()) { |
||||
form.put(entry.getKey(), value); |
||||
} |
||||
} |
||||
return Mono.just(ret); |
||||
}); |
||||
}*/ |
||||
|
||||
@GetMapping("/status/{status}") |
||||
public ResponseEntity<String> status(@PathVariable int status) { |
||||
return ResponseEntity.status(status).body("Failed with " + status); |
||||
} |
||||
|
||||
@RequestMapping(value = "/responseheaders/{status}", method = { RequestMethod.GET, RequestMethod.POST }) |
||||
public ResponseEntity<Map<String, Object>> responseHeaders(@PathVariable int status, ServerWebExchange exchange) { |
||||
HttpHeaders httpHeaders = exchange.getRequest().getHeaders().entrySet().stream() |
||||
.filter(entry -> entry.getKey().startsWith("X-Test-")) |
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, |
||||
(list1, list2) -> Stream.concat(list1.stream(), list2.stream()).collect(Collectors.toList()), |
||||
HttpHeaders::new)); |
||||
|
||||
return ResponseEntity.status(status).headers(httpHeaders).body(Collections.singletonMap("status", status)); |
||||
} |
||||
|
||||
@PostMapping(path = "/post/empty", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public String emptyResponse() { |
||||
return null; |
||||
} |
||||
|
||||
/*@GetMapping(path = "/gzip", produces = MediaType.APPLICATION_JSON_VALUE) |
||||
public Mono<Void> gzip(ServerWebExchange exchange) throws IOException { |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("httpbin /gzip"); |
||||
} |
||||
|
||||
String jsonResponse = OBJECT_MAPPER.writeValueAsString("httpbin compatible home"); |
||||
byte[] bytes = jsonResponse.getBytes(StandardCharsets.UTF_8); |
||||
|
||||
ServerHttpResponse response = exchange.getResponse(); |
||||
response.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); |
||||
DataBufferFactory dataBufferFactory = response.bufferFactory(); |
||||
response.setStatusCode(HttpStatus.OK); |
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
||||
GZIPOutputStream is = new GZIPOutputStream(bos); |
||||
FileCopyUtils.copy(bytes, is); |
||||
|
||||
byte[] gzippedResponse = bos.toByteArray(); |
||||
DataBuffer wrap = dataBufferFactory.wrap(gzippedResponse); |
||||
return response.writeWith(Flux.just(wrap)); |
||||
}*/ |
||||
|
||||
@GetMapping("/vary-on-header/**") |
||||
public ResponseEntity<Map<String, Object>> varyOnAccept(HttpServletRequest request, |
||||
@RequestHeader(name = HEADER_REQ_VARY, required = false) String headerToVary) { |
||||
if (headerToVary == null) { |
||||
return ResponseEntity.badRequest().body(Map.of("error", HEADER_REQ_VARY + " header is mandatory")); |
||||
} |
||||
else { |
||||
var builder = ResponseEntity.ok(); |
||||
builder.varyBy(headerToVary); |
||||
return builder.body(headers(request)); |
||||
} |
||||
} |
||||
|
||||
public Map<String, String> getHeaders(HttpServletRequest req) { |
||||
HashMap<String, String> headers = new HashMap<>(); |
||||
Enumeration<String> headerNames = req.getHeaderNames(); |
||||
|
||||
while (headerNames.hasMoreElements()) { |
||||
String name = headerNames.nextElement(); |
||||
String value = null; |
||||
|
||||
Enumeration<String> values = req.getHeaders(name); |
||||
if (values.hasMoreElements()) { |
||||
value = values.nextElement(); |
||||
} |
||||
headers.put(name, value); |
||||
} |
||||
return headers; |
||||
// return request.headers().asHttpHeaders().toSingleValueMap();
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,193 @@
@@ -0,0 +1,193 @@
|
||||
/* |
||||
* Copyright 2002-2020 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.util.function.Consumer; |
||||
|
||||
import org.hamcrest.Matcher; |
||||
|
||||
import org.springframework.cloud.gateway.server.mvc.test.TestRestClient; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.test.util.JsonPathExpectationsHelper; |
||||
|
||||
/** |
||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.0 |
||||
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
|
||||
* @see JsonPathExpectationsHelper |
||||
*/ |
||||
public class JsonPathAssertions { |
||||
|
||||
private final TestRestClient.BodyContentSpec bodySpec; |
||||
|
||||
private final String content; |
||||
|
||||
private final JsonPathExpectationsHelper pathHelper; |
||||
|
||||
|
||||
JsonPathAssertions(TestRestClient.BodyContentSpec spec, String content, String expression, Object... args) { |
||||
this.bodySpec = spec; |
||||
this.content = content; |
||||
this.pathHelper = new JsonPathExpectationsHelper(expression, args); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValue(String, Object)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isEqualTo(Object expectedValue) { |
||||
this.pathHelper.assertValue(this.content, expectedValue); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#exists(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec exists() { |
||||
this.pathHelper.exists(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#doesNotExist(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec doesNotExist() { |
||||
this.pathHelper.doesNotExist(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsEmpty(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isEmpty() { |
||||
this.pathHelper.assertValueIsEmpty(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNotEmpty(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isNotEmpty() { |
||||
this.pathHelper.assertValueIsNotEmpty(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#hasJsonPath}. |
||||
* @since 5.0.3 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec hasJsonPath() { |
||||
this.pathHelper.hasJsonPath(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#doesNotHaveJsonPath}. |
||||
* @since 5.0.3 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec doesNotHaveJsonPath() { |
||||
this.pathHelper.doesNotHaveJsonPath(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsBoolean(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isBoolean() { |
||||
this.pathHelper.assertValueIsBoolean(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsNumber(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isNumber() { |
||||
this.pathHelper.assertValueIsNumber(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsArray(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isArray() { |
||||
this.pathHelper.assertValueIsArray(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Applies {@link JsonPathExpectationsHelper#assertValueIsMap(String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isMap() { |
||||
this.pathHelper.assertValueIsMap(this.content); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher)}. |
||||
* @since 5.1 |
||||
*/ |
||||
public <T> TestRestClient.BodyContentSpec value(Matcher<? super T> matcher) { |
||||
this.pathHelper.assertValue(this.content, matcher); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}. |
||||
* @since 5.1 |
||||
*/ |
||||
public <T> TestRestClient.BodyContentSpec value(Matcher<? super T> matcher, Class<T> targetType) { |
||||
this.pathHelper.assertValue(this.content, matcher, targetType); |
||||
return this.bodySpec; |
||||
} |
||||
|
||||
/** |
||||
* Consume the result of the JSONPath evaluation. |
||||
* @since 5.1 |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public <T> TestRestClient.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. |
||||
* @since 5.1 |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public <T> TestRestClient.BodyContentSpec value(Consumer<T> consumer, Class<T> targetType) { |
||||
Object value = this.pathHelper.evaluateJsonPath(this.content, targetType); |
||||
consumer.accept((T) 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,354 @@
@@ -0,0 +1,354 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.net.URI; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
|
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.util.DefaultUriBuilderFactory; |
||||
import org.springframework.web.util.UriBuilder; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
import org.springframework.web.util.UriComponents; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
import org.springframework.web.util.UriUtils; |
||||
|
||||
public class LocalHostUriBuilderFactory implements UriBuilderFactory { |
||||
private static final String PREFIX = "server.servlet."; |
||||
|
||||
private final Environment environment; |
||||
|
||||
private final String scheme; |
||||
|
||||
private DefaultUriBuilderFactory.EncodingMode encodingMode = DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES; |
||||
|
||||
private final Map<String, Object> defaultUriVariables = new HashMap<>(); |
||||
|
||||
private boolean parsePath = true; |
||||
|
||||
/** |
||||
* Create a new {@code LocalHostUriTemplateHandler} that will generate {@code http} |
||||
* URIs using the given {@code environment} to determine the context path and port. |
||||
* |
||||
* @param environment the environment used to determine the port |
||||
*/ |
||||
public LocalHostUriBuilderFactory(Environment environment) { |
||||
this(environment, "http"); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code LocalHostUriTemplateHandler} that will generate URIs with the |
||||
* given {@code scheme} and use the given {@code environment} to determine the |
||||
* context-path and port. |
||||
* |
||||
* @param environment the environment used to determine the port |
||||
* @param scheme the scheme of the root uri |
||||
* @since 1.4.1 |
||||
*/ |
||||
public LocalHostUriBuilderFactory(Environment environment, String scheme) { |
||||
Assert.notNull(environment, "Environment must not be null"); |
||||
Assert.notNull(scheme, "Scheme must not be null"); |
||||
this.environment = environment; |
||||
this.scheme = scheme; |
||||
} |
||||
|
||||
public String getPort() { |
||||
return this.environment.getProperty("local.server.port", "8080"); |
||||
} |
||||
|
||||
public String getContextPath() { |
||||
return this.environment.getProperty(PREFIX + "context-path", ""); |
||||
} |
||||
|
||||
public String getScheme() { |
||||
return scheme; |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link DefaultUriBuilderFactory.EncodingMode encoding mode} to use. |
||||
* <p>By default this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES |
||||
* EncodingMode.TEMPLATE_AND_VALUES}. |
||||
* <p><strong>Note:</strong> Prior to 5.1 the default was |
||||
* {@link DefaultUriBuilderFactory.EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT} |
||||
* therefore the {@code WebClient} {@code RestTemplate} have switched their |
||||
* default behavior. |
||||
* @param encodingMode the encoding mode to use |
||||
*/ |
||||
public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode) { |
||||
this.encodingMode = encodingMode; |
||||
} |
||||
|
||||
/** |
||||
* Return the configured encoding mode. |
||||
*/ |
||||
public DefaultUriBuilderFactory.EncodingMode getEncodingMode() { |
||||
return this.encodingMode; |
||||
} |
||||
|
||||
/** |
||||
* Provide default URI variable values to use when expanding URI templates |
||||
* with a Map of variables. |
||||
* @param defaultUriVariables default URI variable values |
||||
*/ |
||||
public void setDefaultUriVariables(@Nullable Map<String, ?> defaultUriVariables) { |
||||
this.defaultUriVariables.clear(); |
||||
if (defaultUriVariables != null) { |
||||
this.defaultUriVariables.putAll(defaultUriVariables); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the configured default URI variable values. |
||||
*/ |
||||
public Map<String, ?> getDefaultUriVariables() { |
||||
return Collections.unmodifiableMap(this.defaultUriVariables); |
||||
} |
||||
|
||||
/** |
||||
* Whether to parse the input path into path segments if the encoding mode |
||||
* is set to {@link DefaultUriBuilderFactory.EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT}, |
||||
* which ensures that URI variables in the path are encoded according to |
||||
* path segment rules and for example a '/' is encoded. |
||||
* <p>By default this is set to {@code true}. |
||||
* @param parsePath whether to parse the path into path segments |
||||
*/ |
||||
public void setParsePath(boolean parsePath) { |
||||
this.parsePath = parsePath; |
||||
} |
||||
|
||||
/** |
||||
* Whether to parse the path into path segments if the encoding mode is set |
||||
* to {@link DefaultUriBuilderFactory.EncodingMode#URI_COMPONENT EncodingMode.URI_COMPONENT}. |
||||
*/ |
||||
public boolean shouldParsePath() { |
||||
return this.parsePath; |
||||
} |
||||
|
||||
@Override |
||||
public UriBuilder uriString(String uriTemplate) { |
||||
return new DefaultUriBuilder(uriTemplate); |
||||
} |
||||
|
||||
@Override |
||||
public UriBuilder builder() { |
||||
return new DefaultUriBuilder(""); |
||||
} |
||||
|
||||
@Override |
||||
public URI expand(String uriTemplate, Map<String, ?> uriVars) { |
||||
return uriString(uriTemplate).build(uriVars); |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public URI expand(String uriTemplate, Object... uriVars) { |
||||
return uriString(uriTemplate).build(uriVars); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* {@link DefaultUriBuilderFactory} specific implementation of UriBuilder. |
||||
*/ |
||||
private class DefaultUriBuilder implements UriBuilder { |
||||
|
||||
private final UriComponentsBuilder uriComponentsBuilder; |
||||
|
||||
public DefaultUriBuilder(String uriTemplate) { |
||||
this.uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); |
||||
} |
||||
|
||||
private UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) { |
||||
UriComponentsBuilder result = UriComponentsBuilder.fromUriString(uriTemplate); |
||||
if (encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES)) { |
||||
result.encode(); |
||||
} |
||||
parsePathIfNecessary(result); |
||||
return result; |
||||
} |
||||
|
||||
private void parsePathIfNecessary(UriComponentsBuilder result) { |
||||
if (parsePath && encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT)) { |
||||
UriComponents uric = result.build(); |
||||
String path = uric.getPath(); |
||||
result.replacePath(null); |
||||
for (String segment : uric.getPathSegments()) { |
||||
result.pathSegment(segment); |
||||
} |
||||
if (path != null && path.endsWith("/")) { |
||||
result.path("/"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public DefaultUriBuilder scheme(@Nullable String scheme) { |
||||
this.uriComponentsBuilder.scheme(scheme); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder userInfo(@Nullable String userInfo) { |
||||
this.uriComponentsBuilder.userInfo(userInfo); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder host(@Nullable String host) { |
||||
this.uriComponentsBuilder.host(host); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder port(int port) { |
||||
this.uriComponentsBuilder.port(port); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder port(@Nullable String port) { |
||||
this.uriComponentsBuilder.port(port); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder path(String path) { |
||||
this.uriComponentsBuilder.path(path); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder replacePath(@Nullable String path) { |
||||
this.uriComponentsBuilder.replacePath(path); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder pathSegment(String... pathSegments) { |
||||
this.uriComponentsBuilder.pathSegment(pathSegments); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder query(String query) { |
||||
this.uriComponentsBuilder.query(query); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder replaceQuery(@Nullable String query) { |
||||
this.uriComponentsBuilder.replaceQuery(query); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder queryParam(String name, Object... values) { |
||||
this.uriComponentsBuilder.queryParam(name, values); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder queryParam(String name, @Nullable Collection<?> values) { |
||||
this.uriComponentsBuilder.queryParam(name, values); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder queryParamIfPresent(String name, Optional<?> value) { |
||||
this.uriComponentsBuilder.queryParamIfPresent(name, value); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder queryParams(MultiValueMap<String, String> params) { |
||||
this.uriComponentsBuilder.queryParams(params); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder replaceQueryParam(String name, Object... values) { |
||||
this.uriComponentsBuilder.replaceQueryParam(name, values); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder replaceQueryParam(String name, @Nullable Collection<?> values) { |
||||
this.uriComponentsBuilder.replaceQueryParam(name, values); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder replaceQueryParams(MultiValueMap<String, String> params) { |
||||
this.uriComponentsBuilder.replaceQueryParams(params); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public DefaultUriBuilder fragment(@Nullable String fragment) { |
||||
this.uriComponentsBuilder.fragment(fragment); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public URI build(Map<String, ?> uriVars) { |
||||
if (!defaultUriVariables.isEmpty()) { |
||||
Map<String, Object> map = new HashMap<>(); |
||||
map.putAll(defaultUriVariables); |
||||
map.putAll(uriVars); |
||||
uriVars = map; |
||||
} |
||||
if (encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY)) { |
||||
uriVars = UriUtils.encodeUriVariables(uriVars); |
||||
} |
||||
|
||||
this.uriComponentsBuilder.scheme(getScheme()); |
||||
this.uriComponentsBuilder.port(getPort()); |
||||
this.uriComponentsBuilder.replacePath(getContextPath()); |
||||
UriComponents uric = this.uriComponentsBuilder.build().expand(uriVars); |
||||
return createUri(uric); |
||||
} |
||||
|
||||
@Override |
||||
public URI build(Object... uriVars) { |
||||
if (ObjectUtils.isEmpty(uriVars) && !defaultUriVariables.isEmpty()) { |
||||
return build(Collections.emptyMap()); |
||||
} |
||||
if (encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY)) { |
||||
uriVars = UriUtils.encodeUriVariables(uriVars); |
||||
} |
||||
UriComponents uric = this.uriComponentsBuilder.build().expand(uriVars); |
||||
return createUri(uric); |
||||
} |
||||
|
||||
private URI createUri(UriComponents uric) { |
||||
if (encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT)) { |
||||
uric = uric.encode(); |
||||
} |
||||
return URI.create(uric.toString()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,242 @@
@@ -0,0 +1,242 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
|
||||
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.reactive.server.WebTestClient; |
||||
|
||||
public class StatusAssertions { |
||||
private final ExchangeResult exchangeResult; |
||||
|
||||
private final TestRestClient.ResponseSpec responseSpec; |
||||
|
||||
|
||||
StatusAssertions(ExchangeResult result, TestRestClient.ResponseSpec spec) { |
||||
this.exchangeResult = result; |
||||
this.responseSpec = spec; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Assert the response status as an {@link HttpStatusCode}. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec isEqualTo(int status) { |
||||
return isEqualTo(HttpStatusCode.valueOf(status)); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.OK} (200). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isOk() { |
||||
return assertStatusAndReturn(HttpStatus.OK); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.CREATED} (201). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isCreated() { |
||||
return assertStatusAndReturn(HttpStatus.CREATED); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isAccepted() { |
||||
return assertStatusAndReturn(HttpStatus.ACCEPTED); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isNoContent() { |
||||
return assertStatusAndReturn(HttpStatus.NO_CONTENT); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.FOUND} (302). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isFound() { |
||||
return assertStatusAndReturn(HttpStatus.FOUND); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isSeeOther() { |
||||
return assertStatusAndReturn(HttpStatus.SEE_OTHER); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isNotModified() { |
||||
return assertStatusAndReturn(HttpStatus.NOT_MODIFIED); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isTemporaryRedirect() { |
||||
return assertStatusAndReturn(HttpStatus.TEMPORARY_REDIRECT); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isPermanentRedirect() { |
||||
return assertStatusAndReturn(HttpStatus.PERMANENT_REDIRECT); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isBadRequest() { |
||||
return assertStatusAndReturn(HttpStatus.BAD_REQUEST); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isUnauthorized() { |
||||
return assertStatusAndReturn(HttpStatus.UNAUTHORIZED); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403). |
||||
* @since 5.0.2 |
||||
*/ |
||||
public TestRestClient.ResponseSpec isForbidden() { |
||||
return assertStatusAndReturn(HttpStatus.FORBIDDEN); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404). |
||||
*/ |
||||
public TestRestClient.ResponseSpec isNotFound() { |
||||
return assertStatusAndReturn(HttpStatus.NOT_FOUND); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response error message. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.ResponseSpec is1xxInformational() { |
||||
return assertSeriesAndReturn(HttpStatus.Series.INFORMATIONAL); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is in the 2xx range. |
||||
*/ |
||||
public TestRestClient.ResponseSpec is2xxSuccessful() { |
||||
return assertSeriesAndReturn(HttpStatus.Series.SUCCESSFUL); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is in the 3xx range. |
||||
*/ |
||||
public TestRestClient.ResponseSpec is3xxRedirection() { |
||||
return assertSeriesAndReturn(HttpStatus.Series.REDIRECTION); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is in the 4xx range. |
||||
*/ |
||||
public TestRestClient.ResponseSpec is4xxClientError() { |
||||
return assertSeriesAndReturn(HttpStatus.Series.CLIENT_ERROR); |
||||
} |
||||
|
||||
/** |
||||
* Assert the response status code is in the 5xx range. |
||||
*/ |
||||
public TestRestClient.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 TestRestClient.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 TestRestClient.ResponseSpec value(Consumer<Integer> consumer) { |
||||
int actual = this.exchangeResult.getStatus().value(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(actual)); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
|
||||
private TestRestClient.ResponseSpec assertStatusAndReturn(HttpStatusCode expected) { |
||||
HttpStatusCode actual = this.exchangeResult.getStatus(); |
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", expected, actual)); |
||||
return this.responseSpec; |
||||
} |
||||
|
||||
private TestRestClient.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,638 @@
@@ -0,0 +1,638 @@
|
||||
/* |
||||
* Copyright 2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
import java.net.URI; |
||||
import java.nio.charset.Charset; |
||||
import java.time.ZonedDateTime; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.Consumer; |
||||
import java.util.function.Function; |
||||
|
||||
import org.hamcrest.Matcher; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.ReactiveAdapterRegistry; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.util.UriBuilder; |
||||
import org.springframework.web.util.UriBuilderFactory; |
||||
|
||||
public interface TestRestClient { |
||||
/** |
||||
* The name of a request header used to assign a unique id to every request |
||||
* performed through the {@code WebTestClient}. This can be useful for |
||||
* storing contextual information at all phases of request processing (e.g. |
||||
* from a server-side component) under that id and later to look up |
||||
* that information once an {@link ExchangeResult} is available. |
||||
*/ |
||||
String TESTRESTCLIENT_REQUEST_ID = "TestRestClient-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);
|
||||
|
||||
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 (e.g. |
||||
* 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 (e.g. |
||||
* 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(Object) 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 length of the body in bytes, as specified by the |
||||
* {@code Content-Length} header. |
||||
* @param contentLength the content length |
||||
* @return the same instance |
||||
* @see HttpHeaders#setContentLength(long) |
||||
*/ |
||||
RequestBodySpec contentLength(long contentLength); |
||||
|
||||
/** |
||||
* 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.reactive.function.client.WebClient.RequestBodySpec#bodyValue(Object) |
||||
* bodyValue} method on the underlying {@code WebClient}. |
||||
* @param body the value to write to the request body |
||||
* @return spec for further declaration of the request |
||||
* @since 5.2 |
||||
*/ |
||||
RequestHeadersSpec<?> bodyValue(Object body); |
||||
|
||||
/** |
||||
* Set the body from the given {@code Publisher}. Shortcut for |
||||
* {@link #body(BodyInserter)} with a |
||||
* {@linkplain BodyInserters#fromPublisher Publisher inserter}. |
||||
* @param publisher the request body data |
||||
* @param elementClass the class of elements contained in the publisher |
||||
* @param <T> the type of the elements contained in the publisher |
||||
* @param <S> the type of the {@code Publisher} |
||||
* @return spec for further declaration of the request |
||||
*/ |
||||
//<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
|
||||
|
||||
/** |
||||
* Variant of {@link #body(Publisher, Class)} that allows providing |
||||
* element type information with generics. |
||||
* @param publisher the request body data |
||||
* @param elementTypeRef the type reference of elements contained in the publisher |
||||
* @param <T> the type of the elements contained in the publisher |
||||
* @param <S> the type of the {@code Publisher} |
||||
* @return spec for further declaration of the request |
||||
* @since 5.2 |
||||
*/ |
||||
//<T, S extends Publisher<T>> RequestHeadersSpec<?> body(
|
||||
// S publisher, ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/** |
||||
* Set the body from the given producer. This method invokes the |
||||
* {@link org.springframework.web.reactive.function.client.WebClient.RequestBodySpec#body(Object, Class) |
||||
* body(Object, Class)} method on the underlying {@code WebClient}. |
||||
* @param producer the producer to write to the request. This must be a |
||||
* {@link Publisher} or another producer adaptable to a |
||||
* {@code Publisher} via {@link ReactiveAdapterRegistry} |
||||
* @param elementClass the class of elements contained in the producer |
||||
* @return spec for further declaration of the request |
||||
* @since 5.2 |
||||
*/ |
||||
RequestHeadersSpec<?> body(Object producer, Class<?> elementClass); |
||||
|
||||
/** |
||||
* Set the body from the given producer. This method invokes the |
||||
* {@link org.springframework.web.reactive.function.client.WebClient.RequestBodySpec#body(Object, ParameterizedTypeReference) |
||||
* body(Object, ParameterizedTypeReference)} method on the underlying {@code WebClient}. |
||||
* @param producer the producer to write to the request. This must be a |
||||
* {@link Publisher} or another producer adaptable to a |
||||
* {@code Publisher} via {@link ReactiveAdapterRegistry} |
||||
* @param elementTypeRef the type reference of elements contained in the producer |
||||
* @return spec for further declaration of the request |
||||
* @since 5.2 |
||||
*/ |
||||
RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementTypeRef); |
||||
|
||||
/** |
||||
* Set the body of the request to the given {@code BodyInserter}. |
||||
* This method invokes the |
||||
* {@link org.springframework.web.reactive.function.client.WebClient.RequestBodySpec#body(BodyInserter) |
||||
* body(BodyInserter)} method on the underlying {@code WebClient}. |
||||
* @param inserter the body inserter to use |
||||
* @return spec for further declaration of the request |
||||
* @see org.springframework.web.reactive.function.BodyInserters |
||||
*/ |
||||
//RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
|
||||
} |
||||
|
||||
/** |
||||
* 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 { |
||||
|
||||
/** |
||||
* Apply multiple assertions to a response with the given |
||||
* {@linkplain 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"> |
||||
* 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 |
||||
* @since 5.3.10 |
||||
*/ |
||||
ResponseSpec expectAll(ResponseSpec.ResponseSpecConsumer... consumers); |
||||
|
||||
/** |
||||
* Assertions on the response status. |
||||
*/ |
||||
StatusAssertions expectStatus(); |
||||
|
||||
/** |
||||
* Assertions on the headers of the response. |
||||
*/ |
||||
HeaderAssertions expectHeader(); |
||||
|
||||
/** |
||||
* Assertions on the cookies of the response. |
||||
* @since 5.3 |
||||
*/ |
||||
CookieAssertions expectCookie(); |
||||
|
||||
/** |
||||
* 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); |
||||
|
||||
/** |
||||
* Consume and decode the response body to {@code List<E>} and then apply |
||||
* List-specific assertions. |
||||
* @param elementType the expected List element type |
||||
*/ |
||||
<E> ListBodySpec<E> expectBodyList(Class<E> elementType); |
||||
|
||||
/** |
||||
* Alternative to {@link #expectBodyList(Class)} that accepts information |
||||
* about a target type with generics. |
||||
*/ |
||||
<E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType); |
||||
|
||||
/** |
||||
* Consume and decode the response body to {@code byte[]} and then apply |
||||
* assertions on the raw content (e.g. isEmpty, JSONPath, etc.) |
||||
*/ |
||||
BodyContentSpec expectBody(); |
||||
|
||||
/** |
||||
* Exit the chained flow in order to consume the response body |
||||
* externally, e.g. via {@link reactor.test.StepVerifier}. |
||||
* <p>Note that when {@code Void.class} is passed in, the response body |
||||
* is consumed and released. If no content is expected, then consider |
||||
* using {@code .expectBody().isEmpty()} instead which asserts that |
||||
* there is no content. |
||||
*/ |
||||
//<T> FluxExchangeResult<T> returnResult(Class<T> elementClass);
|
||||
|
||||
/** |
||||
* Alternative to {@link #returnResult(Class)} that accepts information |
||||
* about a target type with generics. |
||||
*/ |
||||
//<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
|
||||
|
||||
/** |
||||
* {@link Consumer} of a {@link ResponseSpec}. |
||||
* @since 5.3.10 |
||||
* @see ResponseSpec#expectAll(ResponseSpec.ResponseSpecConsumer...) |
||||
*/ |
||||
@FunctionalInterface |
||||
interface ResponseSpecConsumer extends Consumer<ResponseSpec> { |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
/** |
||||
* 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>> { |
||||
|
||||
/** |
||||
* Assert the extracted body is equal to the given value. |
||||
*/ |
||||
<T extends S> T isEqualTo(B expected); |
||||
|
||||
/** |
||||
* Assert the extracted body with a {@link Matcher}. |
||||
* @since 5.1 |
||||
*/ |
||||
<T extends S> T value(Matcher<? super B> matcher); |
||||
|
||||
/** |
||||
* Transform the extracted the body with a function, e.g. extracting a |
||||
* property, and assert the mapped value with a {@link Matcher}. |
||||
* @since 5.1 |
||||
*/ |
||||
<T extends S, R> T value(Function<B, R> bodyMapper, Matcher<? super R> matcher); |
||||
|
||||
/** |
||||
* Assert the extracted body with a {@link Consumer}. |
||||
* @since 5.1 |
||||
*/ |
||||
<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 ExchangeResult} with the |
||||
* decoded response content. |
||||
*/ |
||||
EntityExchangeResult<B> returnResult(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Spec for expectations on the response body decoded to a List. |
||||
* |
||||
* @param <E> the body list element type |
||||
*/ |
||||
interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> { |
||||
|
||||
/** |
||||
* Assert the extracted list of values is of the given size. |
||||
* @param size the expected size |
||||
*/ |
||||
ListBodySpec<E> hasSize(int size); |
||||
|
||||
/** |
||||
* Assert the extracted list of values contains the given elements. |
||||
* @param elements the elements to check |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
ListBodySpec<E> contains(E... elements); |
||||
|
||||
/** |
||||
* Assert the extracted list of values doesn't contain the given elements. |
||||
* @param elements the elements to check |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
ListBodySpec<E> doesNotContain(E... elements); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 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, boolean) |
||||
*/ |
||||
default BodyContentSpec json(String expectedJson) { |
||||
return json(expectedJson, false); |
||||
} |
||||
|
||||
/** |
||||
* 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. |
||||
* <p>Can compare in two modes, depending on the {@code strict} parameter value: |
||||
* <ul> |
||||
* <li>{@code true}: strict checking. Not extensible and strict array ordering.</li> |
||||
* <li>{@code false}: lenient checking. Extensible and non-strict array ordering.</li> |
||||
* </ul> |
||||
* <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 strict enables strict checking if {@code true} |
||||
* @since 5.3.16 |
||||
* @see #json(String) |
||||
*/ |
||||
BodyContentSpec json(String expectedJson, boolean strict); |
||||
|
||||
/** |
||||
* 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 JSON content. |
||||
* @since 5.1 |
||||
* @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String) |
||||
*/ |
||||
BodyContentSpec xml(String expectedXml); |
||||
|
||||
/** |
||||
* 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. |
||||
* <p>The JSON path expression can be a parameterized string using |
||||
* formatting specifiers as defined in {@link String#format}. |
||||
* @param expression the JsonPath expression |
||||
* @param args arguments to parameterize the expression |
||||
*/ |
||||
JsonPathAssertions jsonPath(String expression, Object... args); |
||||
|
||||
/** |
||||
* 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 |
||||
* @since 5.1 |
||||
* @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 |
||||
* @since 5.1 |
||||
*/ |
||||
XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args); |
||||
|
||||
/** |
||||
* Assert the response body content with the given {@link Consumer}. |
||||
* @param consumer the consumer for the response body; the input |
||||
* {@code byte[]} may be {@code null} if there was no response body. |
||||
*/ |
||||
BodyContentSpec consumeWith(Consumer<EntityExchangeResult<byte[]>> consumer); |
||||
|
||||
/** |
||||
* Exit the chained API and return an {@code ExchangeResult} with the |
||||
* raw response content. |
||||
*/ |
||||
EntityExchangeResult<byte[]> returnResult(); |
||||
} |
||||
} |
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
/* |
||||
* Copyright 2002-2023 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.cloud.gateway.server.mvc.test; |
||||
|
||||
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.springframework.cloud.gateway.server.mvc.test.TestRestClient; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.test.util.XpathExpectationsHelper; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* XPath assertions for the {@link TestRestClient}. |
||||
* |
||||
* @author Eric Deandrea |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.1 |
||||
*/ |
||||
public class XpathAssertions { |
||||
|
||||
private final TestRestClient.BodyContentSpec bodySpec; |
||||
|
||||
private final XpathExpectationsHelper xpathHelper; |
||||
|
||||
|
||||
XpathAssertions(TestRestClient.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 TestRestClient.BodyContentSpec isEqualTo(String expectedValue) { |
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isEqualTo(Double expectedValue) { |
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec isEqualTo(boolean expectedValue) { |
||||
return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec exists() { |
||||
return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset())); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec doesNotExist() { |
||||
return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset())); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, int)}. |
||||
*/ |
||||
public TestRestClient.BodyContentSpec nodeCount(int expectedCount) { |
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec string(Matcher<? super String> matcher){ |
||||
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec number(Matcher<? super Double> matcher){ |
||||
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher)); |
||||
} |
||||
|
||||
/** |
||||
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec nodeCount(Matcher<? super Integer> matcher){ |
||||
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher)); |
||||
} |
||||
|
||||
/** |
||||
* Consume the result of the XPath evaluation as a String. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.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. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.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. |
||||
* @since 5.1 |
||||
*/ |
||||
public TestRestClient.BodyContentSpec nodeCount(Consumer<Integer> consumer){ |
||||
return assertWith(() -> { |
||||
Integer value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Integer.class); |
||||
consumer.accept(value); |
||||
}); |
||||
} |
||||
|
||||
private TestRestClient.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; |
||||
|
||||
} |
||||
} |
Loading…
Reference in new issue