View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import static org.hamcrest.MatcherAssert.assertThat;
30  
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.net.SocketTimeoutException;
34  import java.time.Instant;
35  import java.time.temporal.ChronoUnit;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.Random;
39  
40  import org.apache.hc.client5.http.HttpRoute;
41  import org.apache.hc.client5.http.auth.StandardAuthScheme;
42  import org.apache.hc.client5.http.cache.HttpCacheContext;
43  import org.apache.hc.client5.http.cache.HttpCacheEntry;
44  import org.apache.hc.client5.http.classic.ExecChain;
45  import org.apache.hc.client5.http.classic.ExecRuntime;
46  import org.apache.hc.client5.http.utils.DateUtils;
47  import org.apache.hc.core5.http.ClassicHttpRequest;
48  import org.apache.hc.core5.http.ClassicHttpResponse;
49  import org.apache.hc.core5.http.Header;
50  import org.apache.hc.core5.http.HeaderElement;
51  import org.apache.hc.core5.http.HttpEntity;
52  import org.apache.hc.core5.http.HttpException;
53  import org.apache.hc.core5.http.HttpHeaders;
54  import org.apache.hc.core5.http.HttpHost;
55  import org.apache.hc.core5.http.HttpStatus;
56  import org.apache.hc.core5.http.Method;
57  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
58  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
59  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
60  import org.apache.hc.core5.http.message.BasicHeader;
61  import org.apache.hc.core5.http.message.MessageSupport;
62  import org.hamcrest.MatcherAssert;
63  import org.junit.jupiter.api.Assertions;
64  import org.junit.jupiter.api.BeforeEach;
65  import org.junit.jupiter.api.Test;
66  import org.mockito.ArgumentCaptor;
67  import org.mockito.Mock;
68  import org.mockito.Mockito;
69  import org.mockito.MockitoAnnotations;
70  
71  /*
72   * This test class captures functionality required to achieve conditional
73   * compliance with the HTTP/1.1 caching protocol (MUST and MUST NOT behaviors).
74   */
75  class TestProtocolRequirements {
76  
77      static final int MAX_BYTES = 1024;
78      static final int MAX_ENTRIES = 100;
79      static final int ENTITY_LENGTH = 128;
80  
81      HttpHost host;
82      HttpRoute route;
83      HttpEntity body;
84      HttpCacheContext context;
85      @Mock
86      ExecChain mockExecChain;
87      @Mock
88      ExecRuntime mockExecRuntime;
89      @Mock
90      HttpCache mockCache;
91      ClassicHttpRequest request;
92      ClassicHttpResponse originResponse;
93      CacheConfig config;
94      CachingExec impl;
95      HttpCache cache;
96  
97      @BeforeEach
98      void setUp() throws Exception {
99          MockitoAnnotations.openMocks(this);
100         host = new HttpHost("foo.example.com", 80);
101 
102         route = new HttpRoute(host);
103 
104         body = HttpTestUtils.makeBody(ENTITY_LENGTH);
105 
106         request = new BasicClassicHttpRequest("GET", "/");
107 
108         context = HttpCacheContext.create();
109 
110         originResponse = HttpTestUtils.make200Response();
111 
112         config = CacheConfig.custom()
113                 .setMaxCacheEntries(MAX_ENTRIES)
114                 .setMaxObjectSize(MAX_BYTES)
115                 .build();
116 
117         cache = new BasicHttpCache(config);
118         impl = new CachingExec(cache, null, config);
119 
120         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
121     }
122 
123     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
124         return impl.execute(
125                 ClassicRequestBuilder.copy(request).build(),
126                 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
127                 mockExecChain);
128     }
129 
130     @Test
131     void testCacheMissOnGETUsesOriginResponse() throws Exception {
132 
133         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(originResponse);
134 
135         final ClassicHttpResponse result = execute(request);
136 
137         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
138     }
139 
140     private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
141         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
142 
143         final ClassicHttpResponse result = execute(request);
144 
145         Assertions.assertNotNull(result);
146         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
147                 .getCanonicalHeaderValue(result, h));
148 
149     }
150 
151     @Test
152     void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
153         originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
154         originResponse.addHeader("Allow", "HEAD");
155         originResponse.addHeader("Allow", "DELETE");
156         testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
157     }
158 
159     @Test
160     void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
161         originResponse.addHeader("Cache-Control", "max-age=0");
162         originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
163         testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
164     }
165 
166     @Test
167     void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
168         originResponse.addHeader("Content-Encoding", "gzip");
169         originResponse.addHeader("Content-Encoding", "compress");
170         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
171     }
172 
173     @Test
174     void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
175         originResponse.addHeader("Content-Language", "mi");
176         originResponse.addHeader("Content-Language", "en");
177         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
178     }
179 
180     @Test
181     void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
182         originResponse.addHeader(HttpHeaders.VIA, "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
183         originResponse.addHeader(HttpHeaders.VIA, "1.0 ricky, 1.1 mertz, 1.0 lucy");
184         testOrderOfMultipleHeadersIsPreservedOnResponses(HttpHeaders.VIA);
185     }
186 
187     @Test
188     void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
189         originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
190         originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
191         testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
192     }
193 
194     private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
195 
196         originResponse = new BasicClassicHttpResponse(code, "Moo");
197         originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
198         originResponse.setHeader("Server", "MockOrigin/1.0");
199         originResponse.setHeader("Cache-Control", "max-age=3600");
200         originResponse.setEntity(body);
201 
202         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
203 
204         execute(request);
205 
206         // in particular, there were no storage calls on the cache
207         Mockito.verifyNoInteractions(mockCache);
208     }
209 
210     @Test
211     void testUnknownResponseStatusCodesAreNotCached() throws Exception {
212         for (int i = 100; i <= 199; i++) {
213             testUnknownResponseStatusCodeIsNotCached(i);
214         }
215         for (int i = 207; i <= 299; i++) {
216             testUnknownResponseStatusCodeIsNotCached(i);
217         }
218         for (int i = 308; i <= 399; i++) {
219             testUnknownResponseStatusCodeIsNotCached(i);
220         }
221         for (int i = 418; i <= 499; i++) {
222             testUnknownResponseStatusCodeIsNotCached(i);
223         }
224         for (int i = 506; i <= 999; i++) {
225             testUnknownResponseStatusCodeIsNotCached(i);
226         }
227     }
228 
229     @Test
230     void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
231         request.addHeader("X-Unknown-Header", "blahblah");
232         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
233 
234         execute(request);
235 
236         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
237         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
238         final ClassicHttpRequest forwarded = reqCapture.getValue();
239         MatcherAssert.assertThat(forwarded, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
240     }
241 
242     @Test
243     void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
244         originResponse.addHeader("X-Unknown-Header", "blahblah");
245         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
246 
247         final ClassicHttpResponse result = execute(request);
248         MatcherAssert.assertThat(result, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
249     }
250 
251     @Test
252     void testResponsesToOPTIONSAreNotCacheable() throws Exception {
253         request = new BasicClassicHttpRequest("OPTIONS", "/");
254         originResponse.addHeader("Cache-Control", "max-age=3600");
255 
256         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
257 
258         execute(request);
259 
260         Mockito.verifyNoInteractions(mockCache);
261     }
262 
263     @Test
264     void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
265 
266         final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
267         post.setHeader("Content-Length", "128");
268         post.setEntity(HttpTestUtils.makeBody(128));
269 
270         originResponse.removeHeaders("Cache-Control");
271         originResponse.removeHeaders("Expires");
272 
273         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
274 
275         execute(post);
276 
277         Mockito.verifyNoInteractions(mockCache);
278     }
279 
280     @Test
281     void testResponsesToPUTsAreNotCached() throws Exception {
282 
283         final BasicClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
284         put.setEntity(HttpTestUtils.makeBody(128));
285         put.addHeader("Content-Length", "128");
286 
287         originResponse.setHeader("Cache-Control", "max-age=3600");
288 
289         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
290 
291         execute(put);
292 
293         Mockito.verifyNoInteractions(mockCache);
294     }
295 
296     @Test
297     void testResponsesToDELETEsAreNotCached() throws Exception {
298 
299         request = new BasicClassicHttpRequest("DELETE", "/");
300         originResponse.setHeader("Cache-Control", "max-age=3600");
301 
302         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
303 
304         execute(request);
305 
306         Mockito.verifyNoInteractions(mockCache);
307     }
308 
309     @Test
310     void testResponsesToTRACEsAreNotCached() throws Exception {
311 
312         request = new BasicClassicHttpRequest("TRACE", "/");
313         originResponse.setHeader("Cache-Control", "max-age=3600");
314 
315         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
316 
317         execute(request);
318 
319         Mockito.verifyNoInteractions(mockCache);
320     }
321 
322     @Test
323     void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
324 
325         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
326         originResponse.setHeader("Cache-Control", "max-age=3600");
327         originResponse.setHeader("ETag", "\"etag\"");
328 
329         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
330         req2.setHeader("If-None-Match", "\"etag\"");
331 
332         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
333 
334         execute(req1);
335         final ClassicHttpResponse result = execute(req2);
336 
337         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
338         Assertions.assertNotNull(result.getFirstHeader("Date"));
339         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
340     }
341 
342     @Test
343     void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
344         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
345         originResponse.setHeader("Cache-Control", "max-age=3600");
346         originResponse.setHeader("ETag", "\"etag\"");
347 
348         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
349         req2.setHeader("If-None-Match", "\"etag\"");
350 
351         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
352 
353         execute(req1);
354         final ClassicHttpResponse result = execute(req2);
355 
356         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
357         Assertions.assertNotNull(result.getFirstHeader("ETag"));
358         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
359     }
360 
361     @Test
362     void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid() throws Exception {
363         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
364         originResponse.setHeader("Cache-Control", "max-age=3600");
365         originResponse.setHeader("Content-Location", "http://foo.example.com/other");
366         originResponse.setHeader("ETag", "\"etag\"");
367 
368         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
369         req2.setHeader("If-None-Match", "\"etag\"");
370 
371         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
372 
373         execute(req1);
374         final ClassicHttpResponse result = execute(req2);
375 
376         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
377         Assertions.assertNotNull(result.getFirstHeader("Content-Location"));
378         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
379     }
380 
381     @Test
382     void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer() throws Exception {
383 
384         final Instant now = Instant.now();
385         final Instant inTwoHours = now.plus(2, ChronoUnit.HOURS);
386 
387         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
388         req1.setHeader("Accept-Encoding", "gzip");
389 
390         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
391         resp1.setHeader("ETag", "\"v1\"");
392         resp1.setHeader("Cache-Control", "max-age=7200");
393         resp1.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
394         resp1.setHeader("Vary", "Accept-Encoding");
395         resp1.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
396 
397         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
398         req2.setHeader("Accept-Encoding", "gzip");
399         req2.setHeader("Cache-Control", "no-cache");
400 
401         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
402         resp2.setHeader("ETag", "\"v2\"");
403         resp2.setHeader("Cache-Control", "max-age=3600");
404         resp2.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
405         resp2.setHeader("Vary", "Accept-Encoding");
406         resp2.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
407 
408         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
409         req3.setHeader("Accept-Encoding", "gzip");
410         req3.setHeader("If-None-Match", "\"v2\"");
411 
412         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
413 
414         execute(req1);
415 
416         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
417         execute(req2);
418 
419         final ClassicHttpResponse result = execute(req3);
420 
421         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
422         Assertions.assertNotNull(result.getFirstHeader("Expires"));
423         Assertions.assertNotNull(result.getFirstHeader("Cache-Control"));
424         Assertions.assertNotNull(result.getFirstHeader("Vary"));
425         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
426     }
427 
428     @Test
429     void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders() throws Exception {
430 
431         final Instant now = Instant.now();
432         final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
433 
434         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
435 
436         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
437         resp1.setHeader("ETag", "W/\"v1\"");
438         resp1.setHeader("Allow", "GET,HEAD");
439         resp1.setHeader("Content-Encoding", "x-coding");
440         resp1.setHeader("Content-Language", "en");
441         resp1.setHeader("Content-Length", "128");
442         resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
443         resp1.setHeader("Content-Type", "application/octet-stream");
444         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(oneHourAgo));
445         resp1.setHeader("Cache-Control", "max-age=7200");
446 
447         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
448         req2.setHeader("If-None-Match", "W/\"v1\"");
449 
450         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req1), Mockito.any())).thenReturn(resp1);
451 
452         execute(req1);
453         final ClassicHttpResponse result = execute(req2);
454 
455         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
456         Assertions.assertNull(result.getFirstHeader("Allow"));
457         Assertions.assertNull(result.getFirstHeader("Content-Encoding"));
458         Assertions.assertNull(result.getFirstHeader("Content-Length"));
459         Assertions.assertNull(result.getFirstHeader("Content-MD5"));
460         Assertions.assertNull(result.getFirstHeader("Content-Type"));
461         Assertions.assertNull(result.getFirstHeader("Last-Modified"));
462         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
463     }
464 
465     @Test
466     void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET() throws Exception {
467 
468         // load cache with cacheable entry
469         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
470         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
471         resp1.setHeader("ETag", "\"etag1\"");
472         resp1.setHeader("Cache-Control", "max-age=3600");
473 
474         // force a revalidation
475         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
476         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
477 
478         // unconditional validation doesn't use If-None-Match
479         final ClassicHttpRequest unconditionalValidation = new BasicClassicHttpRequest("GET", "/");
480         // new response to unconditional validation provides new body
481         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
482         resp1.setHeader("ETag", "\"etag2\"");
483         resp1.setHeader("Cache-Control", "max-age=3600");
484 
485         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
486         // this next one will happen once if the cache tries to
487         // conditionally validate, zero if it goes full revalidation
488 
489         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(unconditionalValidation), Mockito.any())).thenReturn(resp2);
490 
491         execute(req1);
492         execute(req2);
493 
494         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
495     }
496 
497     @Test
498     void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
499 
500         final Instant now = Instant.now();
501         final Instant inFiveSeconds = now.plusSeconds(5);
502 
503         final ClassicHttpRequest initialRequest = new BasicClassicHttpRequest("GET", "/");
504 
505         final ClassicHttpResponse cachedResponse = HttpTestUtils.make200Response();
506         cachedResponse.setHeader("Cache-Control", "max-age=3600");
507         cachedResponse.setHeader("ETag", "\"etag\"");
508 
509         final ClassicHttpRequest secondRequest = new BasicClassicHttpRequest("GET", "/");
510         secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
511 
512         final ClassicHttpRequest conditionalValidationRequest = new BasicClassicHttpRequest("GET", "/");
513         conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
514 
515         // to be used if the cache generates a conditional validation
516         final ClassicHttpResponse conditionalResponse = HttpTestUtils.make304Response();
517         conditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
518         conditionalResponse.setHeader("Server", "MockUtils/1.0");
519         conditionalResponse.setHeader("ETag", "\"etag\"");
520         conditionalResponse.setHeader("X-Extra", "junk");
521 
522         // to be used if the cache generates an unconditional validation
523         final ClassicHttpResponse unconditionalResponse = HttpTestUtils.make200Response();
524         unconditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
525         unconditionalResponse.setHeader("ETag", "\"etag\"");
526 
527         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(cachedResponse);
528         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(conditionalValidationRequest), Mockito.any())).thenReturn(conditionalResponse);
529 
530         execute(initialRequest);
531         final ClassicHttpResponse result = execute(secondRequest);
532 
533         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
534 
535         Assertions.assertEquals(DateUtils.formatStandardDate(inFiveSeconds), result.getFirstHeader("Date").getValue());
536         Assertions.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
537     }
538 
539     @Test
540     void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
541 
542         final Instant now = Instant.now();
543         final Instant tenSecondsAgo = now.minusSeconds(10);
544         final Instant nineSecondsAgo = now.minusSeconds(9);
545         final Instant eightSecondsAgo = now.minusSeconds(8);
546 
547         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
548                 Method.GET, "/thing", null,
549                 200, new Header[] {
550                         new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
551                         new BasicHeader("ETag", "\"etag\"")
552                 }, HttpTestUtils.makeNullResource());
553 
554         impl = new CachingExec(mockCache, null, config);
555 
556         request = new BasicClassicHttpRequest("GET", "/thing");
557 
558         final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/thing");
559         validate.setHeader("If-None-Match", "\"etag\"");
560 
561         final ClassicHttpResponse notModified = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
562         notModified.setHeader("Date", DateUtils.formatStandardDate(now));
563         notModified.setHeader("ETag", "\"etag\"");
564 
565         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
566                 new CacheMatch(new CacheHit("key", entry), null));
567         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(validate), Mockito.any())).thenReturn(notModified);
568         final HttpCacheEntry updated = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
569                 Method.GET, "/thing", null,
570                 200, new Header[] {
571                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
572                         new BasicHeader("ETag", "\"etag\"")
573                 }, HttpTestUtils.makeNullResource());
574         Mockito.when(mockCache.update(
575                         Mockito.any(),
576                         Mockito.any(),
577                         Mockito.any(),
578                         Mockito.any(),
579                         Mockito.any(),
580                         Mockito.any()))
581                 .thenReturn(new CacheHit("key", updated));
582 
583         execute(request);
584 
585         Mockito.verify(mockCache).update(
586                 Mockito.any(),
587                 Mockito.eq(host),
588                 RequestEquivalent.eq(request),
589                 ResponseEquivalent.eq(notModified),
590                 Mockito.any(),
591                 Mockito.any());
592     }
593 
594     @Test
595     void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
596 
597         final Instant now = Instant.now();
598         final Instant tenSecondsAgo = now.minusSeconds(10);
599         final Instant nineSecondsAgo = now.plusSeconds(9);
600         final Instant eightSecondsAgo = now.plusSeconds(8);
601 
602         final Header[] hdrs = new Header[] {
603                 new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
604                 new BasicHeader("Cache-Control", "max-age=3600"),
605                 new BasicHeader("Content-Length", "128")
606         };
607 
608         final byte[] bytes = new byte[128];
609         new Random().nextBytes(bytes);
610 
611         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
612 
613         impl = new CachingExec(mockCache, null, config);
614         request = new BasicClassicHttpRequest("GET", "/thing");
615 
616         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
617                 new CacheMatch(new CacheHit("key", entry), null));
618 
619         final ClassicHttpResponse result = execute(request);
620 
621         Assertions.assertEquals(200, result.getCode());
622     }
623 
624     @Test
625     void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
626 
627         final Instant now = Instant.now();
628         final Instant tenSecondsAgo = now.minusSeconds(10);
629         final Instant nineSecondsAgo = now.minusSeconds(9);
630         final Instant eightSecondsAgo = now.minusSeconds(8);
631 
632         final Header[] hdrs = new Header[] {
633                 new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
634                 new BasicHeader("Cache-Control", "max-age=3600"),
635                 new BasicHeader("Content-Length", "128")
636         };
637 
638         final byte[] bytes = new byte[128];
639         new Random().nextBytes(bytes);
640 
641         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
642 
643         impl = new CachingExec(mockCache, null, config);
644         request = new BasicClassicHttpRequest("GET", "/");
645 
646         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
647                 new CacheMatch(new CacheHit("key", entry), null));
648 
649         final ClassicHttpResponse result = execute(request);
650 
651         Assertions.assertEquals(200, result.getCode());
652         // We calculate the age of the cache entry as per RFC 9111:
653         // We first find the "corrected_initial_age" which is the maximum of "apparentAge" and "correctedReceivedAge".
654         // In this case, max(1, 2) = 2 seconds.
655         // We then add the "residentTime" which is "now - responseTime",
656         // which is the current time minus the time the cache entry was created. In this case, that is 8 seconds.
657         // So, the total age is "corrected_initial_age" + "residentTime" = 2 + 8 = 10 seconds.
658         assertThat(result, ContainsHeaderMatcher.contains("Age", "10"));
659     }
660 
661     @Test
662     void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
663 
664         final Instant now = Instant.now();
665         final Instant inFiveSecond = now.plusSeconds(5);
666 
667         // put an entry in the cache
668         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
669 
670         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
671         resp1.setHeader("Date", DateUtils.formatStandardDate(inFiveSecond));
672         resp1.setHeader("ETag", "\"etag1\"");
673         resp1.setHeader("Cache-Control", "max-age=3600");
674         resp1.setHeader("Content-Length", "128");
675 
676         // force another origin hit
677         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
678         req2.setHeader("Cache-Control", "no-cache");
679 
680         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
681         resp2.setHeader("Date", DateUtils.formatStandardDate(now)); // older
682         resp2.setHeader("ETag", "\"etag2\"");
683         resp2.setHeader("Cache-Control", "max-age=3600");
684         resp2.setHeader("Content-Length", "128");
685 
686         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
687 
688         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
689 
690         execute(req1);
691 
692         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
693 
694         execute(req2);
695         final ClassicHttpResponse result = execute(req3);
696         Assertions.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
697     }
698 
699     @Test
700     void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
701 
702         final Instant now = Instant.now();
703         final Instant tenSecondsAgo = now.minusSeconds(10);
704 
705         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
706         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
707         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
708         resp1.setHeader("Cache-Control", "max-age=3600");
709         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
710         resp1.setHeader("ETag", "W/\"etag\"");
711 
712         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
713         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
714 
715         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
716 
717         execute(req1);
718         execute(req2);
719 
720         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
721         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
722 
723         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
724         Assertions.assertEquals(2, allRequests.size());
725         final ClassicHttpRequest validation = allRequests.get(1);
726         boolean foundETag = false;
727         final Iterator<HeaderElement> it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
728         while (it.hasNext()) {
729             final HeaderElement elt = it.next();
730             if ("W/\"etag\"".equals(elt.getName())) {
731                 foundETag = true;
732             }
733         }
734         final Iterator<HeaderElement> it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
735         while (it2.hasNext()) {
736             final HeaderElement elt = it2.next();
737             if ("W/\"etag\"".equals(elt.getName())) {
738                 foundETag = true;
739             }
740         }
741         Assertions.assertTrue(foundETag);
742     }
743 
744     @Test
745     void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache() throws Exception {
746         final Instant now = Instant.now();
747         final Instant tenSecondsAgo = now.minusSeconds(10);
748         final Instant twentySecondsAgo = now.plusSeconds(20);
749 
750         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
751         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
752         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
753         resp1.setHeader("Cache-Control", "max-age=3600");
754         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
755         resp1.setHeader("ETag", "W/\"etag\"");
756 
757         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
758         req2.setHeader("If-None-Match", "W/\"etag\"");
759         req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(twentySecondsAgo));
760 
761         // must hit the origin again for the second request
762         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
763 
764         execute(req1);
765         final ClassicHttpResponse result = execute(req2);
766 
767         Assertions.assertNotEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
768         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
769     }
770 
771     @Test
772     void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache() throws Exception {
773         final Instant now = Instant.now();
774         final Instant tenSecondsAgo = now.minusSeconds(10);
775 
776         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
777         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
778         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
779         resp1.setHeader("Cache-Control", "max-age=3600");
780         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
781         resp1.setHeader("ETag", "W/\"etag\"");
782 
783         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
784         req2.setHeader("If-None-Match", "W/\"etag\"");
785         req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
786 
787         // may hit the origin again for the second request
788         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
789 
790         execute(req1);
791         execute(req2);
792 
793         Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
794         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
795     }
796 
797     @Test
798     void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response() throws Exception {
799         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
800         req.setHeader("Range", "bytes=0-50");
801 
802         final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
803         resp.setHeader("Content-Range", "bytes 0-50/128");
804         resp.setHeader("ETag", "\"etag\"");
805         resp.setHeader("Cache-Control", "max-age=3600");
806 
807         Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp);
808 
809         execute(req);
810 
811         Mockito.verifyNoInteractions(mockCache);
812     }
813 
814     @Test
815     void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
816         originResponse = new BasicClassicHttpResponse(302, "Temporary Redirect");
817         originResponse.setHeader("Location", "http://foo.example.com/other");
818         originResponse.removeHeaders("Expires");
819         originResponse.removeHeaders("Cache-Control");
820 
821         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
822 
823         execute(request);
824         execute(request);
825 
826         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
827     }
828 
829     private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
830         originResponse = HttpTestUtils.make200Response();
831         originResponse.setHeader(header, value);
832 
833         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
834 
835         final ClassicHttpResponse result = execute(request);
836 
837         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
838     }
839 
840     @Test
841     void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
842 
843         final String url = "http://foo.example.com/other";
844         testDoesNotModifyHeaderFromOrigin("Content-Location", url);
845     }
846 
847     @Test
848     void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
849         testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
850     }
851 
852     @Test
853     void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
854         testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
855     }
856 
857     @Test
858     void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
859         final String lm = DateUtils.formatStandardDate(Instant.now());
860         testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
861     }
862 
863     private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
864         originResponse.removeHeaders(header);
865 
866         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
867 
868         final ClassicHttpResponse result = execute(request);
869 
870         Assertions.assertNull(result.getFirstHeader(header));
871     }
872 
873     @Test
874     void testDoesNotAddContentLocationToOriginResponse() throws Exception {
875         testDoesNotAddHeaderToOriginResponse("Content-Location");
876     }
877 
878     @Test
879     void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
880         testDoesNotAddHeaderToOriginResponse("Content-MD5");
881     }
882 
883     @Test
884     void testDoesNotAddEtagToOriginResponse() throws Exception {
885         testDoesNotAddHeaderToOriginResponse("ETag");
886     }
887 
888     @Test
889     void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
890         testDoesNotAddHeaderToOriginResponse("Last-Modified");
891     }
892 
893     private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value) throws Exception {
894 
895         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
896         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
897 
898         originResponse = HttpTestUtils.make200Response();
899         originResponse.setHeader("Cache-Control", "max-age=3600");
900         originResponse.setHeader(header, value);
901 
902         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
903 
904         execute(req1);
905         final ClassicHttpResponse result = execute(req2);
906 
907         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
908     }
909 
910     @Test
911     void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
912         final String url = "http://foo.example.com/other";
913         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
914     }
915 
916     @Test
917     void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
918         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
919     }
920 
921     @Test
922     void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
923         testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
924     }
925 
926     @Test
927     void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
928         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
929         testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
930     }
931 
932     private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
933 
934         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
935         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
936 
937         originResponse.addHeader("Cache-Control", "max-age=3600");
938         originResponse.removeHeaders(header);
939 
940         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
941 
942         execute(req1);
943         final ClassicHttpResponse result = execute(req2);
944 
945         Assertions.assertNull(result.getFirstHeader(header));
946     }
947 
948     @Test
949     void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
950         testDoesNotAddHeaderOnCacheHit("Content-Location");
951     }
952 
953     @Test
954     void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
955         testDoesNotAddHeaderOnCacheHit("Content-MD5");
956     }
957 
958     @Test
959     void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
960         testDoesNotAddHeaderOnCacheHit("ETag");
961     }
962 
963     @Test
964     void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
965         testDoesNotAddHeaderOnCacheHit("Last-Modified");
966     }
967 
968     private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
969         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
970         req.setEntity(HttpTestUtils.makeBody(128));
971         req.setHeader("Content-Length","128");
972         req.setHeader(header,value);
973 
974         execute(req);
975 
976         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
977         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
978 
979         final ClassicHttpRequest captured = reqCapture.getValue();
980         Assertions.assertEquals(value, captured.getFirstHeader(header).getValue());
981     }
982 
983     @Test
984     void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
985         final String url = "http://foo.example.com/other";
986         testDoesNotModifyHeaderOnRequest("Content-Location",url);
987     }
988 
989     @Test
990     void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
991         testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
992     }
993 
994     @Test
995     void testDoesNotModifyETagHeaderOnRequest() throws Exception {
996         testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
997     }
998 
999     @Test
1000     void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
1001         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
1002         testDoesNotModifyHeaderOnRequest("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
1003     }
1004 
1005     private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
1006         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
1007         req.setEntity(HttpTestUtils.makeBody(128));
1008         req.setHeader("Content-Length","128");
1009         req.removeHeaders(header);
1010 
1011         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1012 
1013         execute(req);
1014 
1015         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1016         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
1017 
1018         final ClassicHttpRequest captured = reqCapture.getValue();
1019         Assertions.assertNull(captured.getFirstHeader(header));
1020     }
1021 
1022     @Test
1023     void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
1024         testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
1025     }
1026 
1027     @Test
1028     void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
1029         testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
1030     }
1031 
1032     @Test
1033     void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
1034         testDoesNotAddHeaderToRequestIfNotPresent("ETag");
1035     }
1036 
1037     @Test
1038     void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
1039         testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
1040     }
1041 
1042     @Test
1043     void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
1044         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
1045         testDoesNotModifyHeaderFromOrigin("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
1046     }
1047 
1048     @Test
1049     void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
1050         final Instant inTenSeconds = Instant.now().plusSeconds(10);
1051         testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", DateUtils.formatStandardDate(inTenSeconds));
1052     }
1053 
1054     @Test
1055     void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
1056         originResponse.removeHeaders("Expires");
1057 
1058         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1059 
1060         final ClassicHttpResponse result = execute(request);
1061 
1062         final Header expHdr = result.getFirstHeader("Expires");
1063         if (expHdr != null) {
1064             Assertions.assertEquals(result.getFirstHeader("Date").getValue(),
1065                                 expHdr.getValue());
1066         }
1067     }
1068 
1069     private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
1070         originResponse.addHeader("Cache-Control","no-transform");
1071         originResponse.setHeader(header, value);
1072 
1073         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1074 
1075         final ClassicHttpResponse result = execute(request);
1076 
1077         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
1078     }
1079 
1080     @Test
1081     void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
1082         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
1083     }
1084 
1085     @Test
1086     void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
1087         request.setHeader("If-Range","\"etag\"");
1088         request.setHeader("Range","bytes=0-49");
1089 
1090         originResponse = new BasicClassicHttpResponse(206, "Partial Content");
1091         originResponse.setEntity(HttpTestUtils.makeBody(50));
1092         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
1093     }
1094 
1095     @Test
1096     void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
1097         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
1098     }
1099 
1100     private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
1101         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1102         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1103 
1104         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
1105         originResponse.setHeader(header, value);
1106 
1107         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1108 
1109         execute(req1);
1110         final ClassicHttpResponse result = execute(req2);
1111 
1112         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
1113     }
1114 
1115     @Test
1116     void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
1117         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
1118     }
1119 
1120     @Test
1121     void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
1122         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
1123     }
1124 
1125     @Test
1126     void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1127         originResponse.addHeader("Cache-Control","no-transform");
1128         testDoesNotAddHeaderToOriginResponse("Content-Encoding");
1129     }
1130 
1131     @Test
1132     void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1133         originResponse.addHeader("Cache-Control","no-transform");
1134         testDoesNotAddHeaderToOriginResponse("Content-Range");
1135     }
1136 
1137     @Test
1138     void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1139         originResponse.addHeader("Cache-Control","no-transform");
1140         testDoesNotAddHeaderToOriginResponse("Content-Type");
1141     }
1142 
1143     /* no add on cache hit with no-transform */
1144     @Test
1145     void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1146         originResponse.addHeader("Cache-Control","no-transform");
1147         testDoesNotAddHeaderOnCacheHit("Content-Encoding");
1148     }
1149 
1150     @Test
1151     void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1152         originResponse.addHeader("Cache-Control","no-transform");
1153         testDoesNotAddHeaderOnCacheHit("Content-Range");
1154     }
1155 
1156     @Test
1157     void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1158         originResponse.addHeader("Cache-Control","no-transform");
1159         testDoesNotAddHeaderOnCacheHit("Content-Type");
1160     }
1161 
1162     /* no modify on request */
1163     @Test
1164     void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
1165         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
1166     }
1167 
1168     @Test
1169     void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
1170         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
1171     }
1172 
1173     @Test
1174     void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
1175         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
1176     }
1177 
1178     @Test
1179     void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
1180         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
1181     }
1182 
1183     @Test
1184     void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
1185         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
1186     }
1187 
1188     @Test
1189     void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
1190         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
1191     }
1192 
1193     @Test
1194     void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
1195         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1196         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1197         resp1.setHeader("Cache-Control","max-age=3600");
1198         resp1.setHeader("ETag","\"etag\"");
1199 
1200         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1201         req2.setHeader("Cache-Control","max-age=0, max-stale=0");
1202         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1203 
1204         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1205 
1206         execute(req1);
1207 
1208         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1209 
1210         final ClassicHttpResponse result = execute(req2);
1211 
1212         try (final InputStream i1 = resp1.getEntity().getContent();
1213              final InputStream i2 = result.getEntity().getContent()) {
1214             int b1, b2;
1215             while((b1 = i1.read()) != -1) {
1216                 b2 = i2.read();
1217                 Assertions.assertEquals(b1, b2);
1218             }
1219             b2 = i2.read();
1220             Assertions.assertEquals(-1, b2);
1221         }
1222     }
1223 
1224     private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
1225         r.setHeader("Allow","GET");
1226         r.setHeader("Content-Encoding","gzip");
1227         r.setHeader("Content-Language","en");
1228         r.setHeader("Content-Length", "128");
1229         r.setHeader("Content-Location","http://foo.example.com/other");
1230         r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1231         r.setHeader("Content-Type", "text/html;charset=utf-8");
1232         r.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(10)));
1233         r.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now().minusSeconds(10)));
1234         r.setHeader("Location", "http://foo.example.com/other2");
1235         r.setHeader("Retry-After","180");
1236     }
1237 
1238     @Test
1239     void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
1240         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1241         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1242         resp1.setHeader("Cache-Control","max-age=3600");
1243         resp1.setHeader("ETag","\"etag\"");
1244         decorateWithEndToEndHeaders(resp1);
1245 
1246         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1247         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1248         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1249         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1250         resp2.setHeader("Server", "MockServer/1.0");
1251 
1252         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1253 
1254         execute(req1);
1255 
1256         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp2);
1257         final ClassicHttpResponse result = execute(req2);
1258 
1259         final String[] endToEndHeaders = {
1260             "Cache-Control", "ETag", "Allow", "Content-Encoding",
1261             "Content-Language", "Content-Length", "Content-Location",
1262             "Content-MD5", "Content-Type", "Expires", "Last-Modified",
1263             "Location", "Retry-After"
1264         };
1265         for(final String h : endToEndHeaders) {
1266             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
1267                                 HttpTestUtils.getCanonicalHeaderValue(result, h));
1268         }
1269     }
1270 
1271     @Test
1272     void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
1273 
1274         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1275         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1276         resp1.setHeader("Cache-Control","max-age=3600");
1277         resp1.setHeader("ETag","\"etag\"");
1278         decorateWithEndToEndHeaders(resp1);
1279 
1280         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1281         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1282         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1283         resp2.setHeader("Cache-Control", "max-age=1800");
1284         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1285         resp2.setHeader("Server", "MockServer/1.0");
1286         resp2.setHeader("Allow", "GET,HEAD");
1287         resp2.setHeader("Content-Language", "en,en-us");
1288         resp2.setHeader("Content-Location", "http://foo.example.com/new");
1289         resp2.setHeader("Content-Type","text/html");
1290         resp2.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(5)));
1291         resp2.setHeader("Location", "http://foo.example.com/new2");
1292         resp2.setHeader("Retry-After","120");
1293 
1294         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1295 
1296         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1297 
1298         execute(req1);
1299 
1300         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1301         final ClassicHttpResponse result1 = execute(req2);
1302         final ClassicHttpResponse result2 = execute(req3);
1303 
1304         final String[] endToEndHeaders = {
1305             "Date", "Cache-Control", "Allow", "Content-Language",
1306             "Content-Location", "Content-Type", "Expires", "Location",
1307             "Retry-After"
1308         };
1309         for(final String h : endToEndHeaders) {
1310             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1311                                 HttpTestUtils.getCanonicalHeaderValue(result1, h));
1312             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1313                                 HttpTestUtils.getCanonicalHeaderValue(result2, h));
1314         }
1315     }
1316 
1317     @Test
1318     void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
1319         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1320         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1321         resp1.addHeader("Cache-Control","max-age=3600");
1322         resp1.addHeader("Cache-Control","public");
1323         resp1.setHeader("ETag","\"etag\"");
1324 
1325         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1326         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1327         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1328         resp2.setHeader("Cache-Control", "max-age=1800");
1329 
1330         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1331 
1332         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1333 
1334         execute(req1);
1335 
1336         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1337 
1338         final ClassicHttpResponse result1 = execute(req2);
1339         final ClassicHttpResponse result2 = execute(req3);
1340 
1341         final String h = "Cache-Control";
1342         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1343                             HttpTestUtils.getCanonicalHeaderValue(result1, h));
1344         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1345                             HttpTestUtils.getCanonicalHeaderValue(result2, h));
1346     }
1347 
1348     @Test
1349     void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch() throws Exception {
1350 
1351         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1352         req1.setHeader("Accept-Encoding","gzip");
1353 
1354         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1355         resp1.setHeader("ETag","\"etag1\"");
1356         resp1.setHeader("Cache-Control","max-age=3600");
1357         resp1.setHeader("Vary","Accept-Encoding");
1358 
1359         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1360 
1361         execute(req1);
1362 
1363         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1364         req2.removeHeaders("Accept-Encoding");
1365 
1366         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1367         resp2.setHeader("ETag","\"etag1\"");
1368         resp2.setHeader("Cache-Control","max-age=3600");
1369 
1370         // not allowed to have a cache hit; must forward request
1371         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1372 
1373         execute(req2);
1374 
1375         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1376     }
1377 
1378     @Test
1379     void testCannotServeFromCacheForVaryStar() throws Exception {
1380         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1381 
1382         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1383         resp1.setHeader("ETag","\"etag1\"");
1384         resp1.setHeader("Cache-Control","max-age=3600");
1385         resp1.setHeader("Vary","*");
1386 
1387         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1388 
1389         execute(req1);
1390 
1391         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1392 
1393         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1394         resp2.setHeader("ETag","\"etag1\"");
1395         resp2.setHeader("Cache-Control","max-age=3600");
1396 
1397         // not allowed to have a cache hit; must forward request
1398         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1399 
1400         execute(req2);
1401 
1402         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1403     }
1404 
1405     @Test
1406     void testNonMatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
1407 
1408         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1409         req1.setHeader("User-Agent","MyBrowser/1.0");
1410 
1411         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1412         resp1.setHeader("ETag","\"etag1\"");
1413         resp1.setHeader("Cache-Control","max-age=3600");
1414         resp1.setHeader("Vary","User-Agent");
1415         resp1.setHeader("Content-Type","application/octet-stream");
1416 
1417         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1418         req2.setHeader("User-Agent","MyBrowser/1.5");
1419 
1420         final ClassicHttpResponse resp200 = HttpTestUtils.make200Response();
1421         resp200.setHeader("ETag","\"etag1\"");
1422         resp200.setHeader("Vary","User-Agent");
1423 
1424         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1425 
1426         execute(req1);
1427 
1428         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp200);
1429 
1430         final ClassicHttpResponse result = execute(req2);
1431 
1432         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
1433 
1434         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1435 
1436         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
1437     }
1438 
1439     protected void testUnsafeOperationInvalidatesCacheForThatUri(
1440             final ClassicHttpRequest unsafeReq) throws Exception {
1441         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1442         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1443         resp1.setHeader("Cache-Control","public, max-age=3600");
1444 
1445         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1446 
1447         execute(req1);
1448 
1449         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1450 
1451         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1452 
1453         execute(unsafeReq);
1454 
1455         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1456         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1457         resp3.setHeader("Cache-Control","public, max-age=3600");
1458 
1459         // this origin request MUST happen due to invalidation
1460         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1461 
1462         execute(req3);
1463     }
1464 
1465     protected ClassicHttpRequest makeRequestWithBody(final String method, final String requestUri) {
1466         final ClassicHttpRequest req = new BasicClassicHttpRequest(method, requestUri);
1467         final int nbytes = 128;
1468         req.setEntity(HttpTestUtils.makeBody(nbytes));
1469         req.setHeader("Content-Length", Long.toString(nbytes));
1470         return req;
1471     }
1472 
1473     @Test
1474     void testPutToUriInvalidatesCacheForThatUri() throws Exception {
1475         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1476         testUnsafeOperationInvalidatesCacheForThatUri(req);
1477     }
1478 
1479     @Test
1480     void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
1481         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE","/");
1482         testUnsafeOperationInvalidatesCacheForThatUri(req);
1483     }
1484 
1485     @Test
1486     void testPostToUriInvalidatesCacheForThatUri() throws Exception {
1487         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1488         testUnsafeOperationInvalidatesCacheForThatUri(req);
1489     }
1490 
1491     protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
1492             final ClassicHttpRequest unsafeReq) throws Exception {
1493         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
1494         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1495         resp1.setHeader("Cache-Control","public, max-age=3600");
1496 
1497         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1498 
1499         execute(req1);
1500 
1501         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1502 
1503         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1504 
1505         execute(unsafeReq);
1506 
1507         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
1508         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1509         resp3.setHeader("Cache-Control","public, max-age=3600");
1510 
1511         // this origin request MUST happen due to invalidation
1512         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1513 
1514         execute(req3);
1515     }
1516 
1517     protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
1518             final ClassicHttpRequest unsafeReq) throws Exception {
1519         unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
1520         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1521     }
1522 
1523     protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
1524             final ClassicHttpRequest unsafeReq) throws Exception {
1525         unsafeReq.setHeader("Content-Location","/content");
1526         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1527     }
1528 
1529     protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
1530             final ClassicHttpRequest unsafeReq) throws Exception {
1531         unsafeReq.setHeader("Location","http://foo.example.com/content");
1532         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1533     }
1534 
1535     @Test
1536     void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1537         final ClassicHttpRequest req2 = makeRequestWithBody("PUT","/");
1538         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
1539     }
1540 
1541     @Test
1542     void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1543         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1544         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1545     }
1546 
1547     @Test
1548     void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
1549         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1550         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1551     }
1552 
1553     @Test
1554     void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1555         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1556         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
1557     }
1558 
1559     @Test
1560     void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
1561         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1562         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1563     }
1564 
1565     @Test
1566     void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1567         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1568         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1569     }
1570 
1571     @Test
1572     void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1573         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1574         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
1575     }
1576 
1577     @Test
1578     void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1579         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1580         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1581     }
1582 
1583     @Test
1584     void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
1585         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1586         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1587     }
1588 
1589     private void testRequestIsWrittenThroughToOrigin(final ClassicHttpRequest req) throws Exception {
1590         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1591         final ClassicHttpRequest wrapper = req;
1592         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(wrapper), Mockito.any())).thenReturn(resp);
1593 
1594         execute(wrapper);
1595     }
1596 
1597     @Test
1598     void testOPTIONSRequestsAreWrittenThroughToOrigin() throws Exception {
1599         final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS","*");
1600         testRequestIsWrittenThroughToOrigin(req);
1601     }
1602 
1603     @Test
1604     void testPOSTRequestsAreWrittenThroughToOrigin() throws Exception {
1605         final ClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
1606         req.setEntity(HttpTestUtils.makeBody(128));
1607         req.setHeader("Content-Length","128");
1608         testRequestIsWrittenThroughToOrigin(req);
1609     }
1610 
1611     @Test
1612     void testPUTRequestsAreWrittenThroughToOrigin() throws Exception {
1613         final ClassicHttpRequest req = new BasicClassicHttpRequest("PUT","/");
1614         req.setEntity(HttpTestUtils.makeBody(128));
1615         req.setHeader("Content-Length","128");
1616         testRequestIsWrittenThroughToOrigin(req);
1617     }
1618 
1619     @Test
1620     void testDELETERequestsAreWrittenThroughToOrigin() throws Exception {
1621         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1622         testRequestIsWrittenThroughToOrigin(req);
1623     }
1624 
1625     @Test
1626     void testTRACERequestsAreWrittenThroughToOrigin() throws Exception {
1627         final ClassicHttpRequest req = new BasicClassicHttpRequest("TRACE","/");
1628         testRequestIsWrittenThroughToOrigin(req);
1629     }
1630 
1631     @Test
1632     void testCONNECTRequestsAreWrittenThroughToOrigin() throws Exception {
1633         final ClassicHttpRequest req = new BasicClassicHttpRequest("CONNECT","/");
1634         testRequestIsWrittenThroughToOrigin(req);
1635     }
1636 
1637     @Test
1638     void testUnknownMethodRequestsAreWrittenThroughToOrigin() throws Exception {
1639         final ClassicHttpRequest req = new BasicClassicHttpRequest("UNKNOWN","/");
1640         testRequestIsWrittenThroughToOrigin(req);
1641     }
1642 
1643     @Test
1644     void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
1645         final String reallyOldAge = "1" + Long.MAX_VALUE;
1646         originResponse.setHeader("Age",reallyOldAge);
1647 
1648         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1649 
1650         final ClassicHttpResponse result = execute(request);
1651 
1652         Assertions.assertEquals(reallyOldAge,
1653                             result.getFirstHeader("Age").getValue());
1654     }
1655 
1656     @Test
1657     void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
1658         final String allowHeaderValue = "GET, HEAD, FOOBAR";
1659         originResponse.setHeader("Allow",allowHeaderValue);
1660         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1661         final ClassicHttpResponse result = execute(request);
1662         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
1663                             HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
1664     }
1665 
1666     protected void testSharedCacheRevalidatesAuthorizedResponse(
1667             final ClassicHttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception {
1668         if (config.isSharedCache()) {
1669             final String authorization = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
1670             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1671             req1.setHeader("Authorization",authorization);
1672 
1673             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1674             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1675             resp2.setHeader("Cache-Control","max-age=3600");
1676 
1677             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
1678 
1679             execute(req1);
1680 
1681             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1682 
1683             execute(req2);
1684 
1685             Mockito.verify(mockExecChain, Mockito.atLeast(1 + minTimes)).proceed(Mockito.any(), Mockito.any());
1686             Mockito.verify(mockExecChain, Mockito.atMost(1 + maxTimes)).proceed(Mockito.any(), Mockito.any());
1687         }
1688     }
1689 
1690     @Test
1691     void testSharedCacheMustNotNormallyCacheAuthorizedResponses() throws Exception {
1692         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1693         resp.setHeader("Cache-Control","max-age=3600");
1694         resp.setHeader("ETag","\"etag\"");
1695         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
1696     }
1697 
1698     @Test
1699     void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader() throws Exception {
1700         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1701         resp.setHeader("Cache-Control","s-maxage=3600");
1702         resp.setHeader("ETag","\"etag\"");
1703         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1704     }
1705 
1706     @Test
1707     void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero() throws Exception {
1708         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1709         resp.setHeader("Cache-Control","s-maxage=0");
1710         resp.setHeader("ETag","\"etag\"");
1711         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
1712     }
1713 
1714     @Test
1715     void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate() throws Exception {
1716         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1717         resp.setHeader("Cache-Control","must-revalidate");
1718         resp.setHeader("ETag","\"etag\"");
1719         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1720     }
1721 
1722     @Test
1723     void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic() throws Exception {
1724         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1725         resp.setHeader("Cache-Control","public");
1726         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1727     }
1728 
1729     protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
1730             final ClassicHttpResponse authorizedResponse) throws Exception {
1731         if (config.isSharedCache()) {
1732             final String authorization1 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
1733             final String authorization2 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Qy";
1734 
1735             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1736             req1.setHeader("Authorization",authorization1);
1737 
1738             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1739             req2.setHeader("Authorization",authorization2);
1740 
1741             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1742 
1743             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
1744 
1745             execute(req1);
1746 
1747             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1748 
1749             execute(req2);
1750 
1751             final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1752             Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1753 
1754             final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
1755             Assertions.assertEquals(2, allRequests.size());
1756 
1757             final ClassicHttpRequest captured = allRequests.get(1);
1758             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
1759                     HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
1760         }
1761     }
1762 
1763     @Test
1764     void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge() throws Exception {
1765         final Instant now = Instant.now();
1766         final Instant tenSecondsAgo = now.minusSeconds(10);
1767         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1768         resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1769         resp1.setHeader("ETag","\"etag\"");
1770         resp1.setHeader("Cache-Control","s-maxage=5");
1771 
1772         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
1773     }
1774 
1775     @Test
1776     void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate() throws Exception {
1777         final Instant now = Instant.now();
1778         final Instant tenSecondsAgo = now.minusSeconds(10);
1779         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1780         resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1781         resp1.setHeader("ETag","\"etag\"");
1782         resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
1783 
1784         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
1785     }
1786 
1787     protected void testCacheIsNotUsedWhenRespondingToRequest(final ClassicHttpRequest req) throws Exception {
1788         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1789         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1790         resp1.setHeader("Etag","\"etag\"");
1791         resp1.setHeader("Cache-Control","max-age=3600");
1792 
1793         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1794 
1795         execute(req1);
1796 
1797         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1798         resp2.setHeader("Etag","\"etag2\"");
1799         resp2.setHeader("Cache-Control","max-age=1200");
1800 
1801         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1802 
1803         final ClassicHttpResponse result = execute(req);
1804 
1805         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1806 
1807         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1808         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1809 
1810         final ClassicHttpRequest captured = reqCapture.getValue();
1811         Assertions.assertTrue(HttpTestUtils.equivalent(req, captured));
1812     }
1813 
1814     @Test
1815     void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() throws Exception {
1816         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
1817         req.setHeader("Cache-Control","no-cache");
1818         testCacheIsNotUsedWhenRespondingToRequest(req);
1819     }
1820 
1821     protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
1822             final ClassicHttpResponse staleResponse) throws Exception {
1823         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1824 
1825         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1826         req2.setHeader("Cache-Control","max-stale=3600");
1827         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1828         resp2.setHeader("ETag","\"etag2\"");
1829         resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
1830 
1831         // this request MUST happen
1832         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
1833 
1834         execute(req1);
1835 
1836         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1837 
1838         execute(req2);
1839 
1840         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1841         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1842 
1843         final ClassicHttpRequest reval = reqCapture.getValue();
1844         boolean foundMaxAge0 = false;
1845         final Iterator<HeaderElement> it = MessageSupport.iterate(reval, HttpHeaders.CACHE_CONTROL);
1846         while (it.hasNext()) {
1847             final HeaderElement elt = it.next();
1848             if ("max-age".equalsIgnoreCase(elt.getName())
1849                     && "0".equals(elt.getValue())) {
1850                 foundMaxAge0 = true;
1851             }
1852         }
1853         Assertions.assertTrue(foundMaxAge0);
1854     }
1855 
1856     @Test
1857     void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
1858         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1859         final Instant now = Instant.now();
1860         final Instant tenSecondsAgo = now.minusSeconds(10);
1861         response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1862         response.setHeader("ETag","\"etag1\"");
1863         response.setHeader("Cache-Control","max-age=5, must-revalidate");
1864 
1865         testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
1866     }
1867 
1868     protected void testGenerates504IfCannotRevalidateStaleResponse(
1869             final ClassicHttpResponse staleResponse) throws Exception {
1870         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1871 
1872         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1873 
1874         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
1875 
1876         execute(req1);
1877 
1878         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new SocketTimeoutException());
1879 
1880         final ClassicHttpResponse result = execute(req2);
1881 
1882         Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
1883                             result.getCode());
1884     }
1885 
1886     @Test
1887     void testGenerates504IfCannotRevalidateAMustRevalidateEntry() throws Exception {
1888         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1889         final Instant now = Instant.now();
1890         final Instant tenSecondsAgo = now.minusSeconds(10);
1891         resp1.setHeader("ETag","\"etag\"");
1892         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1893         resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
1894 
1895         testGenerates504IfCannotRevalidateStaleResponse(resp1);
1896     }
1897 
1898     @Test
1899     void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
1900         if (config.isSharedCache()) {
1901             final ClassicHttpResponse response = HttpTestUtils.make200Response();
1902             final Instant now = Instant.now();
1903             final Instant tenSecondsAgo = now.minusSeconds(10);
1904             response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1905             response.setHeader("ETag","\"etag1\"");
1906             response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
1907 
1908             testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
1909         }
1910     }
1911 
1912     @Test
1913     void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry() throws Exception {
1914         if (config.isSharedCache()) {
1915             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1916             final Instant now = Instant.now();
1917             final Instant tenSecondsAgo = now.minusSeconds(10);
1918             resp1.setHeader("ETag","\"etag\"");
1919             resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1920             resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
1921 
1922             testGenerates504IfCannotRevalidateStaleResponse(resp1);
1923         }
1924     }
1925 
1926     @Test
1927     void testCacheControlPrivateIsNotCacheableBySharedCache() throws Exception {
1928         if (config.isSharedCache()) {
1929             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1930             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1931             resp1.setHeader("Cache-Control", "private,max-age=3600");
1932 
1933             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1934 
1935             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1936             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1937             // this backend request MUST happen
1938             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1939 
1940             execute(req1);
1941             execute(req2);
1942         }
1943     }
1944 
1945     @Test
1946     void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() throws Exception {
1947         if (config.isSharedCache()) {
1948             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1949             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1950             resp1.setHeader("X-Personal", "stuff");
1951             resp1.setHeader("Cache-Control", "private=\"X-Personal\",s-maxage=3600");
1952 
1953             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1954 
1955             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1956             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1957 
1958             // this backend request MAY happen
1959             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1960 
1961             execute(req1);
1962             final ClassicHttpResponse result = execute(req2);
1963             Assertions.assertNull(result.getFirstHeader("X-Personal"));
1964 
1965             Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
1966             Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
1967         }
1968     }
1969 
1970     @Test
1971     void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() throws Exception {
1972         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1973         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1974         resp1.setHeader("ETag","\"etag\"");
1975         resp1.setHeader("Cache-Control","no-cache");
1976 
1977         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1978 
1979         execute(req1);
1980 
1981         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1982         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1983 
1984         // this MUST happen
1985         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1986 
1987         execute(req2);
1988 
1989         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1990     }
1991 
1992     @Test
1993     void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications() throws Exception {
1994         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1995         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1996         resp1.setHeader("ETag","\"etag\"");
1997         resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
1998 
1999         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2000 
2001         execute(req1);
2002 
2003         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2004         req2.setHeader("Cache-Control","max-stale=7200");
2005         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2006 
2007         // this MUST happen
2008         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2009 
2010         execute(req2);
2011     }
2012 
2013     @Test
2014     void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exception {
2015         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2016         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2017         resp1.setHeader("ETag","\"etag\"");
2018         resp1.setHeader("X-Stuff","things");
2019         resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
2020 
2021         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2022 
2023         execute(req1);
2024 
2025         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2026         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2027         resp2.setHeader("ETag","\"etag\"");
2028         resp2.setHeader("X-Stuff","things");
2029         resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
2030 
2031         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2032 
2033         final ClassicHttpResponse result = execute(req2);
2034 
2035         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
2036         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
2037 
2038         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
2039         if (allRequests.isEmpty()) {
2040             Assertions.assertNull(result.getFirstHeader("X-Stuff"));
2041         }
2042     }
2043 
2044     @Test
2045     void testNoStoreOnRequestIsNotStoredInCache() throws Exception {
2046         request.setHeader("Cache-Control","no-store");
2047         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2048 
2049         execute(request);
2050 
2051         Mockito.verifyNoInteractions(mockCache);
2052     }
2053 
2054     @Test
2055     void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable() throws Exception {
2056         request.setHeader("Cache-Control","no-store");
2057         originResponse.setHeader("Cache-Control","max-age=3600");
2058         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2059 
2060         execute(request);
2061 
2062         Mockito.verifyNoInteractions(mockCache);
2063     }
2064 
2065     @Test
2066     void testNoStoreOnResponseIsNotStoredInCache() throws Exception {
2067         originResponse.setHeader("Cache-Control","no-store");
2068         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2069 
2070         execute(request);
2071 
2072         Mockito.verifyNoInteractions(mockCache);
2073     }
2074 
2075     @Test
2076     void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators() throws Exception {
2077         originResponse.setHeader("Cache-Control","no-store,max-age=3600");
2078         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2079 
2080         execute(request);
2081 
2082         Mockito.verifyNoInteractions(mockCache);
2083     }
2084 
2085     @Test
2086     void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved() throws Exception {
2087         originResponse.addHeader("Content-Encoding","gzip");
2088         originResponse.addHeader("Content-Encoding","deflate");
2089         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2090 
2091         final ClassicHttpResponse result = execute(request);
2092         int total_encodings = 0;
2093         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
2094         while (it.hasNext()) {
2095             final HeaderElement elt = it.next();
2096             switch(total_encodings) {
2097                 case 0:
2098                     Assertions.assertEquals("gzip", elt.getName());
2099                     break;
2100                 case 1:
2101                     Assertions.assertEquals("deflate", elt.getName());
2102                     break;
2103                 default:
2104                     Assertions.fail("too many encodings");
2105             }
2106             total_encodings++;
2107         }
2108         Assertions.assertEquals(2, total_encodings);
2109     }
2110 
2111     @Test
2112     void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved() throws Exception {
2113         originResponse.addHeader("Content-Encoding","gzip,deflate");
2114         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2115 
2116         final ClassicHttpResponse result = execute(request);
2117         int total_encodings = 0;
2118         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
2119         while (it.hasNext()) {
2120             final HeaderElement elt = it.next();
2121             switch(total_encodings) {
2122                 case 0:
2123                     Assertions.assertEquals("gzip", elt.getName());
2124                     break;
2125                 case 1:
2126                     Assertions.assertEquals("deflate", elt.getName());
2127                     break;
2128                 default:
2129                     Assertions.fail("too many encodings");
2130             }
2131             total_encodings++;
2132         }
2133         Assertions.assertEquals(2, total_encodings);
2134     }
2135 
2136     @Test
2137     void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource() throws Exception {
2138         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/foo");
2139         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2140         resp1.setHeader("Cache-Control","public,max-age=3600");
2141         resp1.setHeader("Etag","\"etag\"");
2142         resp1.setHeader("Content-Location","http://foo.example.com/bar");
2143 
2144         execute(req1);
2145 
2146         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/bar");
2147         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2148         resp2.setHeader("Cache-Control","public,max-age=3600");
2149         resp2.setHeader("Etag","\"etag\"");
2150 
2151         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2152         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2153 
2154         execute(req2);
2155     }
2156 
2157     @Test
2158     void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throws Exception {
2159         originResponse.removeHeaders("Date");
2160         originResponse.setHeader("Cache-Control","public");
2161         originResponse.setHeader("ETag","\"etag\"");
2162 
2163         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2164 
2165         final ClassicHttpResponse result = execute(request);
2166         Assertions.assertNotNull(result.getFirstHeader("Date"));
2167     }
2168 
2169     private void testInvalidExpiresHeaderIsTreatedAsStale(
2170             final String expiresHeader) throws Exception {
2171         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2172         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2173         resp1.setHeader("Cache-Control","public");
2174         resp1.setHeader("ETag","\"etag\"");
2175         resp1.setHeader("Expires", expiresHeader);
2176 
2177         execute(req1);
2178 
2179         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2180         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2181 
2182         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2183         // second request to origin MUST happen
2184         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2185 
2186         execute(req2);
2187     }
2188 
2189     @Test
2190     void testMalformedExpiresHeaderIsTreatedAsStale() throws Exception {
2191         testInvalidExpiresHeaderIsTreatedAsStale("garbage");
2192     }
2193 
2194     @Test
2195     void testExpiresZeroHeaderIsTreatedAsStale() throws Exception {
2196         testInvalidExpiresHeaderIsTreatedAsStale("0");
2197     }
2198 
2199     @Test
2200     void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exception {
2201         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2202         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2203         resp1.setHeader("Cache-Control","public");
2204         resp1.setHeader("ETag","\"etag\"");
2205         resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
2206 
2207         execute(req1);
2208 
2209         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2210         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2211 
2212         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2213         // second request to origin MUST happen
2214         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2215 
2216         execute(req2);
2217     }
2218 
2219     @Test
2220     void testDoesNotModifyServerResponseHeader() throws Exception {
2221         final String server = "MockServer/1.0";
2222         originResponse.setHeader("Server", server);
2223 
2224         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2225 
2226         final ClassicHttpResponse result = execute(request);
2227         Assertions.assertEquals(server, result.getFirstHeader("Server").getValue());
2228     }
2229 
2230 }