blob: e813b5ea63c4141f0be2c981e5dd6499060f6cbd [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import static org.hamcrest.MatcherAssert.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.cache.HttpCacheContext;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.MessageSupport;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/*
* This test class captures functionality required to achieve conditional
* compliance with the HTTP/1.1 caching protocol (MUST and MUST NOT behaviors).
*/
public class TestProtocolRequirements {
static final int MAX_BYTES = 1024;
static final int MAX_ENTRIES = 100;
static final int ENTITY_LENGTH = 128;
HttpHost host;
HttpRoute route;
HttpEntity body;
HttpCacheContext context;
@Mock
ExecChain mockExecChain;
@Mock
ExecRuntime mockExecRuntime;
@Mock
HttpCache mockCache;
ClassicHttpRequest request;
ClassicHttpResponse originResponse;
CacheConfig config;
CachingExec impl;
HttpCache cache;
@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
host = new HttpHost("foo.example.com", 80);
route = new HttpRoute(host);
body = HttpTestUtils.makeBody(ENTITY_LENGTH);
request = new BasicClassicHttpRequest("GET", "/");
context = HttpCacheContext.create();
originResponse = HttpTestUtils.make200Response();
config = CacheConfig.custom()
.setMaxCacheEntries(MAX_ENTRIES)
.setMaxObjectSize(MAX_BYTES)
.build();
cache = new BasicHttpCache(config);
impl = new CachingExec(cache, null, config);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
}
public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
return impl.execute(
ClassicRequestBuilder.copy(request).build(),
new ExecChain.Scope("test", route, request, mockExecRuntime, context),
mockExecChain);
}
@Test
public void testCacheMissOnGETUsesOriginResponse() throws Exception {
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
}
private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertNotNull(result);
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
.getCanonicalHeaderValue(result, h));
}
@Test
public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
originResponse.addHeader("Allow", "HEAD");
originResponse.addHeader("Allow", "DELETE");
testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
}
@Test
public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
originResponse.addHeader("Cache-Control", "max-age=0");
originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
}
@Test
public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
originResponse.addHeader("Content-Encoding", "gzip");
originResponse.addHeader("Content-Encoding", "compress");
testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
}
@Test
public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
originResponse.addHeader("Content-Language", "mi");
originResponse.addHeader("Content-Language", "en");
testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
}
@Test
public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
originResponse.addHeader(HttpHeaders.VIA, "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
originResponse.addHeader(HttpHeaders.VIA, "1.0 ricky, 1.1 mertz, 1.0 lucy");
testOrderOfMultipleHeadersIsPreservedOnResponses(HttpHeaders.VIA);
}
@Test
public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
}
private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
originResponse = new BasicClassicHttpResponse(code, "Moo");
originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
originResponse.setHeader("Server", "MockOrigin/1.0");
originResponse.setHeader("Cache-Control", "max-age=3600");
originResponse.setEntity(body);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
// in particular, there were no storage calls on the cache
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
for (int i = 100; i <= 199; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
for (int i = 207; i <= 299; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
for (int i = 308; i <= 399; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
for (int i = 418; i <= 499; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
for (int i = 506; i <= 999; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
}
@Test
public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
request.addHeader("X-Unknown-Header", "blahblah");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest forwarded = reqCapture.getValue();
MatcherAssert.assertThat(forwarded, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
}
@Test
public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
originResponse.addHeader("X-Unknown-Header", "blahblah");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
MatcherAssert.assertThat(result, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
}
@Test
public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
request = new BasicClassicHttpRequest("OPTIONS", "/");
originResponse.addHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
post.setHeader("Content-Length", "128");
post.setEntity(HttpTestUtils.makeBody(128));
originResponse.removeHeaders("Cache-Control");
originResponse.removeHeaders("Expires");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(post);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testResponsesToPUTsAreNotCached() throws Exception {
final BasicClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
put.setEntity(HttpTestUtils.makeBody(128));
put.addHeader("Content-Length", "128");
originResponse.setHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(put);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testResponsesToDELETEsAreNotCached() throws Exception {
request = new BasicClassicHttpRequest("DELETE", "/");
originResponse.setHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testResponsesToTRACEsAreNotCached() throws Exception {
request = new BasicClassicHttpRequest("TRACE", "/");
originResponse.setHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
originResponse.setHeader("Cache-Control", "max-age=3600");
originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Assertions.assertNotNull(result.getFirstHeader("Date"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
originResponse.setHeader("Cache-Control", "max-age=3600");
originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Assertions.assertNotNull(result.getFirstHeader("ETag"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
originResponse.setHeader("Cache-Control", "max-age=3600");
originResponse.setHeader("Content-Location", "http://foo.example.com/other");
originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Assertions.assertNotNull(result.getFirstHeader("Content-Location"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer() throws Exception {
final Instant now = Instant.now();
final Instant inTwoHours = now.plus(2, ChronoUnit.HOURS);
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("Accept-Encoding", "gzip");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag", "\"v1\"");
resp1.setHeader("Cache-Control", "max-age=7200");
resp1.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
resp1.setHeader("Vary", "Accept-Encoding");
resp1.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Accept-Encoding", "gzip");
req2.setHeader("Cache-Control", "no-cache");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"v2\"");
resp2.setHeader("Cache-Control", "max-age=3600");
resp2.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
resp2.setHeader("Vary", "Accept-Encoding");
resp2.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
req3.setHeader("Accept-Encoding", "gzip");
req3.setHeader("If-None-Match", "\"v2\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
final ClassicHttpResponse result = execute(req3);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Assertions.assertNotNull(result.getFirstHeader("Expires"));
Assertions.assertNotNull(result.getFirstHeader("Cache-Control"));
Assertions.assertNotNull(result.getFirstHeader("Vary"));
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders() throws Exception {
final Instant now = Instant.now();
final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag", "W/\"v1\"");
resp1.setHeader("Allow", "GET,HEAD");
resp1.setHeader("Content-Encoding", "x-coding");
resp1.setHeader("Content-Language", "en");
resp1.setHeader("Content-Length", "128");
resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
resp1.setHeader("Content-Type", "application/octet-stream");
resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(oneHourAgo));
resp1.setHeader("Cache-Control", "max-age=7200");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "W/\"v1\"");
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req1), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Assertions.assertNull(result.getFirstHeader("Allow"));
Assertions.assertNull(result.getFirstHeader("Content-Encoding"));
Assertions.assertNull(result.getFirstHeader("Content-Length"));
Assertions.assertNull(result.getFirstHeader("Content-MD5"));
Assertions.assertNull(result.getFirstHeader("Content-Type"));
Assertions.assertNull(result.getFirstHeader("Last-Modified"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET() throws Exception {
final Instant now = Instant.now();
// load cache with cacheable entry
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag", "\"etag1\"");
resp1.setHeader("Cache-Control", "max-age=3600");
// force a revalidation
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
// unconditional validation doesn't use If-None-Match
final ClassicHttpRequest unconditionalValidation = new BasicClassicHttpRequest("GET", "/");
// new response to unconditional validation provides new body
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp1.setHeader("ETag", "\"etag2\"");
resp1.setHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
// this next one will happen once if the cache tries to
// conditionally validate, zero if it goes full revalidation
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(unconditionalValidation), Mockito.any())).thenReturn(resp2);
execute(req1);
execute(req2);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
final Instant now = Instant.now();
final Instant inFiveSeconds = now.plusSeconds(5);
final ClassicHttpRequest initialRequest = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse cachedResponse = HttpTestUtils.make200Response();
cachedResponse.setHeader("Cache-Control", "max-age=3600");
cachedResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest secondRequest = new BasicClassicHttpRequest("GET", "/");
secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
final ClassicHttpRequest conditionalValidationRequest = new BasicClassicHttpRequest("GET", "/");
conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
// to be used if the cache generates a conditional validation
final ClassicHttpResponse conditionalResponse = HttpTestUtils.make304Response();
conditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
conditionalResponse.setHeader("Server", "MockUtils/1.0");
conditionalResponse.setHeader("ETag", "\"etag\"");
conditionalResponse.setHeader("X-Extra", "junk");
// to be used if the cache generates an unconditional validation
final ClassicHttpResponse unconditionalResponse = HttpTestUtils.make200Response();
unconditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
unconditionalResponse.setHeader("ETag", "\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(cachedResponse);
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(conditionalValidationRequest), Mockito.any())).thenReturn(conditionalResponse);
execute(initialRequest);
final ClassicHttpResponse result = execute(secondRequest);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
Assertions.assertEquals(DateUtils.formatStandardDate(inFiveSeconds), result.getFirstHeader("Date").getValue());
Assertions.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
}
@Test
public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final Instant nineSecondsAgo = now.minusSeconds(9);
final Instant eightSecondsAgo = now.minusSeconds(8);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
Method.GET, "/thing", null,
200, new Header[] {
new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
new BasicHeader("ETag", "\"etag\"")
}, HttpTestUtils.makeNullResource());
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/thing");
final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/thing");
validate.setHeader("If-None-Match", "\"etag\"");
final ClassicHttpResponse notModified = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
notModified.setHeader("Date", DateUtils.formatStandardDate(now));
notModified.setHeader("ETag", "\"etag\"");
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(validate), Mockito.any())).thenReturn(notModified);
final HttpCacheEntry updated = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
Method.GET, "/thing", null,
200, new Header[] {
new BasicHeader("Date", DateUtils.formatStandardDate(now)),
new BasicHeader("ETag", "\"etag\"")
}, HttpTestUtils.makeNullResource());
Mockito.when(mockCache.update(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any()))
.thenReturn(new CacheHit("key", updated));
execute(request);
Mockito.verify(mockCache).update(
Mockito.any(),
Mockito.eq(host),
RequestEquivalent.eq(request),
ResponseEquivalent.eq(notModified),
Mockito.any(),
Mockito.any());
}
@Test
public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final Instant nineSecondsAgo = now.plusSeconds(9);
final Instant eightSecondsAgo = now.plusSeconds(8);
final Header[] hdrs = new Header[] {
new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
new BasicHeader("Cache-Control", "max-age=3600"),
new BasicHeader("Content-Length", "128")
};
final byte[] bytes = new byte[128];
new Random().nextBytes(bytes);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/thing");
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(200, result.getCode());
}
@Test
public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final Instant nineSecondsAgo = now.minusSeconds(9);
final Instant eightSecondsAgo = now.minusSeconds(8);
final Header[] hdrs = new Header[] {
new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
new BasicHeader("Cache-Control", "max-age=3600"),
new BasicHeader("Content-Length", "128")
};
final byte[] bytes = new byte[128];
new Random().nextBytes(bytes);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
impl = new CachingExec(mockCache, null, config);
request = new BasicClassicHttpRequest("GET", "/");
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
new CacheMatch(new CacheHit("key", entry), null));
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(200, result.getCode());
// We calculate the age of the cache entry as per RFC 9111:
// We first find the "corrected_initial_age" which is the maximum of "apparentAge" and "correctedReceivedAge".
// In this case, max(1, 2) = 2 seconds.
// We then add the "residentTime" which is "now - responseTime",
// which is the current time minus the time the cache entry was created. In this case, that is 8 seconds.
// So, the total age is "corrected_initial_age" + "residentTime" = 2 + 8 = 10 seconds.
assertThat(result, ContainsHeaderMatcher.contains("Age", "10"));
}
@Test
public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
final Instant now = Instant.now();
final Instant inFiveSecond = now.plusSeconds(5);
// put an entry in the cache
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(inFiveSecond));
resp1.setHeader("ETag", "\"etag1\"");
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Content-Length", "128");
// force another origin hit
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "no-cache");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Date", DateUtils.formatStandardDate(now)); // older
resp2.setHeader("ETag", "\"etag2\"");
resp2.setHeader("Cache-Control", "max-age=3600");
resp2.setHeader("Content-Length", "128");
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
final ClassicHttpResponse result = execute(req3);
Assertions.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
}
@Test
public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("ETag", "W/\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
execute(req2);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
Assertions.assertEquals(2, allRequests.size());
final ClassicHttpRequest validation = allRequests.get(1);
boolean foundETag = false;
final Iterator<HeaderElement> it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
while (it.hasNext()) {
final HeaderElement elt = it.next();
if ("W/\"etag\"".equals(elt.getName())) {
foundETag = true;
}
}
final Iterator<HeaderElement> it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
while (it2.hasNext()) {
final HeaderElement elt = it2.next();
if ("W/\"etag\"".equals(elt.getName())) {
foundETag = true;
}
}
Assertions.assertTrue(foundETag);
}
@Test
public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final Instant twentySecondsAgo = now.plusSeconds(20);
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("ETag", "W/\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "W/\"etag\"");
req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(twentySecondsAgo));
// must hit the origin again for the second request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertNotEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("ETag", "W/\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("If-None-Match", "W/\"etag\"");
req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
// may hit the origin again for the second request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
execute(req2);
Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
req.setHeader("Range", "bytes=0-50");
final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
resp.setHeader("Content-Range", "bytes 0-50/128");
resp.setHeader("ETag", "\"etag\"");
resp.setHeader("Cache-Control", "max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp);
execute(req);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
originResponse = new BasicClassicHttpResponse(302, "Temporary Redirect");
originResponse.setHeader("Location", "http://foo.example.com/other");
originResponse.removeHeaders("Expires");
originResponse.removeHeaders("Cache-Control");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
execute(request);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
originResponse = HttpTestUtils.make200Response();
originResponse.setHeader(header, value);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
}
@Test
public void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
final String url = "http://foo.example.com/other";
testDoesNotModifyHeaderFromOrigin("Content-Location", url);
}
@Test
public void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
@Test
public void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
}
@Test
public void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
final String lm = DateUtils.formatStandardDate(Instant.now());
testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
}
private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
originResponse.removeHeaders(header);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertNull(result.getFirstHeader(header));
}
@Test
public void testDoesNotAddContentLocationToOriginResponse() throws Exception {
testDoesNotAddHeaderToOriginResponse("Content-Location");
}
@Test
public void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
testDoesNotAddHeaderToOriginResponse("Content-MD5");
}
@Test
public void testDoesNotAddEtagToOriginResponse() throws Exception {
testDoesNotAddHeaderToOriginResponse("ETag");
}
@Test
public void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
testDoesNotAddHeaderToOriginResponse("Last-Modified");
}
private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
originResponse = HttpTestUtils.make200Response();
originResponse.setHeader("Cache-Control", "max-age=3600");
originResponse.setHeader(header, value);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
}
@Test
public void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
final String url = "http://foo.example.com/other";
testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
}
@Test
public void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
@Test
public void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
}
@Test
public void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
}
private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
originResponse.addHeader("Cache-Control", "max-age=3600");
originResponse.removeHeaders(header);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertNull(result.getFirstHeader(header));
}
@Test
public void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
testDoesNotAddHeaderOnCacheHit("Content-Location");
}
@Test
public void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
testDoesNotAddHeaderOnCacheHit("Content-MD5");
}
@Test
public void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
testDoesNotAddHeaderOnCacheHit("ETag");
}
@Test
public void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
testDoesNotAddHeaderOnCacheHit("Last-Modified");
}
private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
req.setEntity(HttpTestUtils.makeBody(128));
req.setHeader("Content-Length","128");
req.setHeader(header,value);
execute(req);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest captured = reqCapture.getValue();
Assertions.assertEquals(value, captured.getFirstHeader(header).getValue());
}
@Test
public void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
final String url = "http://foo.example.com/other";
testDoesNotModifyHeaderOnRequest("Content-Location",url);
}
@Test
public void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
@Test
public void testDoesNotModifyETagHeaderOnRequest() throws Exception {
testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
}
@Test
public void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
testDoesNotModifyHeaderOnRequest("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
}
private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
req.setEntity(HttpTestUtils.makeBody(128));
req.setHeader("Content-Length","128");
req.removeHeaders(header);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest captured = reqCapture.getValue();
Assertions.assertNull(captured.getFirstHeader(header));
}
@Test
public void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
}
@Test
public void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
}
@Test
public void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("ETag");
}
@Test
public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
}
@Test
public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
testDoesNotModifyHeaderFromOrigin("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
}
@Test
public void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
final Instant inTenSeconds = Instant.now().plusSeconds(10);
testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", DateUtils.formatStandardDate(inTenSeconds));
}
@Test
public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
originResponse.removeHeaders("Expires");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
final Header expHdr = result.getFirstHeader("Expires");
if (expHdr != null) {
Assertions.assertEquals(result.getFirstHeader("Date").getValue(),
expHdr.getValue());
}
}
private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
originResponse.setHeader(header, value);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
}
@Test
public void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
}
@Test
public void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
request.setHeader("If-Range","\"etag\"");
request.setHeader("Range","bytes=0-49");
originResponse = new BasicClassicHttpResponse(206, "Partial Content");
originResponse.setEntity(HttpTestUtils.makeBody(50));
testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
}
@Test
public void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
}
private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
originResponse.setHeader(header, value);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
}
@Test
public void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
}
@Test
public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
}
@Test
public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderToOriginResponse("Content-Encoding");
}
@Test
public void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderToOriginResponse("Content-Range");
}
@Test
public void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderToOriginResponse("Content-Type");
}
/* no add on cache hit with no-transform */
@Test
public void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderOnCacheHit("Content-Encoding");
}
@Test
public void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderOnCacheHit("Content-Range");
}
@Test
public void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
testDoesNotAddHeaderOnCacheHit("Content-Type");
}
/* no modify on request */
@Test
public void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
}
@Test
public void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
}
@Test
public void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
}
@Test
public void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
}
@Test
public void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
}
@Test
public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
}
@Test
public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag","\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control","max-age=0, max-stale=0");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result = execute(req2);
try (final InputStream i1 = resp1.getEntity().getContent();
final InputStream i2 = result.getEntity().getContent()) {
int b1, b2;
while((b1 = i1.read()) != -1) {
b2 = i2.read();
Assertions.assertEquals(b1, b2);
}
b2 = i2.read();
Assertions.assertEquals(-1, b2);
}
}
private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
r.setHeader("Allow","GET");
r.setHeader("Content-Encoding","gzip");
r.setHeader("Content-Language","en");
r.setHeader("Content-Length", "128");
r.setHeader("Content-Location","http://foo.example.com/other");
r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
r.setHeader("Content-Type", "text/html;charset=utf-8");
r.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(10)));
r.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now().minusSeconds(10)));
r.setHeader("Location", "http://foo.example.com/other2");
r.setHeader("Retry-After","180");
}
@Test
public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag","\"etag\"");
decorateWithEndToEndHeaders(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
resp2.setHeader("Server", "MockServer/1.0");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result = execute(req2);
final String[] endToEndHeaders = {
"Cache-Control", "ETag", "Allow", "Content-Encoding",
"Content-Language", "Content-Length", "Content-Location",
"Content-MD5", "Content-Type", "Expires", "Last-Modified",
"Location", "Retry-After"
};
for(final String h : endToEndHeaders) {
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
HttpTestUtils.getCanonicalHeaderValue(result, h));
}
}
@Test
public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag","\"etag\"");
decorateWithEndToEndHeaders(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Cache-Control", "max-age=1800");
resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
resp2.setHeader("Server", "MockServer/1.0");
resp2.setHeader("Allow", "GET,HEAD");
resp2.setHeader("Content-Language", "en,en-us");
resp2.setHeader("Content-Location", "http://foo.example.com/new");
resp2.setHeader("Content-Type","text/html");
resp2.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(5)));
resp2.setHeader("Location", "http://foo.example.com/new2");
resp2.setHeader("Retry-After","120");
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result1 = execute(req2);
final ClassicHttpResponse result2 = execute(req3);
final String[] endToEndHeaders = {
"Date", "Cache-Control", "Allow", "Content-Language",
"Content-Location", "Content-Type", "Expires", "Location",
"Retry-After"
};
for(final String h : endToEndHeaders) {
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
HttpTestUtils.getCanonicalHeaderValue(result1, h));
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
HttpTestUtils.getCanonicalHeaderValue(result2, h));
}
}
@Test
public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.addHeader("Cache-Control","max-age=3600");
resp1.addHeader("Cache-Control","public");
resp1.setHeader("ETag","\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Cache-Control", "max-age=1800");
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result1 = execute(req2);
final ClassicHttpResponse result2 = execute(req3);
final String h = "Cache-Control";
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
HttpTestUtils.getCanonicalHeaderValue(result1, h));
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
HttpTestUtils.getCanonicalHeaderValue(result2, h));
}
@Test
public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("Accept-Encoding","gzip");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","Accept-Encoding");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.removeHeaders("Accept-Encoding");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Cache-Control","max-age=3600");
// not allowed to have a cache hit; must forward request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testCannotServeFromCacheForVaryStar() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","*");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag","\"etag1\"");
resp2.setHeader("Cache-Control","max-age=3600");
// not allowed to have a cache hit; must forward request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testNonMatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("User-Agent","MyBrowser/1.0");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag1\"");
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","User-Agent");
resp1.setHeader("Content-Type","application/octet-stream");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("User-Agent","MyBrowser/1.5");
final ClassicHttpResponse resp200 = HttpTestUtils.make200Response();
resp200.setHeader("ETag","\"etag1\"");
resp200.setHeader("Vary","User-Agent");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp200);
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
}
protected void testUnsafeOperationInvalidatesCacheForThatUri(
final ClassicHttpRequest unsafeReq) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","public, max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(unsafeReq);
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("Cache-Control","public, max-age=3600");
// this origin request MUST happen due to invalidation
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
execute(req3);
}
protected ClassicHttpRequest makeRequestWithBody(final String method, final String requestUri) {
final ClassicHttpRequest req = new BasicClassicHttpRequest(method, requestUri);
final int nbytes = 128;
req.setEntity(HttpTestUtils.makeBody(nbytes));
req.setHeader("Content-Length", Long.toString(nbytes));
return req;
}
@Test
public void testPutToUriInvalidatesCacheForThatUri() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
testUnsafeOperationInvalidatesCacheForThatUri(req);
}
@Test
public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE","/");
testUnsafeOperationInvalidatesCacheForThatUri(req);
}
@Test
public void testPostToUriInvalidatesCacheForThatUri() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("POST","/");
testUnsafeOperationInvalidatesCacheForThatUri(req);
}
protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
final ClassicHttpRequest unsafeReq) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","public, max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(unsafeReq);
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("Cache-Control","public, max-age=3600");
// this origin request MUST happen due to invalidation
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
execute(req3);
}
protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
final ClassicHttpRequest unsafeReq) throws Exception {
unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
}
protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
final ClassicHttpRequest unsafeReq) throws Exception {
unsafeReq.setHeader("Content-Location","/content");
testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
}
protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
final ClassicHttpRequest unsafeReq) throws Exception {
unsafeReq.setHeader("Location","http://foo.example.com/content");
testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
}
@Test
public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
final ClassicHttpRequest req2 = makeRequestWithBody("PUT","/");
testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
}
@Test
public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
}
@Test
public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
}
@Test
public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
}
@Test
public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
}
@Test
public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
}
@Test
public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("POST","/");
testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
}
@Test
public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("POST","/");
testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
}
@Test
public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
final ClassicHttpRequest req = makeRequestWithBody("POST","/");
testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
}
private void testRequestIsWrittenThroughToOrigin(final ClassicHttpRequest req) throws Exception {
final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
final ClassicHttpRequest wrapper = req;
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(wrapper), Mockito.any())).thenReturn(resp);
execute(wrapper);
}
@Test
public void testOPTIONSRequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS","*");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testPOSTRequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
req.setEntity(HttpTestUtils.makeBody(128));
req.setHeader("Content-Length","128");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testPUTRequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("PUT","/");
req.setEntity(HttpTestUtils.makeBody(128));
req.setHeader("Content-Length","128");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testDELETERequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testTRACERequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("TRACE","/");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testCONNECTRequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("CONNECT","/");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testUnknownMethodRequestsAreWrittenThroughToOrigin() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("UNKNOWN","/");
testRequestIsWrittenThroughToOrigin(req);
}
@Test
public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
final String reallyOldAge = "1" + Long.MAX_VALUE;
originResponse.setHeader("Age",reallyOldAge);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(reallyOldAge,
result.getFirstHeader("Age").getValue());
}
@Test
public void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
final String allowHeaderValue = "GET, HEAD, FOOBAR";
originResponse.setHeader("Allow",allowHeaderValue);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
}
protected void testSharedCacheRevalidatesAuthorizedResponse(
final ClassicHttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception {
if (config.isSharedCache()) {
final String authorization = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("Authorization",authorization);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
Mockito.verify(mockExecChain, Mockito.atLeast(1 + minTimes)).proceed(Mockito.any(), Mockito.any());
Mockito.verify(mockExecChain, Mockito.atMost(1 + maxTimes)).proceed(Mockito.any(), Mockito.any());
}
}
@Test
public void testSharedCacheMustNotNormallyCacheAuthorizedResponses() throws Exception {
final ClassicHttpResponse resp = HttpTestUtils.make200Response();
resp.setHeader("Cache-Control","max-age=3600");
resp.setHeader("ETag","\"etag\"");
testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
}
@Test
public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader() throws Exception {
final ClassicHttpResponse resp = HttpTestUtils.make200Response();
resp.setHeader("Cache-Control","s-maxage=3600");
resp.setHeader("ETag","\"etag\"");
testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
}
@Test
public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero() throws Exception {
final ClassicHttpResponse resp = HttpTestUtils.make200Response();
resp.setHeader("Cache-Control","s-maxage=0");
resp.setHeader("ETag","\"etag\"");
testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
}
@Test
public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate() throws Exception {
final ClassicHttpResponse resp = HttpTestUtils.make200Response();
resp.setHeader("Cache-Control","must-revalidate");
resp.setHeader("ETag","\"etag\"");
testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
}
@Test
public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic() throws Exception {
final ClassicHttpResponse resp = HttpTestUtils.make200Response();
resp.setHeader("Cache-Control","public");
testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
}
protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
final ClassicHttpResponse authorizedResponse) throws Exception {
if (config.isSharedCache()) {
final String authorization1 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
final String authorization2 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Qy";
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("Authorization",authorization1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Authorization",authorization2);
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
Assertions.assertEquals(2, allRequests.size());
final ClassicHttpRequest captured = allRequests.get(1);
Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
}
}
@Test
public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","s-maxage=5");
testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
}
@Test
public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate() throws Exception {
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
}
protected void testCacheIsNotUsedWhenRespondingToRequest(final ClassicHttpRequest req) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Etag","\"etag\"");
resp1.setHeader("Cache-Control","max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Etag","\"etag2\"");
resp2.setHeader("Cache-Control","max-age=1200");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result = execute(req);
Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest captured = reqCapture.getValue();
Assertions.assertTrue(HttpTestUtils.equivalent(req, captured));
}
@Test
public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() throws Exception {
final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
req.setHeader("Cache-Control","no-cache");
testCacheIsNotUsedWhenRespondingToRequest(req);
}
protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
final ClassicHttpResponse staleResponse) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control","max-stale=3600");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag","\"etag2\"");
resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
// this request MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest reval = reqCapture.getValue();
boolean foundMaxAge0 = false;
final Iterator<HeaderElement> it = MessageSupport.iterate(reval, HttpHeaders.CACHE_CONTROL);
while (it.hasNext()) {
final HeaderElement elt = it.next();
if ("max-age".equalsIgnoreCase(elt.getName())
&& "0".equals(elt.getValue())) {
foundMaxAge0 = true;
}
}
Assertions.assertTrue(foundMaxAge0);
}
@Test
public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
final ClassicHttpResponse response = HttpTestUtils.make200Response();
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
response.setHeader("ETag","\"etag1\"");
response.setHeader("Cache-Control","max-age=5, must-revalidate");
testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
}
protected void testGenerates504IfCannotRevalidateStaleResponse(
final ClassicHttpResponse staleResponse) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new SocketTimeoutException());
final ClassicHttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
result.getCode());
}
@Test
public void testGenerates504IfCannotRevalidateAMustRevalidateEntry() throws Exception {
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
testGenerates504IfCannotRevalidateStaleResponse(resp1);
}
@Test
public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
if (config.isSharedCache()) {
final ClassicHttpResponse response = HttpTestUtils.make200Response();
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
response.setHeader("ETag","\"etag1\"");
response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
}
}
@Test
public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry() throws Exception {
if (config.isSharedCache()) {
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
testGenerates504IfCannotRevalidateStaleResponse(resp1);
}
}
@Test
public void testCacheControlPrivateIsNotCacheableBySharedCache() throws Exception {
if (config.isSharedCache()) {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control", "private,max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// this backend request MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req1);
execute(req2);
}
}
@Test
public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() throws Exception {
if (config.isSharedCache()) {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("X-Personal", "stuff");
resp1.setHeader("Cache-Control", "private=\"X-Personal\",s-maxage=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// this backend request MAY happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req1);
final ClassicHttpResponse result = execute(req2);
Assertions.assertNull(result.getFirstHeader("X-Personal"));
Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
}
}
@Test
public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","no-cache");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// this MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control","max-stale=7200");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// this MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
@Test
public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("X-Stuff","things");
resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag","\"etag\"");
resp2.setHeader("X-Stuff","things");
resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse result = execute(req2);
final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
if (allRequests.isEmpty()) {
Assertions.assertNull(result.getFirstHeader("X-Stuff"));
}
}
@Test
public void testNoStoreOnRequestIsNotStoredInCache() throws Exception {
request.setHeader("Cache-Control","no-store");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable() throws Exception {
request.setHeader("Cache-Control","no-store");
originResponse.setHeader("Cache-Control","max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testNoStoreOnResponseIsNotStoredInCache() throws Exception {
originResponse.setHeader("Cache-Control","no-store");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators() throws Exception {
originResponse.setHeader("Cache-Control","no-store,max-age=3600");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(request);
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved() throws Exception {
originResponse.addHeader("Content-Encoding","gzip");
originResponse.addHeader("Content-Encoding","deflate");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
int total_encodings = 0;
final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
while (it.hasNext()) {
final HeaderElement elt = it.next();
switch(total_encodings) {
case 0:
Assertions.assertEquals("gzip", elt.getName());
break;
case 1:
Assertions.assertEquals("deflate", elt.getName());
break;
default:
Assertions.fail("too many encodings");
}
total_encodings++;
}
Assertions.assertEquals(2, total_encodings);
}
@Test
public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved() throws Exception {
originResponse.addHeader("Content-Encoding","gzip,deflate");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
int total_encodings = 0;
final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
while (it.hasNext()) {
final HeaderElement elt = it.next();
switch(total_encodings) {
case 0:
Assertions.assertEquals("gzip", elt.getName());
break;
case 1:
Assertions.assertEquals("deflate", elt.getName());
break;
default:
Assertions.fail("too many encodings");
}
total_encodings++;
}
Assertions.assertEquals(2, total_encodings);
}
@Test
public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/foo");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","public,max-age=3600");
resp1.setHeader("Etag","\"etag\"");
resp1.setHeader("Content-Location","http://foo.example.com/bar");
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/bar");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","public,max-age=3600");
resp2.setHeader("Etag","\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
@Test
public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throws Exception {
originResponse.removeHeaders("Date");
originResponse.setHeader("Cache-Control","public");
originResponse.setHeader("ETag","\"etag\"");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertNotNull(result.getFirstHeader("Date"));
}
private void testInvalidExpiresHeaderIsTreatedAsStale(
final String expiresHeader) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","public");
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Expires", expiresHeader);
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
// second request to origin MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
@Test
public void testMalformedExpiresHeaderIsTreatedAsStale() throws Exception {
testInvalidExpiresHeaderIsTreatedAsStale("garbage");
}
@Test
public void testExpiresZeroHeaderIsTreatedAsStale() throws Exception {
testInvalidExpiresHeaderIsTreatedAsStale("0");
}
@Test
public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","public");
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
execute(req1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
// second request to origin MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
execute(req2);
}
@Test
public void testDoesNotModifyServerResponseHeader() throws Exception {
final String server = "MockServer/1.0";
originResponse.setHeader("Server", server);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
Assertions.assertEquals(server, result.getFirstHeader("Server").getValue());
}
}