Browse Source

Adds HttpbinTestcontainers support

Retires HttpBinCompatibleController.java in favor of container.

See gh-2949
pull/3006/head
sgibb 1 year ago
parent
commit
2c6ba7d954
No known key found for this signature in database
GPG Key ID: 7788A47380690861
  1. 5
      spring-cloud-gateway-server-mvc/pom.xml
  2. 191
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java
  3. 34
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java
  4. 236
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpBinCompatibleController.java
  5. 99
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java
  6. 47
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java
  7. 17
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestAutoConfiguration.java
  8. 131
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java
  9. 10
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestFilterSupplier.java
  10. 26
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestLoadBalancerConfig.java
  11. 49
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java
  12. 11
      spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml
  13. 5
      spring-cloud-gateway-server-mvc/src/test/resources/application.yml

5
spring-cloud-gateway-server-mvc/pom.xml

@ -86,5 +86,10 @@ @@ -86,5 +86,10 @@
<artifactId>bucket4j-caffeine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

191
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

@ -42,8 +42,10 @@ import org.springframework.boot.test.web.server.LocalServerPort; @@ -42,8 +42,10 @@ import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver;
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
@ -55,6 +57,7 @@ import org.springframework.http.MediaType; @@ -55,6 +57,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
@ -84,14 +87,17 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti @@ -84,14 +87,17 @@ import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFuncti
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.cookie;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.header;
import static org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.host;
import static org.springframework.cloud.gateway.server.mvc.test.TestUtils.getMap;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import static org.springframework.web.servlet.function.RequestPredicates.GET;
import static org.springframework.web.servlet.function.RequestPredicates.POST;
import static org.springframework.web.servlet.function.RequestPredicates.path;
import static org.springframework.web.servlet.function.RouterFunctions.route;
@SuppressWarnings("unchecked")
@SpringBootTest(properties = {}, webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class ServerMvcIntegrationTests {
@LocalServerPort
@ -111,19 +117,9 @@ public class ServerMvcIntegrationTests { @@ -111,19 +117,9 @@ public class ServerMvcIntegrationTests {
@SuppressWarnings("rawtypes")
@Test
public void addRequestHeaderWorks() {
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");
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Foo", "Bar");
});
}
@ -132,9 +128,8 @@ public class ServerMvcIntegrationTests { @@ -132,9 +128,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/addrequestparam").exchange().expectStatus().isOk().expectBody(Map.class)
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
assertThat(map).isNotEmpty().containsKey("args");
Map<String, Object> args = (Map<String, Object>) map.get("args");
assertThat(args).containsEntry("param1", Collections.singletonList("param1val"));
Map<String, Object> args = getMap(map, "args");
assertThat(args).containsEntry("param1", "param1val");
});
}
@ -143,8 +138,7 @@ public class ServerMvcIntegrationTests { @@ -143,8 +138,7 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/removehopbyhoprequestheaders").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");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).doesNotContainKeys("x-application-context");
});
}
@ -154,9 +148,8 @@ public class ServerMvcIntegrationTests { @@ -154,9 +148,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/mycustompathextra1").exchange().expectStatus().isOk().expectBody(Map.class)
.consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
assertThat(map).isNotEmpty().containsKey("args");
Map<String, Object> args = (Map<String, Object>) map.get("args");
assertThat(args).containsEntry("param1", Collections.singletonList("param1valextra1"));
Map<String, Object> args = getMap(map, "args");
assertThat(args).containsEntry("param1", "param1valextra1");
});
}
@ -165,16 +158,16 @@ public class ServerMvcIntegrationTests { @@ -165,16 +158,16 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/long/path/to/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-test", "stripPrefix");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("X-Test", "stripPrefix");
});
}
@Test
public void setStatusGatewayRouterFunctionWorks() {
restClient.get().uri("/status/201").exchange().expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
.expectHeader().valueEquals("x-status", "201").expectBody(String.class).isEqualTo("Failed with 201");
.expectHeader().valueEquals("x-status", "201"); // .expectBody(String.class).isEqualTo("Failed
// with 201");
}
@Test
@ -182,8 +175,7 @@ public class ServerMvcIntegrationTests { @@ -182,8 +175,7 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/addresheader").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");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).doesNotContainKey("x-bar");
assertThat(res.getResponseHeaders()).containsEntry("x-bar", Collections.singletonList("val1"));
});
@ -191,8 +183,8 @@ public class ServerMvcIntegrationTests { @@ -191,8 +183,8 @@ public class ServerMvcIntegrationTests {
@Test
public void postWorks() {
restClient.post().uri("/post").bodyValue("Post Value").exchange().expectStatus().isOk().expectBody(Map.class)
.consumeWith(res -> {
restClient.post().uri("/post").bodyValue("Post Value").header("test", "post").exchange().expectStatus().isOk()
.expectBody(Map.class).consumeWith(res -> {
Map<String, Object> map = res.getResponseBody();
assertThat(map).isNotEmpty().containsEntry("data", "Post Value");
});
@ -203,9 +195,8 @@ public class ServerMvcIntegrationTests { @@ -203,9 +195,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/loadbalancer").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-test", "loadbalancer");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("X-Test", "loadbalancer");
});
}
@ -215,9 +206,8 @@ public class ServerMvcIntegrationTests { @@ -215,9 +206,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/hostpredicate").header("Host", host).exchange().expectStatus().isOk()
.expectHeader().valueEquals("X-SubDomain", "www1").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("host", host);
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("Host", host);
});
}
@ -250,9 +240,8 @@ public class ServerMvcIntegrationTests { @@ -250,9 +240,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/headerregex").header("X-MyHeader", "foo").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-myheader", "foo");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("X-Myheader", "foo");
});
}
@ -262,9 +251,8 @@ public class ServerMvcIntegrationTests { @@ -262,9 +251,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/cookieregex").cookie("mycookie", "foo").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("cookie", "mycookie=foo");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("Cookie", "mycookie=foo");
});
}
@ -273,9 +261,8 @@ public class ServerMvcIntegrationTests { @@ -273,9 +261,8 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/foo/get").header("Host", "www.rewritepath.org").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-test", "rewritepath");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsEntry("X-Test", "rewritepath");
});
}
@ -284,21 +271,20 @@ public class ServerMvcIntegrationTests { @@ -284,21 +271,20 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/headers").header("test", "forwarded").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).containsKeys(ForwardedRequestHeadersFilter.FORWARDED_HEADER.toLowerCase(),
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER.toLowerCase(),
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER.toLowerCase(),
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER.toLowerCase(),
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER.toLowerCase());
assertThat(headers.get(ForwardedRequestHeadersFilter.FORWARDED_HEADER.toLowerCase())).asString()
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).containsKeys(ForwardedRequestHeadersFilter.FORWARDED_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER,
XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER);
assertThat(headers.get(ForwardedRequestHeadersFilter.FORWARDED_HEADER)).asString()
.contains("proto=http").contains("host=\"localhost:").contains("for=\"127.0.0.1:");
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER.toLowerCase()))
.asString().isEqualTo("localhost:" + this.port);
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER.toLowerCase()))
.asString().isEqualTo(String.valueOf(this.port));
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER.toLowerCase())
.toString()).asString().isEqualTo("http");
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER)).asString()
.isEqualTo("localhost:" + this.port);
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER)).asString()
.isEqualTo(String.valueOf(this.port));
assertThat(headers.get(XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER).toString())
.asString().isEqualTo("http");
});
}
@ -318,9 +304,9 @@ public class ServerMvcIntegrationTests { @@ -318,9 +304,9 @@ public class ServerMvcIntegrationTests {
.expectStatus().isOk()
.expectBody(Map.class).consumeWith(result -> {
Map map = result.getResponseBody();
Map<String, Object> form = (Map<String, Object>) map.get("form");
assertThat(form).containsEntry("foo", Collections.singletonList("bar"));
assertThat(form).containsEntry("baz", Collections.singletonList("bam"));
Map<String, Object> form = getMap(map, "form");
assertThat(form).containsEntry("foo", "bar");
assertThat(form).containsEntry("baz", "bam");
});
// @formatter:on
}
@ -376,15 +362,14 @@ public class ServerMvcIntegrationTests { @@ -376,15 +362,14 @@ public class ServerMvcIntegrationTests {
restClient.get().uri("/anything/removerequestheader").header("X-Request-Foo", "Bar").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).doesNotContainKey("x-request-foo");
Map<String, Object> headers = getMap(map, "headers");
assertThat(headers).doesNotContainKey("X-Request-Foo");
});
}
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "testservice", configuration = TestLoadBalancerConfig.class)
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
protected static class TestConfiguration {
@Bean
@ -412,9 +397,8 @@ public class ServerMvcIntegrationTests { @@ -412,9 +397,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddReqHeader() {
// @formatter:off
return route(GET("/get"), http())
.filter(new LocalServerPortUriResolver())
.filter(new HttpbinUriResolver())
.filter(addRequestHeader("X-Foo", "Bar"))
.filter(prefixPath("/httpbin"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testaddreqheader");
// @formatter:on
}
@ -423,14 +407,12 @@ public class ServerMvcIntegrationTests { @@ -423,14 +407,12 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetStatusAndAddRespHeader() {
// @formatter:off
return (RouterFunction<ServerResponse>) route().GET("/status/{status}", http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(setStatus(HttpStatus.TOO_MANY_REQUESTS))
.filter(addResponseHeader("X-Status", "{status}"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testsetstatus")
.build().andOther(route().GET("/anything/addresheader", http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(addResponseHeader("X-Bar", "val1"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testaddresponseheader")
.build());
@ -441,8 +423,7 @@ public class ServerMvcIntegrationTests { @@ -441,8 +423,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsAddRequestParam() {
// @formatter:off
return route(GET("/anything/addrequestparam"), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(addRequestParameter("param1", "param1val"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testaddrequestparam");
// @formatter:on
@ -452,8 +433,8 @@ public class ServerMvcIntegrationTests { @@ -452,8 +433,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsSetPath() {
// @formatter:off
return route(GET("/mycustompath{extra}"), http())
.filter(new LocalServerPortUriResolver())
.filter(setPath("/httpbin/anything/mycustompath{extra}"))
.filter(new HttpbinUriResolver())
.filter(setPath("/anything/mycustompath{extra}"))
.filter(addRequestParameter("param1", "param1val{extra}"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testsetpath");
// @formatter:on
@ -463,8 +444,7 @@ public class ServerMvcIntegrationTests { @@ -463,8 +444,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripPrefix() {
// @formatter:off
return route(GET("/long/path/to/get"), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(stripPrefix(3))
.filter(addRequestHeader("X-Test", "stripPrefix"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "teststripprefix");
@ -475,8 +455,7 @@ public class ServerMvcIntegrationTests { @@ -475,8 +455,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsRemoveHopByHopRequestHeaders() {
// @formatter:off
return route(GET("/anything/removehopbyhoprequestheaders"), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(addRequestHeader("x-application-context", "context-id1"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testremovehopbyhopheaders");
// @formatter:on
@ -485,12 +464,9 @@ public class ServerMvcIntegrationTests { @@ -485,12 +464,9 @@ public class ServerMvcIntegrationTests {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsPost() {
// @formatter:off
return route()
.POST("/post", http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testpost")
.build();
return route(POST("/post").and(header("test", "post")), http())
.filter(new HttpbinUriResolver())
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testpost");
// @formatter:on
}
@ -499,8 +475,7 @@ public class ServerMvcIntegrationTests { @@ -499,8 +475,7 @@ public class ServerMvcIntegrationTests {
// @formatter:off
return route()
.GET("/anything/loadbalancer", http())
.filter(lb("testservice"))
.filter(prefixPath("/httpbin"))
.filter(lb("httpbin"))
.filter(addRequestHeader("X-Test", "loadbalancer"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testloadbalancer")
.build();
@ -511,8 +486,7 @@ public class ServerMvcIntegrationTests { @@ -511,8 +486,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsHost() {
// @formatter:off
return route(host("{sub}.myjavadslhost.com").and(path("/anything/hostpredicate")), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(preserveHost())
.filter(addResponseHeader("X-SubDomain", "{sub}"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testhostpredicate");
@ -524,7 +498,6 @@ public class ServerMvcIntegrationTests { @@ -524,7 +498,6 @@ public class ServerMvcIntegrationTests {
// @formatter:off
return route(path("/anything/circuitbreakerfallback"), http(URI.create("https://nonexistantdomain.com1234")))
.filter(circuitBreaker("mycb1", "/hello"))
.filter(prefixPath("/httpbin"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testcircuitbreakerfallback");
// @formatter:on
}
@ -533,9 +506,9 @@ public class ServerMvcIntegrationTests { @@ -533,9 +506,9 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsCircuitBreakerNoFallback() {
// @formatter:off
return route(path("/anything/circuitbreakernofallback"), http())
.filter(new LocalServerPortUriResolver())
.filter(new HttpbinUriResolver())
.filter(circuitBreaker("mycb1", null))
.filter(setPath("/httpbin/delay/5"))
.filter(setPath("/delay/5"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testcircuitbreakernofallback");
// @formatter:on
}
@ -546,7 +519,7 @@ public class ServerMvcIntegrationTests { @@ -546,7 +519,7 @@ public class ServerMvcIntegrationTests {
return route(path("/retry"), http())
.filter(new LocalServerPortUriResolver())
.filter(retry(3))
.filter(prefixPath("/httpbin"))
.filter(prefixPath("/do"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testretry");
// @formatter:on
}
@ -555,12 +528,11 @@ public class ServerMvcIntegrationTests { @@ -555,12 +528,11 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsRateLimit() {
// @formatter:off
return route(GET("/anything/ratelimit"), http())
.filter(new LocalServerPortUriResolver())
.filter(new HttpbinUriResolver())
//.filter(rateLimit(1, Duration.ofMinutes(1), request -> "ratelimittest1min"))
.filter(rateLimit(c -> c.setCapacity(1)
.setPeriod(Duration.ofMinutes(1))
.setKeyResolver(request -> "ratelimitttest1min")))
.filter(prefixPath("/httpbin"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testratelimit");
// @formatter:on
}
@ -569,8 +541,8 @@ public class ServerMvcIntegrationTests { @@ -569,8 +541,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsHeaderPredicate() {
// @formatter:off
return route(path("/headerregex").and(header("X-MyHeader", "fo.")), http())
.filter(new LocalServerPortUriResolver())
.filter(setPath("/httpbin/headers"))
.filter(new HttpbinUriResolver())
.filter(setPath("/headers"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testheaderpredicate");
// @formatter:on
}
@ -579,8 +551,8 @@ public class ServerMvcIntegrationTests { @@ -579,8 +551,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsCookiePredicate() {
// @formatter:off
return route(path("/cookieregex").and(cookie("mycookie", "fo.")), http())
.filter(new LocalServerPortUriResolver())
.filter(setPath("/httpbin/headers"))
.filter(new HttpbinUriResolver())
.filter(setPath("/headers"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testcookiepredicate");
// @formatter:on
}
@ -589,8 +561,8 @@ public class ServerMvcIntegrationTests { @@ -589,8 +561,8 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsRewritePath() {
// @formatter:off
return route(path("/foo/**").and(host("**.rewritepath.org")), http())
.filter(new LocalServerPortUriResolver())
.filter(rewritePath("/foo/(?<segment>.*)", "/httpbin/${segment}"))
.filter(new HttpbinUriResolver())
.filter(rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.filter(addRequestHeader("X-Test", "rewritepath"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testrewritepath");
// @formatter:on
@ -600,8 +572,7 @@ public class ServerMvcIntegrationTests { @@ -600,8 +572,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsForwardedHeaders() {
// @formatter:off
return route(path("/headers").and(header("test", "forwarded")), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(addRequestHeader("X-Test", "forwarded"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testforwardedheaders");
// @formatter:on
@ -610,11 +581,14 @@ public class ServerMvcIntegrationTests { @@ -610,11 +581,14 @@ public class ServerMvcIntegrationTests {
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsForm() {
// @formatter:off
return route(path("/post").and(header("test", "form")), http())
return route()
.POST("/post", header("test", "form"), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(prefixPath("/test"))
.filter(addRequestHeader("X-Test", "form"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testform");
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testform")
.build();
// @formatter:on
}
@ -631,8 +605,7 @@ public class ServerMvcIntegrationTests { @@ -631,8 +605,7 @@ public class ServerMvcIntegrationTests {
public RouterFunction<ServerResponse> gatewayRouterFunctionsRemoveRequestHeader() {
// @formatter:off
return route(path("/anything/removerequestheader"), http())
.filter(new LocalServerPortUriResolver())
.filter(prefixPath("/httpbin"))
.filter(new HttpbinUriResolver())
.filter(removeRequestHeader("X-Request-Foo"))
.withAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "testremoverequestheader");
// @formatter:on
@ -647,7 +620,7 @@ public class ServerMvcIntegrationTests { @@ -647,7 +620,7 @@ public class ServerMvcIntegrationTests {
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
@GetMapping("/httpbin/retry")
@GetMapping("/do/retry")
public ResponseEntity<String> retry(@RequestParam("key") String key,
@RequestParam(name = "count", defaultValue = "3") int count,
@RequestParam(name = "failStatus", required = false) Integer failStatus) {

34
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java

@ -32,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -32,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
@ -40,6 +41,7 @@ import org.springframework.context.ConfigurableApplicationContext; @@ -40,6 +41,7 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RequestPredicates;
@ -48,9 +50,11 @@ import org.springframework.web.servlet.function.RouterFunctions; @@ -48,9 +50,11 @@ import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.test.TestUtils.getMap;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("propertiesbeandefinitionregistrartests")
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
@Autowired
@ -153,10 +157,8 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests { @@ -153,10 +157,8 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
public void configuredRouteWorks() {
restClient.get().uri("/anything/listRoute1").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-test", "listRoute1");
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Test", "listRoute1");
});
}
@ -165,10 +167,8 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests { @@ -165,10 +167,8 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
public void lbRouteWorks() {
restClient.get().uri("/anything/listRoute3").header("MyHeaderName", "MyHeaderVal").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-test", "listRoute3");
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Test", "listRoute3");
});
}
@ -180,21 +180,17 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests { @@ -180,21 +180,17 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
TestPropertyValues.of("spring.cloud.gateway.mvc.routesMap.route3.uri=https://example3.com",
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].name=Path",
"spring.cloud.gateway.mvc.routesMap.route3.predicates[0].args.pattern=/anything/mapRoute3",
"spring.cloud.gateway.mvc.routesMap.route3.filters[0].Name=LocalServerPortUriResolver",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].Name=PrefixPath",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].args.prefix=/httpbin",
"spring.cloud.gateway.mvc.routesMap.route3.filters[2].Name=AddRequestHeader",
"spring.cloud.gateway.mvc.routesMap.route3.filters[2].args.name=X-Test",
"spring.cloud.gateway.mvc.routesMap.route3.filters[2].args.values=mapRoute3").applyTo(context);
"spring.cloud.gateway.mvc.routesMap.route3.filters[0].Name=HttpbinUriResolver",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].Name=AddRequestHeader",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].args.name=X-Test",
"spring.cloud.gateway.mvc.routesMap.route3.filters[1].args.values=mapRoute3").applyTo(context);
ContextRefresher contextRefresher = context.getBean(ContextRefresher.class);
contextRefresher.refresh();
// make http call before getRouterFunction()
restClient.get().uri("/anything/mapRoute3").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-test", "mapRoute3");
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
assertThat(headers).containsEntry("X-Test", "mapRoute3");
});
GatewayMvcProperties properties = context.getBean(GatewayMvcProperties.class);
@ -205,7 +201,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests { @@ -205,7 +201,7 @@ public class GatewayMvcPropertiesBeanDefinitionRegistrarTests {
@SpringBootConfiguration
@EnableAutoConfiguration
@LoadBalancerClient(name = "testservice", configuration = TestLoadBalancerConfig.class)
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
static class Config {
}

236
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpBinCompatibleController.java

@ -1,236 +0,0 @@ @@ -1,236 +0,0 @@
/*
* Copyright 2013-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.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
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.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
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 Map<String, Object> delay(HttpServletRequest request, @PathVariable int sec) throws InterruptedException {
int delay = Math.min(sec, 10) * 1000;
Thread.sleep(delay);
return get(request);
}
@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 Map<String, Object> postFormData(HttpServletRequest request,
@RequestParam MultiValueMap<String, MultipartFile> parts) throws ServletException, IOException {
Collection<Part> parts1 = request.getParts();
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
HashMap<String, Object> files = new HashMap<>();
ret.put("files", files);
// StringDecoder decoder = StringDecoder.allMimeTypes(true);
parts.values().stream().flatMap(List::stream).forEach(part -> {
String contentType = part.getContentType();
long contentLength = part.getSize();
// TODO: get part data
files.put(part.getName(), "data:" + contentType + ";base64," + contentLength);
});
return ret;
}
@PostMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> postUrlEncoded(HttpServletRequest request,
@RequestBody(required = false) MultiValueMap form) throws IOException {
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
ret.put("form", form);
return ret;
}
@PostMapping(path = "/post", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> post(HttpServletRequest request, @RequestBody(required = false) String body) {
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
ret.put("data", body);
HashMap<String, Object> form = new HashMap<>();
ret.put("form", form);
return ret;
// 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();
}
}

99
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinTestcontainers.java

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
/*
* Copyright 2013-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.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.utility.DockerImageName;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
public class HttpbinTestcontainers implements ApplicationContextInitializer<ConfigurableApplicationContext> {
// https://github.com/mccutchen/go-httpbin
// https://hub.docker.com/r/mccutchen/go-httpbin
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mccutchen/go-httpbin");
static final Logger logger = LoggerFactory.getLogger(HttpbinTestcontainers.class);
/**
* Default httpbin port.
*/
public static final int DEFAULT_PORT = 8080;
/**
* Shared httpbin container.
*/
public static GenericContainer<?> container = createContainer();
public static GenericContainer<?> createContainer() {
return new GenericContainer<>(DEFAULT_IMAGE_NAME).withExposedPorts(DEFAULT_PORT)
.waitingFor(new HttpWaitStrategy().forPort(DEFAULT_PORT));
}
@Override
public void initialize(ConfigurableApplicationContext context) {
start();
MutablePropertySources sources = context.getEnvironment().getPropertySources();
if (!sources.contains("httpbinTestcontainer")) {
boolean running = container.isRunning();
Integer mappedPort = container.getMappedPort(DEFAULT_PORT);
HashMap<String, Object> map = new HashMap<>();
map.put("httpbin.port", String.valueOf(mappedPort));
map.put("httpbin.host", container.getHost());
sources.addFirst(new MapPropertySource("httpbinTestcontainer", map));
}
}
public static void start() {
if (!container.isRunning()) {
container.start();
}
}
public static Integer getPort() {
if (!container.isRunning()) {
throw new IllegalStateException("httpbin Testcontainer is not running");
}
return container.getMappedPort(DEFAULT_PORT);
}
public static String getHost() {
if (!container.isRunning()) {
throw new IllegalStateException("httpbin Testcontainer is not running");
}
return container.getHost();
}
public static void initializeSystemProperties() {
start();
System.setProperty("httpbin.port", getPort().toString());
System.setProperty("httpbin.host", getHost());
}
}

47
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/HttpbinUriResolver.java

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
/*
* Copyright 2013-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 org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchangeHandlerFunction;
import org.springframework.context.ApplicationContext;
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
public class HttpbinUriResolver
implements ProxyExchangeHandlerFunction.URIResolver, HandlerFilterFunction<ServerResponse, ServerResponse> {
@Override
public URI apply(ServerRequest request) {
ApplicationContext context = MvcUtils.getApplicationContext(request);
Integer port = context.getEnvironment().getProperty("httpbin.port", Integer.class);
String host = context.getEnvironment().getProperty("httpbin.host");
return URI.create(String.format("http://%s:%d", host, port));
}
@Override
public ServerResponse filter(ServerRequest request, HandlerFunction<ServerResponse> next) throws Exception {
URI uri = apply(request);
MvcUtils.setRequestUrl(request, uri);
return next.handle(request);
}
}

17
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestAutoConfiguration.java

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.cloud.gateway.server.mvc.test;
import java.util.List;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateCustomizer;
@ -24,6 +26,8 @@ import org.springframework.cloud.gateway.server.mvc.test.client.DefaultTestRestC @@ -24,6 +26,8 @@ import org.springframework.cloud.gateway.server.mvc.test.client.DefaultTestRestC
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestFactory;
@AutoConfiguration(after = GatewayServerMvcAutoConfiguration.class)
@ -31,7 +35,14 @@ public class TestAutoConfiguration { @@ -31,7 +35,14 @@ public class TestAutoConfiguration {
@Bean
RestTemplateCustomizer testRestClientRestTemplateCustomizer(ApplicationContext context) {
return restTemplate -> restTemplate.setRequestFactory(context.getBean(ClientHttpRequestFactory.class));
return restTemplate -> {
restTemplate.setRequestFactory(context.getBean(ClientHttpRequestFactory.class));
restTemplate.setClientHttpRequestInitializers(List.of(request -> {
if (!request.getHeaders().containsKey(HttpHeaders.ACCEPT)) {
request.getHeaders().setAccept(List.of(MediaType.ALL));
}
}));
};
}
@Bean
@ -41,8 +52,8 @@ public class TestAutoConfiguration { @@ -41,8 +52,8 @@ public class TestAutoConfiguration {
}
@Bean
public HttpBinCompatibleController httpBinCompatibleController() {
return new HttpBinCompatibleController();
public TestController testController() {
return new TestController();
}
}

131
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestController.java

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
/*
* Copyright 2013-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.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ServerWebExchange;
@RestController
@RequestMapping("/test")
public class TestController {
private static final String HEADER_REQ_VARY = "X-Request-Vary";
@GetMapping("/")
public String home() {
return "test controller home";
}
@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;
}
@PostMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> postFormData(HttpServletRequest request,
@RequestParam MultiValueMap<String, MultipartFile> parts) throws ServletException, IOException {
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
HashMap<String, Object> files = new HashMap<>();
ret.put("files", files);
parts.values().stream().flatMap(List::stream).forEach(part -> {
String contentType = part.getContentType();
long contentLength = part.getSize();
// TODO: get part data
files.put(part.getName(), "data:" + contentType + ";base64," + contentLength);
});
return ret;
}
@PostMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> postUrlEncoded(HttpServletRequest request,
@RequestBody(required = false) MultiValueMap form) throws IOException {
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
ret.put("form", form);
return ret;
}
@PostMapping(path = "/post", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> post(HttpServletRequest request, @RequestBody(required = false) String body) {
HashMap<String, Object> ret = new HashMap<>();
ret.put("headers", getHeaders(request));
ret.put("data", body);
HashMap<String, Object> form = new HashMap<>();
ret.put("form", form);
return ret;
}
@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);
Map<String, Object> result = new HashMap<>();
result.put("headers", getHeaders(request));
return builder.body(result);
}
}
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;
}
}

10
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestFilterSupplier.java

@ -17,11 +17,10 @@ @@ -17,11 +17,10 @@
package org.springframework.cloud.gateway.server.mvc.test;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.cloud.gateway.server.mvc.filter.FilterSupplier;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.function.HandlerFilterFunction;
import org.springframework.web.servlet.function.ServerResponse;
@ -29,12 +28,15 @@ public class TestFilterSupplier implements FilterSupplier { @@ -29,12 +28,15 @@ public class TestFilterSupplier implements FilterSupplier {
@Override
public Collection<Method> get() {
return Collections
.singleton(ReflectionUtils.findMethod(TestFilterSupplier.class, "localServerPortUriResolver"));
return Arrays.asList(TestFilterSupplier.class.getMethods());
}
public static HandlerFilterFunction<ServerResponse, ServerResponse> localServerPortUriResolver() {
return new LocalServerPortUriResolver();
}
public static HandlerFilterFunction<ServerResponse, ServerResponse> httpbinUriResolver() {
return new HttpbinUriResolver();
}
}

26
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestLoadBalancerConfig.java

@ -24,13 +24,27 @@ import org.springframework.context.annotation.Bean; @@ -24,13 +24,27 @@ import org.springframework.context.annotation.Bean;
public class TestLoadBalancerConfig {
@LocalServerPort
protected int port = 0;
public static class Httpbin {
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("httpbin", new DefaultServiceInstance("httpbin" + "-1", "httpbin",
HttpbinTestcontainers.getHost(), HttpbinTestcontainers.getPort(), false));
}
}
public static class Local {
@LocalServerPort
protected int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("testservice",
new DefaultServiceInstance("testservice" + "-1", "testservice", "localhost", port, false));
}
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("testservice",
new DefaultServiceInstance("testservice" + "-1", "testservice", "localhost", port, false));
}
}

49
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
/*
* Copyright 2013-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.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestUtils {
public static Map<String, Object> getMap(Map<String, Object> map, String mapKey) {
assertThat(map).isNotEmpty().containsKey(mapKey);
Map<String, Object> headers = (Map<String, Object>) map.get(mapKey);
Map<String, Object> newHeaders = new HashMap<>();
// flatten map as mccutchen/go-httpbin only gets lists
headers.forEach((key, value) -> {
if (value instanceof List<?> values) {
if (values.size() == 1) {
newHeaders.put(key, values.get(0));
}
else {
newHeaders.put(key, values);
}
}
else {
newHeaders.put(key, value);
}
});
return newHeaders;
}
}

11
spring-cloud-gateway-server-mvc/src/test/resources/application-propertiesbeandefinitionregistrartests.yml

@ -23,8 +23,7 @@ spring.cloud.gateway.mvc: @@ -23,8 +23,7 @@ spring.cloud.gateway.mvc:
args:
pattern: /anything/listRoute1
filters:
- LocalServerPortUriResolver=
- PrefixPath=/httpbin
- HttpbinUriResolver=
- AddRequestHeader=X-Test,listRoute1
- id: listRoute2
uri: https://examplel2.com
@ -32,18 +31,14 @@ spring.cloud.gateway.mvc: @@ -32,18 +31,14 @@ spring.cloud.gateway.mvc:
- Method=GET,POST
- Path=/anything/listRoute2
filters:
- LocalServerPortUriResolver=
- PrefixPath=/httpbin
- HttpbinUriResolver=
- AddRequestHeader=X-Test,listRoute2
- id: listRoute3
uri: lb://testservice
uri: lb://httpbin
predicates:
- Path=/anything/listRoute3
- Header=MyHeaderName,MyHeader.*
filters:
- name: PrefixPath
args:
prefix: /httpbin
- name: AddRequestHeader
args:
name: X-Test

5
spring-cloud-gateway-server-mvc/src/test/resources/application.yml

@ -3,4 +3,7 @@ logging: @@ -3,4 +3,7 @@ logging:
org.apache.hc.client5.http: DEBUG
org.apache.hc.client5.http.wire: DEBUG
org.springframework.web: TRACE
org.springframework.retry: TRACE
org.springframework.retry: TRACE
spring:
mvc:
log-request-details: true
Loading…
Cancel
Save