+ * If `timeToLive` and `size` are null, a global cache is used configured by the
+ * global configuration
+ * {@link org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties}.
+ * @param timeToLive time an entry is kept in cache. Default: 5 minutes
+ * @param size size expression to limit cache size (See format in {@link DataSize}.
+ * Default: {@code null} (no limit)
+ * @return a {@link GatewayFilterSpec} that can be used to apply additional filters
+ */
+ public GatewayFilterSpec localResponseCache(Duration timeToLive, DataSize size) {
+ return filter(getBean(LocalResponseCacheGatewayFilterFactory.class)
+ .apply(c -> c.setTimeToLive(timeToLive).setSize(size)));
+ }
+
/**
* A filter that removes duplication on a response header before it is returned to the
* client by the Gateway.
diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index c6062513..a4c1387e 100644
--- a/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -36,6 +36,12 @@
"description": "Enables the modify-request-body filter.",
"defaultValue": "true"
},
+ {
+ "name": "spring.cloud.gateway.filter.local-response-cache.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables the local-response-cache filter.",
+ "defaultValue": "true"
+ },
{
"name": "spring.cloud.gateway.filter.dedupe-response-header.enabled",
"type": "java.lang.Boolean",
diff --git a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 10ced6f9..2c486c7c 100644
--- a/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-cloud-gateway-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -7,4 +7,5 @@ org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration
-org.springframework.cloud.gateway.config.GatewayReactiveOAuth2AutoConfiguration
\ No newline at end of file
+org.springframework.cloud.gateway.config.GatewayReactiveOAuth2AutoConfiguration
+org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration
\ No newline at end of file
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java
index 43cee407..2e70bf00 100644
--- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java
@@ -78,13 +78,14 @@ public class DisableBuiltInFiltersTests {
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Config.class,
- properties = { "spring.cloud.gateway.filter.add-request-header.enabled=false",
+ properties = {"spring.cloud.gateway.filter.add-request-header.enabled=false",
"spring.cloud.gateway.filter.map-request-header.enabled=false",
"spring.cloud.gateway.filter.add-request-headers-if-not-present.enabled=false",
"spring.cloud.gateway.filter.add-request-parameter.enabled=false",
"spring.cloud.gateway.filter.add-response-header.enabled=false",
"spring.cloud.gateway.filter.json-to-grpc.enabled=false",
"spring.cloud.gateway.filter.modify-request-body.enabled=false",
+ "spring.cloud.gateway.filter.local-response-cache.enabled=false",
"spring.cloud.gateway.filter.dedupe-response-header.enabled=false",
"spring.cloud.gateway.filter.modify-response-body.enabled=false",
"spring.cloud.gateway.filter.prefix-path.enabled=false",
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java
index 0db80831..db55ccb0 100644
--- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInGlobalFiltersTests.java
@@ -59,8 +59,8 @@ public class DisableBuiltInGlobalFiltersTests {
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Config.class,
- properties = { "spring.cloud.gateway.global-filter.remove-cached-body.enabled=false",
- "spring.cloud.gateway.global-filter.route-to-request-url.enabled=false" })
+ properties = {"spring.cloud.gateway.global-filter.remove-cached-body.enabled=false",
+ "spring.cloud.gateway.global-filter.route-to-request-url.enabled=false"})
@ActiveProfiles("disable-components")
public static class DisableSpecificsFiltersByProperty {
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java
new file mode 100644
index 00000000..b22e9e2b
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CacheKeyGeneratorTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2013-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.filter.factory.cache;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+
+/**
+ * @author Ignacio Lozano
+ */
+class CacheKeyGeneratorTest {
+
+ final CacheKeyGenerator cacheKeyGenerator = new CacheKeyGenerator();
+
+ @Test
+ public void shouldGenerateSameKeyForSameUri() {
+ MockServerHttpRequest request1 = MockServerHttpRequest.get("http://this").build();
+ MockServerHttpRequest request2 = MockServerHttpRequest.get("http://this").build();
+
+ var key1 = cacheKeyGenerator.generateKey(request1);
+ var key2 = cacheKeyGenerator.generateKey(request2);
+
+ assertThat(key1).isEqualTo(key2);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenAuthAreDifferent() {
+ var uri = "https://this";
+
+ var requestWithoutAuth = MockServerHttpRequest.get(uri).build();
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put(AUTHORIZATION, List.of("my-token"));
+ var requestWithAuth = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+
+ var keyWithoutAuth = cacheKeyGenerator.generateKey(requestWithoutAuth);
+ var keyWithAuth = cacheKeyGenerator.generateKey(requestWithAuth);
+
+ assertThat(keyWithAuth).isNotEqualTo(keyWithoutAuth);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenCookiesAreDifferent() {
+ var httpHeaders = new HttpHeaders();
+ var uri = "https://this";
+
+ var requestWithoutCookies = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ var cookies = new HttpCookie[] { new HttpCookie("user", "my-first-cookie") };
+ var requestWithCookies = MockServerHttpRequest.get(uri).headers(httpHeaders).cookie(cookies).build();
+
+ var keyWithoutCookies = cacheKeyGenerator.generateKey(requestWithoutCookies);
+ var keyWithCookies = cacheKeyGenerator.generateKey(requestWithCookies);
+
+ assertThat(keyWithoutCookies).isNotEqualTo(keyWithCookies);
+ }
+
+ @Test
+ void shouldGenerateSameKeyWhenSameAuthAndCookieArePresent() {
+ var uri = "https://this";
+ var cookies = new HttpCookie[] { new HttpCookie("user", "my-first-cookie") };
+ var httpHeaders = new HttpHeaders();
+ httpHeaders.put(AUTHORIZATION, List.of("my-token"));
+
+ var request1 = MockServerHttpRequest.get(uri).headers(httpHeaders).cookie(cookies).build();
+ var request2 = MockServerHttpRequest.get(uri).headers(httpHeaders).cookie(cookies).build();
+
+ var key1 = cacheKeyGenerator.generateKey(request1);
+ var key2 = cacheKeyGenerator.generateKey(request2);
+
+ assertThat(key1).isEqualTo(key2);
+ }
+
+ @Test
+ void shouldGenerateSameKeyWhenVaryHeadersAreEqual() {
+ final String varyHeader = "X-MY-VARY";
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE1"));
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ HttpHeaders httpHeaders2 = new HttpHeaders();
+ httpHeaders2.put(varyHeader, List.of("VALUE1"));
+ var withSecondVary = MockServerHttpRequest.get(uri).headers(httpHeaders2).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, varyHeader);
+ var keyWithSecondVary = cacheKeyGenerator.generateKey(withSecondVary, varyHeader);
+
+ assertThat(keyWithFirstVary).isEqualTo(keyWithSecondVary);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenVaryHeadersAreDifferent() {
+ final String varyHeader = "X-MY-VARY";
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE1"));
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ HttpHeaders httpHeaders2 = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE2"));
+ var withSecondVary = MockServerHttpRequest.get(uri).headers(httpHeaders2).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, varyHeader);
+ var keyWithSecondVary = cacheKeyGenerator.generateKey(withSecondVary, varyHeader);
+
+ assertThat(keyWithFirstVary).isNotEqualTo(keyWithSecondVary);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenVaryHeaderIsMissingInSecondRequest() {
+ final String varyHeader = "X-MY-VARY";
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE1"));
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ var withSecondVary = MockServerHttpRequest.get(uri).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, varyHeader);
+ var keyWithSecondVary = cacheKeyGenerator.generateKey(withSecondVary, varyHeader);
+
+ assertThat(keyWithFirstVary).isNotEqualTo(keyWithSecondVary);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenOneOfMultipleVaryHeadersIsDifferent() {
+ final String varyHeader = "X-MY-VARY";
+ String varyHeader2 = "X-MY-SEC-VARY";
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE1"));
+ httpHeaders.put(varyHeader2, List.of("VALUE1"));
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ HttpHeaders httpHeaders2 = new HttpHeaders();
+ httpHeaders.put(varyHeader, List.of("VALUE2"));
+ var withSecondVary = MockServerHttpRequest.get(uri).headers(httpHeaders2).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, varyHeader);
+ var keyWithSecondVary = cacheKeyGenerator.generateKey(withSecondVary, varyHeader);
+
+ assertThat(keyWithFirstVary).isNotEqualTo(keyWithSecondVary);
+ }
+
+ @Test
+ void shouldGenerateDifferentKeyWhenHeadersAreDifferentButValuesAreTheSame() {
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put("X-MY-VARY-1", List.of("VALUE1"));
+ httpHeaders.put("X-MY-VARY-2", List.of("VALUE2"));
+
+ HttpHeaders httpHeaders2 = new HttpHeaders();
+ httpHeaders2.put("X-MY-VARY-3", List.of("VALUE1"));
+ httpHeaders2.put("X-MY-VARY-4", List.of("VALUE2"));
+
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ var withSecondVary = MockServerHttpRequest.get(uri).headers(httpHeaders2).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, "X-MY-VARY-1", "X-MY-VARY-2");
+ var keyWithSecondVary = cacheKeyGenerator.generateKey(withSecondVary, "X-MY-VARY-3", "X-MY-VARY-4");
+
+ assertThat(keyWithFirstVary).isNotEqualTo(keyWithSecondVary);
+ }
+
+ @Test
+ void whenHeaderHasEmptyValue() {
+ var uri = "https://this";
+
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.put("X-MY-VARY-1", List.of(""));
+
+ var withFirstVary = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+ var withoutVaryHeader = MockServerHttpRequest.get(uri).build();
+
+ var keyWithFirstVary = cacheKeyGenerator.generateKey(withFirstVary, "X-MY-VARY-1");
+ var keyWithoutVary = cacheKeyGenerator.generateKey(withFirstVary, "X-MY-VARY-1");
+
+ assertThat(keyWithoutVary).isEqualTo(keyWithFirstVary);
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponseTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponseTest.java
new file mode 100644
index 00000000..bf55d1cb
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponseTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2013-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.filter.factory.cache;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.zip.GZIPOutputStream;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Ignacio Lozano
+ */
+class CachedResponseTest {
+
+ @Test
+ void bodyAsByteArray_whenEmptyBody() throws IOException {
+ CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).build();
+
+ byte[] asByteArray = cachedResponse.bodyAsByteArray();
+
+ assertThat(asByteArray).isEmpty();
+ }
+
+ @Test
+ void bodyAsByteArray_whenThereIsContent() throws IOException {
+ String body = "example";
+ CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).body(body).build();
+
+ byte[] asByteArray = cachedResponse.bodyAsByteArray();
+
+ assertThat(asByteArray).isEqualTo(body.getBytes());
+ }
+
+ @Test
+ void bodyAsString_whenEmptyBody() throws IOException {
+ CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).build();
+
+ String asString = cachedResponse.bodyAsString();
+
+ assertThat(asString).isEmpty();
+ }
+
+ @Test
+ void bodyAsString_whenThereIsContent() throws IOException {
+ String body = "example";
+ CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).body(body).build();
+
+ String asString = cachedResponse.bodyAsString();
+
+ assertThat(asString).isEqualTo(body);
+ }
+
+ @Test
+ void bodyAsString_whenThereIsGZipContent() throws IOException {
+ String body = "example";
+ CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK)
+ .header(HttpHeaders.CONTENT_ENCODING, "gzip").appendToBody(convertToGzip(body)).build();
+
+ String asString = cachedResponse.bodyAsString();
+
+ assertThat(asString).isEqualTo(body);
+ }
+
+ private ByteBuffer convertToGzip(String str) throws IOException {
+ var outBytes = new ByteArrayOutputStream();
+ var outGzip = new GZIPOutputStream(outBytes);
+ outGzip.write(str.getBytes());
+ outGzip.close();
+ return ByteBuffer.wrap(outBytes.toByteArray());
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java
new file mode 100644
index 00000000..51df60a2
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/LocalResponseCacheGatewayFilterFactoryTests.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2013-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.filter.factory.cache;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.cloud.gateway.test.BaseWebClientTests;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.CacheControl;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.util.StringUtils;
+import org.springframework.util.unit.DataSize;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+/**
+ * @author Ignacio Lozano
+ */
+@SpringBootTest(webEnvironment = RANDOM_PORT)
+@DirtiesContext
+@ActiveProfiles(profiles = "local-cache-filter")
+public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests {
+
+ private static final String CUSTOM_HEADER = "X-Custom-Date";
+
+ @Test
+ void shouldNotCacheResponseWhenRouteDoesNotHaveFilter() {
+ String uri = "/" + UUID.randomUUID() + "/no-cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenGetRequestHasBody() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.method(HttpMethod.GET).uri(uri).header("Host", "www.localresponsecache.org")
+ .header(CUSTOM_HEADER, "1").bodyValue("whatever").exchange().expectBody()
+ .jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.method(HttpMethod.GET).uri(uri).header("Host", "www.localresponsecache.org").bodyValue("whatever")
+ .header(CUSTOM_HEADER, "2").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER)
+ .isEqualTo("2");
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenPostRequestHasBody() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.method(HttpMethod.POST).uri(uri).header("Host", "www.localresponsecache.org")
+ .header(CUSTOM_HEADER, "1").bodyValue("whatever").exchange().expectBody()
+ .jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.method(HttpMethod.POST).uri(uri).header("Host", "www.localresponsecache.org").bodyValue("whatever")
+ .header(CUSTOM_HEADER, "2").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER)
+ .isEqualTo("2");
+ }
+
+ @Test
+ void shouldNotCacheWhenCacheControlAsksToDoNotCache() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2")
+ // Cache-Control asks to not use the cached content and not store the
+ // response
+ .header(HttpHeaders.CACHE_CONTROL, CacheControl.noStore().getHeaderValue()).exchange().expectBody()
+ .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
+ }
+
+ @Test
+ void shouldCacheAndReturnNotModifiedStatusWhenCacheControlIsNoCache() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2")
+ // Cache-Control asks to not return cached content because it is
+ // HttpHeaders.NotModified
+ .header(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue()).exchange().expectStatus()
+ .isNotModified().expectBody().isEmpty();
+ }
+
+ @Test
+ void shouldCacheResponseWhenOnlyNonVaryHeaderIsDifferent() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER)
+ .value(customHeaderFromReq1 -> testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
+ .header(CUSTOM_HEADER, "2").exchange().expectBody()
+ .jsonPath("$.headers." + CUSTOM_HEADER, customHeaderFromReq1));
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenVaryHeaderIsDifferent() {
+ String varyHeader = HttpHeaders.ORIGIN;
+ String sameUri = "/" + UUID.randomUUID() + "/cache/vary-on-header";
+ String firstNonVary = "1";
+ String secondNonVary = "2";
+ assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-1", CUSTOM_HEADER, firstNonVary, firstNonVary);
+ assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-1", CUSTOM_HEADER, secondNonVary, firstNonVary);
+ assertNonVaryHeaderInContent(sameUri, varyHeader, "origin-2", CUSTOM_HEADER, secondNonVary, secondNonVary);
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenResponseVaryIsWildcard() {
+ String uri = "/" + UUID.randomUUID() + "/cache/vary-on-header";
+ // Vary: *
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1")
+ .header("X-Request-Vary", "*").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "1");
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2")
+ .header("X-Request-Vary", "*").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "2");
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenPathIsDifferent() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+ String uri2 = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri2).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
+ }
+
+ @Test
+ void shouldDecreaseCacheControlMaxAgeTimeWhenResponseIsFromCache() throws InterruptedException {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+ Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
+ .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
+ .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
+ Thread.sleep(2000);
+ Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
+ .expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
+ .map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
+
+ assertThat(maxAgeRequest2).isLessThan(maxAgeRequest1);
+ }
+
+ @Test
+ void shouldNotCacheResponseWhenTimeToLiveIsReached() {
+ String uri = "/" + UUID.randomUUID() + "/ephemeral-cache/headers";
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER).value(customHeaderFromReq1 -> {
+ try {
+ Thread.sleep(100); // Min time to have entry expired
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
+ .header(CUSTOM_HEADER, "2").exchange().expectBody()
+ .jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Test
+ void shouldNotCacheWhenLocalResponseCacheSizeIsReached() {
+ String uri = "/" + UUID.randomUUID() + "/one-byte-cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
+ .expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "2");
+ }
+
+ @Test
+ void shouldNotCacheWhenAuthorizationHeaderIsDifferent() {
+ String uri = "/" + UUID.randomUUID() + "/cache/headers";
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(HttpHeaders.AUTHORIZATION, "1")
+ .header(CUSTOM_HEADER, "1").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
+
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(HttpHeaders.AUTHORIZATION, "2")
+ .header(CUSTOM_HEADER, "2").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER, "2");
+ }
+
+ private Long parseMaxAge(String cacheControlValue) {
+ if (StringUtils.hasText(cacheControlValue)) {
+ Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
+ Matcher matcher = maxAgePattern.matcher(cacheControlValue);
+ if (matcher.find()) {
+ return Long.parseLong(matcher.group(1));
+ }
+ }
+ return null;
+ }
+
+ void assertNonVaryHeaderInContent(String uri, String varyHeader, String varyHeaderValue, String nonVaryHeader,
+ String nonVaryHeaderValue, String expectedNonVaryResponse) {
+ testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header("X-Request-Vary", varyHeader)
+ .header(varyHeader, varyHeaderValue).header(nonVaryHeader, nonVaryHeaderValue).exchange()
+ .expectBody(Map.class).consumeWith(response -> {
+ assertThat(response.getResponseHeaders()).hasEntrySatisfying("Vary",
+ o -> assertThat(o).contains(varyHeader));
+ assertThat((Map) response.getResponseBody().get("headers")).containsEntry(nonVaryHeader,
+ expectedNonVaryResponse);
+ });
+ }
+
+ @EnableAutoConfiguration
+ @SpringBootConfiguration
+ @Import(DefaultTestConfig.class)
+ public static class TestConfig {
+
+ @Value("${test.uri}")
+ String uri;
+
+ @Bean
+ public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
+ return builder.routes()
+ .route("no_local_response_cache_java_test",
+ r -> r.path("/{namespace}/no-cache/**").and().host("{sub}.localresponsecache.org")
+ .filters(f -> f.stripPrefix(2).prefixPath("/httpbin")).uri(uri))
+ .route("local_response_cache_java_test",
+ r -> r.path("/{namespace}/cache/**").and().host("{sub}.localresponsecache.org")
+ .filters(
+ f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null, null))
+ .uri(uri))
+ .route("100_millisec_ephemeral_prefix_local_response_cache_java_test",
+ r -> r.path("/{namespace}/ephemeral-cache/**").and().host("{sub}.localresponsecache.org")
+ .filters(f -> f.stripPrefix(2).prefixPath("/httpbin")
+ .localResponseCache(Duration.ofMillis(100), null))
+ .uri(uri))
+ .route("min_sized_prefix_local_response_cache_java_test",
+ r -> r.path("/{namespace}/one-byte-cache/**").and().host("{sub}.localresponsecache.org")
+ .filters(f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null,
+ DataSize.ofBytes(1L)))
+ .uri(uri))
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java
new file mode 100644
index 00000000..64616cc8
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/ResponseCacheGatewayFilterTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2013-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.filter.factory.cache;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Ignacio Lozano
+ */
+class ResponseCacheGatewayFilterTest {
+
+ ResponseCacheManager cacheManagerToTest = new ResponseCacheManager(null, null, null);
+
+ @Test
+ void requestShouldBeCacheable() {
+ var uri = "http://test.com";
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-transform");
+ var request = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+
+ assertThat(cacheManagerToTest.isRequestCacheable(request)).isTrue();
+ }
+
+ @Test
+ void requestShouldNotBeCacheable() {
+ var uri = "http://test.com";
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-store");
+ var request = MockServerHttpRequest.get(uri).headers(httpHeaders).build();
+
+ assertThat(cacheManagerToTest.isRequestCacheable(request)).isFalse();
+
+ httpHeaders = new HttpHeaders();
+ httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-transform");
+ request = MockServerHttpRequest.post(uri).headers(httpHeaders).build();
+
+ assertThat(cacheManagerToTest.isRequestCacheable(request)).isFalse();
+ }
+
+ @Test
+ void responseShouldBeCacheable() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "public");
+ var response = new MockServerHttpResponse();
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.OK);
+
+ assertThat(cacheManagerToTest.isResponseCacheable(response)).isTrue();
+
+ headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "public");
+ response = new MockServerHttpResponse();
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.PARTIAL_CONTENT);
+
+ assertThat(cacheManagerToTest.isResponseCacheable(response)).isTrue();
+ }
+
+ @Test
+ void responseShouldNotBeCacheable() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "public");
+ var response = new MockServerHttpResponse();
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
+
+ assertThat(cacheManagerToTest.isResponseCacheable(response)).isFalse();
+
+ headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "private");
+ response = new MockServerHttpResponse();
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.OK);
+
+ assertThat(cacheManagerToTest.isResponseCacheable(response)).isFalse();
+
+ headers = new HttpHeaders();
+ headers.add(HttpHeaders.CACHE_CONTROL, "no-store");
+ response = new MockServerHttpResponse();
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.OK);
+
+ assertThat(cacheManagerToTest.isResponseCacheable(response)).isFalse();
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CommonKeyValueGeneratorTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CommonKeyValueGeneratorTest.java
new file mode 100644
index 00000000..81b8bea6
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/CommonKeyValueGeneratorTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013-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.filter.factory.cache.keygenerator;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Ignacio Lozano
+ */
+class CommonKeyValueGeneratorTest {
+
+ @Test
+ void uriAuthorizationAndCookiesArePresent() {
+ String uri = "http://myuri";
+ HttpHeaders headers = new HttpHeaders();
+ String authorization = "my-auth";
+ headers.set("Authorization", authorization);
+ String cookieName = "my-cookie";
+ String cookieValue = "cookie-value";
+ HttpCookie cookie = new HttpCookie(cookieName, cookieValue);
+ MockServerHttpRequest request = MockServerHttpRequest.get(uri).cookie(cookie).headers(headers).build();
+
+ String result = new CommonKeyValueGenerator().apply(request);
+
+ assertThat(result).isEqualTo(uri + ";Authorization=" + authorization + ";" + cookieName + "=" + cookieValue);
+ }
+
+ @Test
+ void uriAndCookiesArePresent() {
+ String uri = "http://myuri";
+ HttpHeaders headers = new HttpHeaders();
+ String cookieName = "my-cookie";
+ String cookieValue = "cookie-value";
+ HttpCookie cookie = new HttpCookie(cookieName, cookieValue);
+ MockServerHttpRequest request = MockServerHttpRequest.get(uri).cookie(cookie).headers(headers).build();
+
+ String result = new CommonKeyValueGenerator().apply(request);
+
+ assertThat(result).isEqualTo(uri + ";" + "" + ";" + cookieName + "=" + cookieValue);
+ }
+
+ @Test
+ void onlyUriPresent() {
+ String uri = "http://myuri";
+ MockServerHttpRequest request = MockServerHttpRequest.get(uri).build();
+
+ String result = new CommonKeyValueGenerator().apply(request);
+
+ assertThat(result).isEqualTo(uri + ";" + "" + ";" + "");
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/HeaderKeyValueGeneratorTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/HeaderKeyValueGeneratorTest.java
new file mode 100644
index 00000000..38f2cdde
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/keygenerator/HeaderKeyValueGeneratorTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013-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.filter.factory.cache.keygenerator;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * @author Ignacio Lozano
+ */
+class HeaderKeyValueGeneratorTest {
+
+ private static final String HEADER_NAME = "X-Header";
+
+ private static final String SINGLE_HEADER_VALUE = "header-value";
+
+ private static final String VALUE1 = "value-1";
+
+ private static final String VALUE2 = "value-2";
+
+ private static final String SEPARATOR = ",";
+
+ @Test
+ void exceptionIsThrown_whenConstructorHeaderIsNull() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> new HeaderKeyValueGenerator(null, SEPARATOR));
+ }
+
+ @Test
+ void keyValuePatternIsGenerated_whenOneSingleValueHeaderIsFound() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HEADER_NAME, SINGLE_HEADER_VALUE);
+ MockServerHttpRequest request = MockServerHttpRequest.get("http://this").headers(headers).build();
+
+ String result = new HeaderKeyValueGenerator(HEADER_NAME, SEPARATOR).apply(request);
+
+ assertThat(result).isEqualTo(HEADER_NAME + "=" + SINGLE_HEADER_VALUE);
+ }
+
+ @Test
+ void keyValuePatternIsGenerated_whenOneMultipleValueHeaderIsFound() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.put(HEADER_NAME, List.of(VALUE1, VALUE2));
+ MockServerHttpRequest request = MockServerHttpRequest.get("http://this").headers(headers).build();
+
+ String result = new HeaderKeyValueGenerator(HEADER_NAME, SEPARATOR).apply(request);
+
+ assertThat(result).isEqualTo(HEADER_NAME + "=" + VALUE1 + SEPARATOR + VALUE2);
+ }
+
+ @Test
+ void sotedKeyValuePatternIsGenerated_whenOneMultipleUnsortedValueHeaderIsFound() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.put(HEADER_NAME, List.of(VALUE2, VALUE1));
+ MockServerHttpRequest request = MockServerHttpRequest.get("http://this").headers(headers).build();
+
+ String result = new HeaderKeyValueGenerator(HEADER_NAME, SEPARATOR).apply(request);
+
+ assertThat(result).isEqualTo(HEADER_NAME + "=" + VALUE1 + SEPARATOR + VALUE2);
+ }
+
+}
diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/postprocessor/SetMaxAgeHeaderAfterCacheExchangeMutatorTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/postprocessor/SetMaxAgeHeaderAfterCacheExchangeMutatorTest.java
new file mode 100644
index 00000000..b2adcd64
--- /dev/null
+++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/cache/postprocessor/SetMaxAgeHeaderAfterCacheExchangeMutatorTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2013-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.filter.factory.cache.postprocessor;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.util.StringUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Ignacio Lozano
+ */
+class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
+
+ private static final int SECONDS_LATER = 10;
+
+ private MockServerWebExchange inputExchange;
+
+ private Clock clock;
+
+ private Clock clockSecondsLater;
+
+ @BeforeEach
+ void setUp() {
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.setCacheControl("max-age=1234");
+ MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this").build();
+
+ inputExchange = MockServerWebExchange.from(httpRequest);
+ MockServerHttpResponse httpResponse = inputExchange.getResponse();
+ httpResponse.setStatusCode(HttpStatus.OK);
+ httpResponse.getHeaders().putAll(responseHeaders);
+
+ clock = Clock.fixed(Instant.now(), Clock.systemDefaultZone().getZone());
+ clockSecondsLater = Clock.fixed(clock.instant().plusSeconds(SECONDS_LATER), clock.getZone());
+ }
+
+ @Test
+ void maxAgeIsNotAdded_whenMaxAgeIsNotPresent() {
+ inputExchange.getResponse().getHeaders().setCacheControl((String) null);
+
+ Duration timeToLive = Duration.ofSeconds(30);
+ CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
+
+ SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
+ clock);
+ toTest.accept(inputExchange, inputCachedResponse);
+ assertThat(parseMaxAge(inputExchange.getResponse())).isEmpty();
+ }
+
+ @Test
+ void maxAgeIsDecreasedByTimePassed_whenFilterIsAppliedAfterSecondsLater() {
+ Duration timeToLive = Duration.ofSeconds(30);
+ CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
+
+ SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
+ clock);
+ toTest.accept(inputExchange, inputCachedResponse);
+ Optional