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.HttpCacheEntry;
43  import org.apache.hc.client5.http.classic.ExecChain;
44  import org.apache.hc.client5.http.classic.ExecRuntime;
45  import org.apache.hc.client5.http.protocol.HttpClientContext;
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  public 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      HttpClientContext 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      public 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 = HttpClientContext.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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public 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     public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET() throws Exception {
467 
468         final Instant now = Instant.now();
469 
470         // load cache with cacheable entry
471         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
472         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
473         resp1.setHeader("ETag", "\"etag1\"");
474         resp1.setHeader("Cache-Control", "max-age=3600");
475 
476         // force a revalidation
477         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
478         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
479 
480         // unconditional validation doesn't use If-None-Match
481         final ClassicHttpRequest unconditionalValidation = new BasicClassicHttpRequest("GET", "/");
482         // new response to unconditional validation provides new body
483         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
484         resp1.setHeader("ETag", "\"etag2\"");
485         resp1.setHeader("Cache-Control", "max-age=3600");
486 
487         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
488         // this next one will happen once if the cache tries to
489         // conditionally validate, zero if it goes full revalidation
490 
491         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(unconditionalValidation), Mockito.any())).thenReturn(resp2);
492 
493         execute(req1);
494         execute(req2);
495 
496         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
497     }
498 
499     @Test
500     public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
501 
502         final Instant now = Instant.now();
503         final Instant inFiveSeconds = now.plusSeconds(5);
504 
505         final ClassicHttpRequest initialRequest = new BasicClassicHttpRequest("GET", "/");
506 
507         final ClassicHttpResponse cachedResponse = HttpTestUtils.make200Response();
508         cachedResponse.setHeader("Cache-Control", "max-age=3600");
509         cachedResponse.setHeader("ETag", "\"etag\"");
510 
511         final ClassicHttpRequest secondRequest = new BasicClassicHttpRequest("GET", "/");
512         secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
513 
514         final ClassicHttpRequest conditionalValidationRequest = new BasicClassicHttpRequest("GET", "/");
515         conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
516 
517         // to be used if the cache generates a conditional validation
518         final ClassicHttpResponse conditionalResponse = HttpTestUtils.make304Response();
519         conditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
520         conditionalResponse.setHeader("Server", "MockUtils/1.0");
521         conditionalResponse.setHeader("ETag", "\"etag\"");
522         conditionalResponse.setHeader("X-Extra", "junk");
523 
524         // to be used if the cache generates an unconditional validation
525         final ClassicHttpResponse unconditionalResponse = HttpTestUtils.make200Response();
526         unconditionalResponse.setHeader("Date", DateUtils.formatStandardDate(inFiveSeconds));
527         unconditionalResponse.setHeader("ETag", "\"etag\"");
528 
529         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(cachedResponse);
530         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(conditionalValidationRequest), Mockito.any())).thenReturn(conditionalResponse);
531 
532         execute(initialRequest);
533         final ClassicHttpResponse result = execute(secondRequest);
534 
535         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
536 
537         Assertions.assertEquals(DateUtils.formatStandardDate(inFiveSeconds), result.getFirstHeader("Date").getValue());
538         Assertions.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
539     }
540 
541     @Test
542     public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
543 
544         final Instant now = Instant.now();
545         final Instant tenSecondsAgo = now.minusSeconds(10);
546         final Instant nineSecondsAgo = now.minusSeconds(9);
547         final Instant eightSecondsAgo = now.minusSeconds(8);
548 
549         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
550                 Method.GET, "/thing", null,
551                 200, new Header[] {
552                         new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
553                         new BasicHeader("ETag", "\"etag\"")
554                 }, HttpTestUtils.makeNullResource());
555 
556         impl = new CachingExec(mockCache, null, config);
557 
558         request = new BasicClassicHttpRequest("GET", "/thing");
559 
560         final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/thing");
561         validate.setHeader("If-None-Match", "\"etag\"");
562 
563         final ClassicHttpResponse notModified = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
564         notModified.setHeader("Date", DateUtils.formatStandardDate(now));
565         notModified.setHeader("ETag", "\"etag\"");
566 
567         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
568                 new CacheMatch(new CacheHit("key", entry), null));
569         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(validate), Mockito.any())).thenReturn(notModified);
570         final HttpCacheEntry updated = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
571                 Method.GET, "/thing", null,
572                 200, new Header[] {
573                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
574                         new BasicHeader("ETag", "\"etag\"")
575                 }, HttpTestUtils.makeNullResource());
576         Mockito.when(mockCache.update(
577                         Mockito.any(),
578                         Mockito.any(),
579                         Mockito.any(),
580                         Mockito.any(),
581                         Mockito.any(),
582                         Mockito.any()))
583                 .thenReturn(new CacheHit("key", updated));
584 
585         execute(request);
586 
587         Mockito.verify(mockCache).update(
588                 Mockito.any(),
589                 Mockito.eq(host),
590                 RequestEquivalent.eq(request),
591                 ResponseEquivalent.eq(notModified),
592                 Mockito.any(),
593                 Mockito.any());
594     }
595 
596     @Test
597     public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
598 
599         final Instant now = Instant.now();
600         final Instant tenSecondsAgo = now.minusSeconds(10);
601         final Instant nineSecondsAgo = now.plusSeconds(9);
602         final Instant eightSecondsAgo = now.plusSeconds(8);
603 
604         final Header[] hdrs = new Header[] {
605                 new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
606                 new BasicHeader("Cache-Control", "max-age=3600"),
607                 new BasicHeader("Content-Length", "128")
608         };
609 
610         final byte[] bytes = new byte[128];
611         new Random().nextBytes(bytes);
612 
613         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
614 
615         impl = new CachingExec(mockCache, null, config);
616         request = new BasicClassicHttpRequest("GET", "/thing");
617 
618         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
619                 new CacheMatch(new CacheHit("key", entry), null));
620 
621         final ClassicHttpResponse result = execute(request);
622 
623         Assertions.assertEquals(200, result.getCode());
624     }
625 
626     @Test
627     public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
628 
629         final Instant now = Instant.now();
630         final Instant tenSecondsAgo = now.minusSeconds(10);
631         final Instant nineSecondsAgo = now.minusSeconds(9);
632         final Instant eightSecondsAgo = now.minusSeconds(8);
633 
634         final Header[] hdrs = new Header[] {
635                 new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
636                 new BasicHeader("Cache-Control", "max-age=3600"),
637                 new BasicHeader("Content-Length", "128")
638         };
639 
640         final byte[] bytes = new byte[128];
641         new Random().nextBytes(bytes);
642 
643         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
644 
645         impl = new CachingExec(mockCache, null, config);
646         request = new BasicClassicHttpRequest("GET", "/");
647 
648         Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
649                 new CacheMatch(new CacheHit("key", entry), null));
650 
651         final ClassicHttpResponse result = execute(request);
652 
653         Assertions.assertEquals(200, result.getCode());
654         // We calculate the age of the cache entry as per RFC 9111:
655         // We first find the "corrected_initial_age" which is the maximum of "apparentAge" and "correctedReceivedAge".
656         // In this case, max(1, 2) = 2 seconds.
657         // We then add the "residentTime" which is "now - responseTime",
658         // which is the current time minus the time the cache entry was created. In this case, that is 8 seconds.
659         // So, the total age is "corrected_initial_age" + "residentTime" = 2 + 8 = 10 seconds.
660         assertThat(result, ContainsHeaderMatcher.contains("Age", "10"));
661     }
662 
663     @Test
664     public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
665 
666         final Instant now = Instant.now();
667         final Instant inFiveSecond = now.plusSeconds(5);
668 
669         // put an entry in the cache
670         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
671 
672         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
673         resp1.setHeader("Date", DateUtils.formatStandardDate(inFiveSecond));
674         resp1.setHeader("ETag", "\"etag1\"");
675         resp1.setHeader("Cache-Control", "max-age=3600");
676         resp1.setHeader("Content-Length", "128");
677 
678         // force another origin hit
679         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
680         req2.setHeader("Cache-Control", "no-cache");
681 
682         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
683         resp2.setHeader("Date", DateUtils.formatStandardDate(now)); // older
684         resp2.setHeader("ETag", "\"etag2\"");
685         resp2.setHeader("Cache-Control", "max-age=3600");
686         resp2.setHeader("Content-Length", "128");
687 
688         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
689 
690         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
691 
692         execute(req1);
693 
694         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
695 
696         execute(req2);
697         final ClassicHttpResponse result = execute(req3);
698         Assertions.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
699     }
700 
701     @Test
702     public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
703 
704         final Instant now = Instant.now();
705         final Instant tenSecondsAgo = now.minusSeconds(10);
706 
707         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
708         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
709         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
710         resp1.setHeader("Cache-Control", "max-age=3600");
711         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
712         resp1.setHeader("ETag", "W/\"etag\"");
713 
714         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
715         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
716 
717         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
718 
719         execute(req1);
720         execute(req2);
721 
722         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
723         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
724 
725         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
726         Assertions.assertEquals(2, allRequests.size());
727         final ClassicHttpRequest validation = allRequests.get(1);
728         boolean foundETag = false;
729         final Iterator<HeaderElement> it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
730         while (it.hasNext()) {
731             final HeaderElement elt = it.next();
732             if ("W/\"etag\"".equals(elt.getName())) {
733                 foundETag = true;
734             }
735         }
736         final Iterator<HeaderElement> it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
737         while (it2.hasNext()) {
738             final HeaderElement elt = it2.next();
739             if ("W/\"etag\"".equals(elt.getName())) {
740                 foundETag = true;
741             }
742         }
743         Assertions.assertTrue(foundETag);
744     }
745 
746     @Test
747     public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache() throws Exception {
748         final Instant now = Instant.now();
749         final Instant tenSecondsAgo = now.minusSeconds(10);
750         final Instant twentySecondsAgo = now.plusSeconds(20);
751 
752         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
753         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
754         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
755         resp1.setHeader("Cache-Control", "max-age=3600");
756         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
757         resp1.setHeader("ETag", "W/\"etag\"");
758 
759         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
760         req2.setHeader("If-None-Match", "W/\"etag\"");
761         req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(twentySecondsAgo));
762 
763         // must hit the origin again for the second request
764         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
765 
766         execute(req1);
767         final ClassicHttpResponse result = execute(req2);
768 
769         Assertions.assertNotEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
770         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
771     }
772 
773     @Test
774     public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache() throws Exception {
775         final Instant now = Instant.now();
776         final Instant tenSecondsAgo = now.minusSeconds(10);
777 
778         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
779         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
780         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
781         resp1.setHeader("Cache-Control", "max-age=3600");
782         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
783         resp1.setHeader("ETag", "W/\"etag\"");
784 
785         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
786         req2.setHeader("If-None-Match", "W/\"etag\"");
787         req2.setHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
788 
789         // may hit the origin again for the second request
790         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
791 
792         execute(req1);
793         execute(req2);
794 
795         Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
796         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
797     }
798 
799     @Test
800     public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response() throws Exception {
801         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
802         req.setHeader("Range", "bytes=0-50");
803 
804         final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
805         resp.setHeader("Content-Range", "bytes 0-50/128");
806         resp.setHeader("ETag", "\"etag\"");
807         resp.setHeader("Cache-Control", "max-age=3600");
808 
809         Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp);
810 
811         execute(req);
812 
813         Mockito.verifyNoInteractions(mockCache);
814     }
815 
816     @Test
817     public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
818         originResponse = new BasicClassicHttpResponse(302, "Temporary Redirect");
819         originResponse.setHeader("Location", "http://foo.example.com/other");
820         originResponse.removeHeaders("Expires");
821         originResponse.removeHeaders("Cache-Control");
822 
823         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
824 
825         execute(request);
826         execute(request);
827 
828         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
829     }
830 
831     private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
832         originResponse = HttpTestUtils.make200Response();
833         originResponse.setHeader(header, value);
834 
835         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
836 
837         final ClassicHttpResponse result = execute(request);
838 
839         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
840     }
841 
842     @Test
843     public void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
844 
845         final String url = "http://foo.example.com/other";
846         testDoesNotModifyHeaderFromOrigin("Content-Location", url);
847     }
848 
849     @Test
850     public void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
851         testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
852     }
853 
854     @Test
855     public void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
856         testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
857     }
858 
859     @Test
860     public void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
861         final String lm = DateUtils.formatStandardDate(Instant.now());
862         testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
863     }
864 
865     private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
866         originResponse.removeHeaders(header);
867 
868         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
869 
870         final ClassicHttpResponse result = execute(request);
871 
872         Assertions.assertNull(result.getFirstHeader(header));
873     }
874 
875     @Test
876     public void testDoesNotAddContentLocationToOriginResponse() throws Exception {
877         testDoesNotAddHeaderToOriginResponse("Content-Location");
878     }
879 
880     @Test
881     public void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
882         testDoesNotAddHeaderToOriginResponse("Content-MD5");
883     }
884 
885     @Test
886     public void testDoesNotAddEtagToOriginResponse() throws Exception {
887         testDoesNotAddHeaderToOriginResponse("ETag");
888     }
889 
890     @Test
891     public void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
892         testDoesNotAddHeaderToOriginResponse("Last-Modified");
893     }
894 
895     private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value) throws Exception {
896 
897         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
898         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
899 
900         originResponse = HttpTestUtils.make200Response();
901         originResponse.setHeader("Cache-Control", "max-age=3600");
902         originResponse.setHeader(header, value);
903 
904         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
905 
906         execute(req1);
907         final ClassicHttpResponse result = execute(req2);
908 
909         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
910     }
911 
912     @Test
913     public void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
914         final String url = "http://foo.example.com/other";
915         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
916     }
917 
918     @Test
919     public void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
920         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
921     }
922 
923     @Test
924     public void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
925         testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
926     }
927 
928     @Test
929     public void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
930         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
931         testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
932     }
933 
934     private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
935 
936         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
937         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
938 
939         originResponse.addHeader("Cache-Control", "max-age=3600");
940         originResponse.removeHeaders(header);
941 
942         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
943 
944         execute(req1);
945         final ClassicHttpResponse result = execute(req2);
946 
947         Assertions.assertNull(result.getFirstHeader(header));
948     }
949 
950     @Test
951     public void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
952         testDoesNotAddHeaderOnCacheHit("Content-Location");
953     }
954 
955     @Test
956     public void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
957         testDoesNotAddHeaderOnCacheHit("Content-MD5");
958     }
959 
960     @Test
961     public void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
962         testDoesNotAddHeaderOnCacheHit("ETag");
963     }
964 
965     @Test
966     public void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
967         testDoesNotAddHeaderOnCacheHit("Last-Modified");
968     }
969 
970     private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
971         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
972         req.setEntity(HttpTestUtils.makeBody(128));
973         req.setHeader("Content-Length","128");
974         req.setHeader(header,value);
975 
976         execute(req);
977 
978         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
979         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
980 
981         final ClassicHttpRequest captured = reqCapture.getValue();
982         Assertions.assertEquals(value, captured.getFirstHeader(header).getValue());
983     }
984 
985     @Test
986     public void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
987         final String url = "http://foo.example.com/other";
988         testDoesNotModifyHeaderOnRequest("Content-Location",url);
989     }
990 
991     @Test
992     public void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
993         testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
994     }
995 
996     @Test
997     public void testDoesNotModifyETagHeaderOnRequest() throws Exception {
998         testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
999     }
1000 
1001     @Test
1002     public void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
1003         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
1004         testDoesNotModifyHeaderOnRequest("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
1005     }
1006 
1007     private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
1008         final BasicClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
1009         req.setEntity(HttpTestUtils.makeBody(128));
1010         req.setHeader("Content-Length","128");
1011         req.removeHeaders(header);
1012 
1013         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1014 
1015         execute(req);
1016 
1017         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1018         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
1019 
1020         final ClassicHttpRequest captured = reqCapture.getValue();
1021         Assertions.assertNull(captured.getFirstHeader(header));
1022     }
1023 
1024     @Test
1025     public void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
1026         testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
1027     }
1028 
1029     @Test
1030     public void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
1031         testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
1032     }
1033 
1034     @Test
1035     public void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
1036         testDoesNotAddHeaderToRequestIfNotPresent("ETag");
1037     }
1038 
1039     @Test
1040     public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
1041         testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
1042     }
1043 
1044     @Test
1045     public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
1046         final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
1047         testDoesNotModifyHeaderFromOrigin("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
1048     }
1049 
1050     @Test
1051     public void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
1052         final Instant inTenSeconds = Instant.now().plusSeconds(10);
1053         testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", DateUtils.formatStandardDate(inTenSeconds));
1054     }
1055 
1056     @Test
1057     public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
1058         originResponse.removeHeaders("Expires");
1059 
1060         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1061 
1062         final ClassicHttpResponse result = execute(request);
1063 
1064         final Header expHdr = result.getFirstHeader("Expires");
1065         if (expHdr != null) {
1066             Assertions.assertEquals(result.getFirstHeader("Date").getValue(),
1067                                 expHdr.getValue());
1068         }
1069     }
1070 
1071     private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
1072         originResponse.addHeader("Cache-Control","no-transform");
1073         originResponse.setHeader(header, value);
1074 
1075         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1076 
1077         final ClassicHttpResponse result = execute(request);
1078 
1079         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
1080     }
1081 
1082     @Test
1083     public void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
1084         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
1085     }
1086 
1087     @Test
1088     public void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
1089         request.setHeader("If-Range","\"etag\"");
1090         request.setHeader("Range","bytes=0-49");
1091 
1092         originResponse = new BasicClassicHttpResponse(206, "Partial Content");
1093         originResponse.setEntity(HttpTestUtils.makeBody(50));
1094         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
1095     }
1096 
1097     @Test
1098     public void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
1099         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
1100     }
1101 
1102     private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
1103         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1104         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1105 
1106         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
1107         originResponse.setHeader(header, value);
1108 
1109         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1110 
1111         execute(req1);
1112         final ClassicHttpResponse result = execute(req2);
1113 
1114         Assertions.assertEquals(value, result.getFirstHeader(header).getValue());
1115     }
1116 
1117     @Test
1118     public void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
1119         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
1120     }
1121 
1122     @Test
1123     public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
1124         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
1125     }
1126 
1127     @Test
1128     public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1129         originResponse.addHeader("Cache-Control","no-transform");
1130         testDoesNotAddHeaderToOriginResponse("Content-Encoding");
1131     }
1132 
1133     @Test
1134     public void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1135         originResponse.addHeader("Cache-Control","no-transform");
1136         testDoesNotAddHeaderToOriginResponse("Content-Range");
1137     }
1138 
1139     @Test
1140     public void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
1141         originResponse.addHeader("Cache-Control","no-transform");
1142         testDoesNotAddHeaderToOriginResponse("Content-Type");
1143     }
1144 
1145     /* no add on cache hit with no-transform */
1146     @Test
1147     public void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1148         originResponse.addHeader("Cache-Control","no-transform");
1149         testDoesNotAddHeaderOnCacheHit("Content-Encoding");
1150     }
1151 
1152     @Test
1153     public void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1154         originResponse.addHeader("Cache-Control","no-transform");
1155         testDoesNotAddHeaderOnCacheHit("Content-Range");
1156     }
1157 
1158     @Test
1159     public void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
1160         originResponse.addHeader("Cache-Control","no-transform");
1161         testDoesNotAddHeaderOnCacheHit("Content-Type");
1162     }
1163 
1164     /* no modify on request */
1165     @Test
1166     public void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
1167         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
1168     }
1169 
1170     @Test
1171     public void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
1172         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
1173     }
1174 
1175     @Test
1176     public void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
1177         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
1178     }
1179 
1180     @Test
1181     public void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
1182         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
1183     }
1184 
1185     @Test
1186     public void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
1187         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
1188     }
1189 
1190     @Test
1191     public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
1192         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
1193     }
1194 
1195     @Test
1196     public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
1197         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1198         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1199         resp1.setHeader("Cache-Control","max-age=3600");
1200         resp1.setHeader("ETag","\"etag\"");
1201 
1202         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1203         req2.setHeader("Cache-Control","max-age=0, max-stale=0");
1204         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1205 
1206         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1207 
1208         execute(req1);
1209 
1210         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1211 
1212         final ClassicHttpResponse result = execute(req2);
1213 
1214         try (final InputStream i1 = resp1.getEntity().getContent();
1215              final InputStream i2 = result.getEntity().getContent()) {
1216             int b1, b2;
1217             while((b1 = i1.read()) != -1) {
1218                 b2 = i2.read();
1219                 Assertions.assertEquals(b1, b2);
1220             }
1221             b2 = i2.read();
1222             Assertions.assertEquals(-1, b2);
1223         }
1224     }
1225 
1226     private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
1227         r.setHeader("Allow","GET");
1228         r.setHeader("Content-Encoding","gzip");
1229         r.setHeader("Content-Language","en");
1230         r.setHeader("Content-Length", "128");
1231         r.setHeader("Content-Location","http://foo.example.com/other");
1232         r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1233         r.setHeader("Content-Type", "text/html;charset=utf-8");
1234         r.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(10)));
1235         r.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now().minusSeconds(10)));
1236         r.setHeader("Location", "http://foo.example.com/other2");
1237         r.setHeader("Retry-After","180");
1238     }
1239 
1240     @Test
1241     public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
1242         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1243         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1244         resp1.setHeader("Cache-Control","max-age=3600");
1245         resp1.setHeader("ETag","\"etag\"");
1246         decorateWithEndToEndHeaders(resp1);
1247 
1248         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1249         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1250         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1251         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1252         resp2.setHeader("Server", "MockServer/1.0");
1253 
1254         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1255 
1256         execute(req1);
1257 
1258         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp2);
1259         final ClassicHttpResponse result = execute(req2);
1260 
1261         final String[] endToEndHeaders = {
1262             "Cache-Control", "ETag", "Allow", "Content-Encoding",
1263             "Content-Language", "Content-Length", "Content-Location",
1264             "Content-MD5", "Content-Type", "Expires", "Last-Modified",
1265             "Location", "Retry-After"
1266         };
1267         for(final String h : endToEndHeaders) {
1268             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
1269                                 HttpTestUtils.getCanonicalHeaderValue(result, h));
1270         }
1271     }
1272 
1273     @Test
1274     public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
1275 
1276         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1277         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1278         resp1.setHeader("Cache-Control","max-age=3600");
1279         resp1.setHeader("ETag","\"etag\"");
1280         decorateWithEndToEndHeaders(resp1);
1281 
1282         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1283         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1284         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1285         resp2.setHeader("Cache-Control", "max-age=1800");
1286         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1287         resp2.setHeader("Server", "MockServer/1.0");
1288         resp2.setHeader("Allow", "GET,HEAD");
1289         resp2.setHeader("Content-Language", "en,en-us");
1290         resp2.setHeader("Content-Location", "http://foo.example.com/new");
1291         resp2.setHeader("Content-Type","text/html");
1292         resp2.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(5)));
1293         resp2.setHeader("Location", "http://foo.example.com/new2");
1294         resp2.setHeader("Retry-After","120");
1295 
1296         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1297 
1298         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1299 
1300         execute(req1);
1301 
1302         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1303         final ClassicHttpResponse result1 = execute(req2);
1304         final ClassicHttpResponse result2 = execute(req3);
1305 
1306         final String[] endToEndHeaders = {
1307             "Date", "Cache-Control", "Allow", "Content-Language",
1308             "Content-Location", "Content-Type", "Expires", "Location",
1309             "Retry-After"
1310         };
1311         for(final String h : endToEndHeaders) {
1312             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1313                                 HttpTestUtils.getCanonicalHeaderValue(result1, h));
1314             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1315                                 HttpTestUtils.getCanonicalHeaderValue(result2, h));
1316         }
1317     }
1318 
1319     @Test
1320     public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
1321         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1322         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1323         resp1.addHeader("Cache-Control","max-age=3600");
1324         resp1.addHeader("Cache-Control","public");
1325         resp1.setHeader("ETag","\"etag\"");
1326 
1327         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1328         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
1329         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1330         resp2.setHeader("Cache-Control", "max-age=1800");
1331 
1332         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1333 
1334         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1335 
1336         execute(req1);
1337 
1338         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1339 
1340         final ClassicHttpResponse result1 = execute(req2);
1341         final ClassicHttpResponse result2 = execute(req3);
1342 
1343         final String h = "Cache-Control";
1344         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1345                             HttpTestUtils.getCanonicalHeaderValue(result1, h));
1346         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
1347                             HttpTestUtils.getCanonicalHeaderValue(result2, h));
1348     }
1349 
1350     @Test
1351     public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch() throws Exception {
1352 
1353         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1354         req1.setHeader("Accept-Encoding","gzip");
1355 
1356         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1357         resp1.setHeader("ETag","\"etag1\"");
1358         resp1.setHeader("Cache-Control","max-age=3600");
1359         resp1.setHeader("Vary","Accept-Encoding");
1360 
1361         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1362 
1363         execute(req1);
1364 
1365         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1366         req2.removeHeaders("Accept-Encoding");
1367 
1368         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1369         resp2.setHeader("ETag","\"etag1\"");
1370         resp2.setHeader("Cache-Control","max-age=3600");
1371 
1372         // not allowed to have a cache hit; must forward request
1373         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1374 
1375         execute(req2);
1376 
1377         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1378     }
1379 
1380     @Test
1381     public void testCannotServeFromCacheForVaryStar() throws Exception {
1382         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1383 
1384         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1385         resp1.setHeader("ETag","\"etag1\"");
1386         resp1.setHeader("Cache-Control","max-age=3600");
1387         resp1.setHeader("Vary","*");
1388 
1389         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1390 
1391         execute(req1);
1392 
1393         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1394 
1395         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1396         resp2.setHeader("ETag","\"etag1\"");
1397         resp2.setHeader("Cache-Control","max-age=3600");
1398 
1399         // not allowed to have a cache hit; must forward request
1400         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1401 
1402         execute(req2);
1403 
1404         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1405     }
1406 
1407     @Test
1408     public void testNonMatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
1409 
1410         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1411         req1.setHeader("User-Agent","MyBrowser/1.0");
1412 
1413         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1414         resp1.setHeader("ETag","\"etag1\"");
1415         resp1.setHeader("Cache-Control","max-age=3600");
1416         resp1.setHeader("Vary","User-Agent");
1417         resp1.setHeader("Content-Type","application/octet-stream");
1418 
1419         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1420         req2.setHeader("User-Agent","MyBrowser/1.5");
1421 
1422         final ClassicHttpResponse resp200 = HttpTestUtils.make200Response();
1423         resp200.setHeader("ETag","\"etag1\"");
1424         resp200.setHeader("Vary","User-Agent");
1425 
1426         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1427 
1428         execute(req1);
1429 
1430         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp200);
1431 
1432         final ClassicHttpResponse result = execute(req2);
1433 
1434         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
1435 
1436         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1437 
1438         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
1439     }
1440 
1441     protected void testUnsafeOperationInvalidatesCacheForThatUri(
1442             final ClassicHttpRequest unsafeReq) throws Exception {
1443         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1444         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1445         resp1.setHeader("Cache-Control","public, max-age=3600");
1446 
1447         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1448 
1449         execute(req1);
1450 
1451         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1452 
1453         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1454 
1455         execute(unsafeReq);
1456 
1457         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1458         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1459         resp3.setHeader("Cache-Control","public, max-age=3600");
1460 
1461         // this origin request MUST happen due to invalidation
1462         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1463 
1464         execute(req3);
1465     }
1466 
1467     protected ClassicHttpRequest makeRequestWithBody(final String method, final String requestUri) {
1468         final ClassicHttpRequest req = new BasicClassicHttpRequest(method, requestUri);
1469         final int nbytes = 128;
1470         req.setEntity(HttpTestUtils.makeBody(nbytes));
1471         req.setHeader("Content-Length", Long.toString(nbytes));
1472         return req;
1473     }
1474 
1475     @Test
1476     public void testPutToUriInvalidatesCacheForThatUri() throws Exception {
1477         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1478         testUnsafeOperationInvalidatesCacheForThatUri(req);
1479     }
1480 
1481     @Test
1482     public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
1483         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE","/");
1484         testUnsafeOperationInvalidatesCacheForThatUri(req);
1485     }
1486 
1487     @Test
1488     public void testPostToUriInvalidatesCacheForThatUri() throws Exception {
1489         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1490         testUnsafeOperationInvalidatesCacheForThatUri(req);
1491     }
1492 
1493     protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
1494             final ClassicHttpRequest unsafeReq) throws Exception {
1495         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
1496         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1497         resp1.setHeader("Cache-Control","public, max-age=3600");
1498 
1499         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1500 
1501         execute(req1);
1502 
1503         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1504 
1505         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1506 
1507         execute(unsafeReq);
1508 
1509         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
1510         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1511         resp3.setHeader("Cache-Control","public, max-age=3600");
1512 
1513         // this origin request MUST happen due to invalidation
1514         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1515 
1516         execute(req3);
1517     }
1518 
1519     protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
1520             final ClassicHttpRequest unsafeReq) throws Exception {
1521         unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
1522         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1523     }
1524 
1525     protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
1526             final ClassicHttpRequest unsafeReq) throws Exception {
1527         unsafeReq.setHeader("Content-Location","/content");
1528         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1529     }
1530 
1531     protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
1532             final ClassicHttpRequest unsafeReq) throws Exception {
1533         unsafeReq.setHeader("Location","http://foo.example.com/content");
1534         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
1535     }
1536 
1537     @Test
1538     public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1539         final ClassicHttpRequest req2 = makeRequestWithBody("PUT","/");
1540         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
1541     }
1542 
1543     @Test
1544     public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1545         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1546         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1547     }
1548 
1549     @Test
1550     public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
1551         final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
1552         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1553     }
1554 
1555     @Test
1556     public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1557         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1558         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
1559     }
1560 
1561     @Test
1562     public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
1563         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1564         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1565     }
1566 
1567     @Test
1568     public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1569         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1570         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1571     }
1572 
1573     @Test
1574     public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
1575         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1576         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
1577     }
1578 
1579     @Test
1580     public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
1581         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1582         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
1583     }
1584 
1585     @Test
1586     public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
1587         final ClassicHttpRequest req = makeRequestWithBody("POST","/");
1588         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
1589     }
1590 
1591     private void testRequestIsWrittenThroughToOrigin(final ClassicHttpRequest req) throws Exception {
1592         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
1593         final ClassicHttpRequest wrapper = req;
1594         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(wrapper), Mockito.any())).thenReturn(resp);
1595 
1596         execute(wrapper);
1597     }
1598 
1599     @Test
1600     public void testOPTIONSRequestsAreWrittenThroughToOrigin() throws Exception {
1601         final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS","*");
1602         testRequestIsWrittenThroughToOrigin(req);
1603     }
1604 
1605     @Test
1606     public void testPOSTRequestsAreWrittenThroughToOrigin() throws Exception {
1607         final ClassicHttpRequest req = new BasicClassicHttpRequest("POST","/");
1608         req.setEntity(HttpTestUtils.makeBody(128));
1609         req.setHeader("Content-Length","128");
1610         testRequestIsWrittenThroughToOrigin(req);
1611     }
1612 
1613     @Test
1614     public void testPUTRequestsAreWrittenThroughToOrigin() throws Exception {
1615         final ClassicHttpRequest req = new BasicClassicHttpRequest("PUT","/");
1616         req.setEntity(HttpTestUtils.makeBody(128));
1617         req.setHeader("Content-Length","128");
1618         testRequestIsWrittenThroughToOrigin(req);
1619     }
1620 
1621     @Test
1622     public void testDELETERequestsAreWrittenThroughToOrigin() throws Exception {
1623         final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
1624         testRequestIsWrittenThroughToOrigin(req);
1625     }
1626 
1627     @Test
1628     public void testTRACERequestsAreWrittenThroughToOrigin() throws Exception {
1629         final ClassicHttpRequest req = new BasicClassicHttpRequest("TRACE","/");
1630         testRequestIsWrittenThroughToOrigin(req);
1631     }
1632 
1633     @Test
1634     public void testCONNECTRequestsAreWrittenThroughToOrigin() throws Exception {
1635         final ClassicHttpRequest req = new BasicClassicHttpRequest("CONNECT","/");
1636         testRequestIsWrittenThroughToOrigin(req);
1637     }
1638 
1639     @Test
1640     public void testUnknownMethodRequestsAreWrittenThroughToOrigin() throws Exception {
1641         final ClassicHttpRequest req = new BasicClassicHttpRequest("UNKNOWN","/");
1642         testRequestIsWrittenThroughToOrigin(req);
1643     }
1644 
1645     @Test
1646     public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
1647         final String reallyOldAge = "1" + Long.MAX_VALUE;
1648         originResponse.setHeader("Age",reallyOldAge);
1649 
1650         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1651 
1652         final ClassicHttpResponse result = execute(request);
1653 
1654         Assertions.assertEquals(reallyOldAge,
1655                             result.getFirstHeader("Age").getValue());
1656     }
1657 
1658     @Test
1659     public void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
1660         final String allowHeaderValue = "GET, HEAD, FOOBAR";
1661         originResponse.setHeader("Allow",allowHeaderValue);
1662         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
1663         final ClassicHttpResponse result = execute(request);
1664         Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
1665                             HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
1666     }
1667 
1668     protected void testSharedCacheRevalidatesAuthorizedResponse(
1669             final ClassicHttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception {
1670         if (config.isSharedCache()) {
1671             final String authorization = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
1672             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1673             req1.setHeader("Authorization",authorization);
1674 
1675             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1676             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1677             resp2.setHeader("Cache-Control","max-age=3600");
1678 
1679             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
1680 
1681             execute(req1);
1682 
1683             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1684 
1685             execute(req2);
1686 
1687             Mockito.verify(mockExecChain, Mockito.atLeast(1 + minTimes)).proceed(Mockito.any(), Mockito.any());
1688             Mockito.verify(mockExecChain, Mockito.atMost(1 + maxTimes)).proceed(Mockito.any(), Mockito.any());
1689         }
1690     }
1691 
1692     @Test
1693     public void testSharedCacheMustNotNormallyCacheAuthorizedResponses() throws Exception {
1694         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1695         resp.setHeader("Cache-Control","max-age=3600");
1696         resp.setHeader("ETag","\"etag\"");
1697         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
1698     }
1699 
1700     @Test
1701     public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader() throws Exception {
1702         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1703         resp.setHeader("Cache-Control","s-maxage=3600");
1704         resp.setHeader("ETag","\"etag\"");
1705         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1706     }
1707 
1708     @Test
1709     public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero() throws Exception {
1710         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1711         resp.setHeader("Cache-Control","s-maxage=0");
1712         resp.setHeader("ETag","\"etag\"");
1713         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
1714     }
1715 
1716     @Test
1717     public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate() throws Exception {
1718         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1719         resp.setHeader("Cache-Control","must-revalidate");
1720         resp.setHeader("ETag","\"etag\"");
1721         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1722     }
1723 
1724     @Test
1725     public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic() throws Exception {
1726         final ClassicHttpResponse resp = HttpTestUtils.make200Response();
1727         resp.setHeader("Cache-Control","public");
1728         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
1729     }
1730 
1731     protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
1732             final ClassicHttpResponse authorizedResponse) throws Exception {
1733         if (config.isSharedCache()) {
1734             final String authorization1 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=";
1735             final String authorization2 = StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Qy";
1736 
1737             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1738             req1.setHeader("Authorization",authorization1);
1739 
1740             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1741             req2.setHeader("Authorization",authorization2);
1742 
1743             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1744 
1745             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(authorizedResponse);
1746 
1747             execute(req1);
1748 
1749             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1750 
1751             execute(req2);
1752 
1753             final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1754             Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1755 
1756             final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
1757             Assertions.assertEquals(2, allRequests.size());
1758 
1759             final ClassicHttpRequest captured = allRequests.get(1);
1760             Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
1761                     HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
1762         }
1763     }
1764 
1765     @Test
1766     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge() throws Exception {
1767         final Instant now = Instant.now();
1768         final Instant tenSecondsAgo = now.minusSeconds(10);
1769         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1770         resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1771         resp1.setHeader("ETag","\"etag\"");
1772         resp1.setHeader("Cache-Control","s-maxage=5");
1773 
1774         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
1775     }
1776 
1777     @Test
1778     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate() throws Exception {
1779         final Instant now = Instant.now();
1780         final Instant tenSecondsAgo = now.minusSeconds(10);
1781         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1782         resp1.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1783         resp1.setHeader("ETag","\"etag\"");
1784         resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
1785 
1786         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
1787     }
1788 
1789     protected void testCacheIsNotUsedWhenRespondingToRequest(final ClassicHttpRequest req) throws Exception {
1790         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1791         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1792         resp1.setHeader("Etag","\"etag\"");
1793         resp1.setHeader("Cache-Control","max-age=3600");
1794 
1795         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1796 
1797         execute(req1);
1798 
1799         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1800         resp2.setHeader("Etag","\"etag2\"");
1801         resp2.setHeader("Cache-Control","max-age=1200");
1802 
1803         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1804 
1805         final ClassicHttpResponse result = execute(req);
1806 
1807         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1808 
1809         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1810         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1811 
1812         final ClassicHttpRequest captured = reqCapture.getValue();
1813         Assertions.assertTrue(HttpTestUtils.equivalent(req, captured));
1814     }
1815 
1816     @Test
1817     public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() throws Exception {
1818         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
1819         req.setHeader("Cache-Control","no-cache");
1820         testCacheIsNotUsedWhenRespondingToRequest(req);
1821     }
1822 
1823     protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
1824             final ClassicHttpResponse staleResponse) throws Exception {
1825         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1826 
1827         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1828         req2.setHeader("Cache-Control","max-stale=3600");
1829         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1830         resp2.setHeader("ETag","\"etag2\"");
1831         resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
1832 
1833         // this request MUST happen
1834         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
1835 
1836         execute(req1);
1837 
1838         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1839 
1840         execute(req2);
1841 
1842         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1843         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
1844 
1845         final ClassicHttpRequest reval = reqCapture.getValue();
1846         boolean foundMaxAge0 = false;
1847         final Iterator<HeaderElement> it = MessageSupport.iterate(reval, HttpHeaders.CACHE_CONTROL);
1848         while (it.hasNext()) {
1849             final HeaderElement elt = it.next();
1850             if ("max-age".equalsIgnoreCase(elt.getName())
1851                     && "0".equals(elt.getValue())) {
1852                 foundMaxAge0 = true;
1853             }
1854         }
1855         Assertions.assertTrue(foundMaxAge0);
1856     }
1857 
1858     @Test
1859     public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
1860         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1861         final Instant now = Instant.now();
1862         final Instant tenSecondsAgo = now.minusSeconds(10);
1863         response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1864         response.setHeader("ETag","\"etag1\"");
1865         response.setHeader("Cache-Control","max-age=5, must-revalidate");
1866 
1867         testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
1868     }
1869 
1870     protected void testGenerates504IfCannotRevalidateStaleResponse(
1871             final ClassicHttpResponse staleResponse) throws Exception {
1872         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1873 
1874         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1875 
1876         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(staleResponse);
1877 
1878         execute(req1);
1879 
1880         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new SocketTimeoutException());
1881 
1882         final ClassicHttpResponse result = execute(req2);
1883 
1884         Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
1885                             result.getCode());
1886     }
1887 
1888     @Test
1889     public void testGenerates504IfCannotRevalidateAMustRevalidateEntry() throws Exception {
1890         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1891         final Instant now = Instant.now();
1892         final Instant tenSecondsAgo = now.minusSeconds(10);
1893         resp1.setHeader("ETag","\"etag\"");
1894         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1895         resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
1896 
1897         testGenerates504IfCannotRevalidateStaleResponse(resp1);
1898     }
1899 
1900     @Test
1901     public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
1902         if (config.isSharedCache()) {
1903             final ClassicHttpResponse response = HttpTestUtils.make200Response();
1904             final Instant now = Instant.now();
1905             final Instant tenSecondsAgo = now.minusSeconds(10);
1906             response.setHeader("Date",DateUtils.formatStandardDate(tenSecondsAgo));
1907             response.setHeader("ETag","\"etag1\"");
1908             response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
1909 
1910             testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
1911         }
1912     }
1913 
1914     @Test
1915     public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry() throws Exception {
1916         if (config.isSharedCache()) {
1917             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1918             final Instant now = Instant.now();
1919             final Instant tenSecondsAgo = now.minusSeconds(10);
1920             resp1.setHeader("ETag","\"etag\"");
1921             resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1922             resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
1923 
1924             testGenerates504IfCannotRevalidateStaleResponse(resp1);
1925         }
1926     }
1927 
1928     @Test
1929     public void testCacheControlPrivateIsNotCacheableBySharedCache() throws Exception {
1930         if (config.isSharedCache()) {
1931             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1932             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1933             resp1.setHeader("Cache-Control", "private,max-age=3600");
1934 
1935             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1936 
1937             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1938             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1939             // this backend request MUST happen
1940             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1941 
1942             execute(req1);
1943             execute(req2);
1944         }
1945     }
1946 
1947     @Test
1948     public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() throws Exception {
1949         if (config.isSharedCache()) {
1950             final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1951             final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1952             resp1.setHeader("X-Personal", "stuff");
1953             resp1.setHeader("Cache-Control", "private=\"X-Personal\",s-maxage=3600");
1954 
1955             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1956 
1957             final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1958             final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1959 
1960             // this backend request MAY happen
1961             Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1962 
1963             execute(req1);
1964             final ClassicHttpResponse result = execute(req2);
1965             Assertions.assertNull(result.getFirstHeader("X-Personal"));
1966 
1967             Mockito.verify(mockExecChain, Mockito.atLeastOnce()).proceed(Mockito.any(), Mockito.any());
1968             Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
1969         }
1970     }
1971 
1972     @Test
1973     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() throws Exception {
1974         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1975         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1976         resp1.setHeader("ETag","\"etag\"");
1977         resp1.setHeader("Cache-Control","no-cache");
1978 
1979         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1980 
1981         execute(req1);
1982 
1983         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1984         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1985 
1986         // this MUST happen
1987         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1988 
1989         execute(req2);
1990 
1991         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1992     }
1993 
1994     @Test
1995     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications() throws Exception {
1996         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1997         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1998         resp1.setHeader("ETag","\"etag\"");
1999         resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
2000 
2001         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2002 
2003         execute(req1);
2004 
2005         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2006         req2.setHeader("Cache-Control","max-stale=7200");
2007         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2008 
2009         // this MUST happen
2010         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2011 
2012         execute(req2);
2013     }
2014 
2015     @Test
2016     public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exception {
2017         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2018         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2019         resp1.setHeader("ETag","\"etag\"");
2020         resp1.setHeader("X-Stuff","things");
2021         resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
2022 
2023         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2024 
2025         execute(req1);
2026 
2027         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2028         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2029         resp2.setHeader("ETag","\"etag\"");
2030         resp2.setHeader("X-Stuff","things");
2031         resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
2032 
2033         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2034 
2035         final ClassicHttpResponse result = execute(req2);
2036 
2037         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
2038         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
2039 
2040         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
2041         if (allRequests.isEmpty()) {
2042             Assertions.assertNull(result.getFirstHeader("X-Stuff"));
2043         }
2044     }
2045 
2046     @Test
2047     public void testNoStoreOnRequestIsNotStoredInCache() throws Exception {
2048         request.setHeader("Cache-Control","no-store");
2049         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2050 
2051         execute(request);
2052 
2053         Mockito.verifyNoInteractions(mockCache);
2054     }
2055 
2056     @Test
2057     public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable() throws Exception {
2058         request.setHeader("Cache-Control","no-store");
2059         originResponse.setHeader("Cache-Control","max-age=3600");
2060         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2061 
2062         execute(request);
2063 
2064         Mockito.verifyNoInteractions(mockCache);
2065     }
2066 
2067     @Test
2068     public void testNoStoreOnResponseIsNotStoredInCache() throws Exception {
2069         originResponse.setHeader("Cache-Control","no-store");
2070         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2071 
2072         execute(request);
2073 
2074         Mockito.verifyNoInteractions(mockCache);
2075     }
2076 
2077     @Test
2078     public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators() throws Exception {
2079         originResponse.setHeader("Cache-Control","no-store,max-age=3600");
2080         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2081 
2082         execute(request);
2083 
2084         Mockito.verifyNoInteractions(mockCache);
2085     }
2086 
2087     @Test
2088     public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved() throws Exception {
2089         originResponse.addHeader("Content-Encoding","gzip");
2090         originResponse.addHeader("Content-Encoding","deflate");
2091         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2092 
2093         final ClassicHttpResponse result = execute(request);
2094         int total_encodings = 0;
2095         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
2096         while (it.hasNext()) {
2097             final HeaderElement elt = it.next();
2098             switch(total_encodings) {
2099                 case 0:
2100                     Assertions.assertEquals("gzip", elt.getName());
2101                     break;
2102                 case 1:
2103                     Assertions.assertEquals("deflate", elt.getName());
2104                     break;
2105                 default:
2106                     Assertions.fail("too many encodings");
2107             }
2108             total_encodings++;
2109         }
2110         Assertions.assertEquals(2, total_encodings);
2111     }
2112 
2113     @Test
2114     public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved() throws Exception {
2115         originResponse.addHeader("Content-Encoding","gzip,deflate");
2116         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2117 
2118         final ClassicHttpResponse result = execute(request);
2119         int total_encodings = 0;
2120         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
2121         while (it.hasNext()) {
2122             final HeaderElement elt = it.next();
2123             switch(total_encodings) {
2124                 case 0:
2125                     Assertions.assertEquals("gzip", elt.getName());
2126                     break;
2127                 case 1:
2128                     Assertions.assertEquals("deflate", elt.getName());
2129                     break;
2130                 default:
2131                     Assertions.fail("too many encodings");
2132             }
2133             total_encodings++;
2134         }
2135         Assertions.assertEquals(2, total_encodings);
2136     }
2137 
2138     @Test
2139     public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource() throws Exception {
2140         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/foo");
2141         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2142         resp1.setHeader("Cache-Control","public,max-age=3600");
2143         resp1.setHeader("Etag","\"etag\"");
2144         resp1.setHeader("Content-Location","http://foo.example.com/bar");
2145 
2146         execute(req1);
2147 
2148         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/bar");
2149         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2150         resp2.setHeader("Cache-Control","public,max-age=3600");
2151         resp2.setHeader("Etag","\"etag\"");
2152 
2153         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2154         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2155 
2156         execute(req2);
2157     }
2158 
2159     @Test
2160     public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throws Exception {
2161         originResponse.removeHeaders("Date");
2162         originResponse.setHeader("Cache-Control","public");
2163         originResponse.setHeader("ETag","\"etag\"");
2164 
2165         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2166 
2167         final ClassicHttpResponse result = execute(request);
2168         Assertions.assertNotNull(result.getFirstHeader("Date"));
2169     }
2170 
2171     private void testInvalidExpiresHeaderIsTreatedAsStale(
2172             final String expiresHeader) throws Exception {
2173         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2174         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2175         resp1.setHeader("Cache-Control","public");
2176         resp1.setHeader("ETag","\"etag\"");
2177         resp1.setHeader("Expires", expiresHeader);
2178 
2179         execute(req1);
2180 
2181         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2182         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2183 
2184         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2185         // second request to origin MUST happen
2186         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2187 
2188         execute(req2);
2189     }
2190 
2191     @Test
2192     public void testMalformedExpiresHeaderIsTreatedAsStale() throws Exception {
2193         testInvalidExpiresHeaderIsTreatedAsStale("garbage");
2194     }
2195 
2196     @Test
2197     public void testExpiresZeroHeaderIsTreatedAsStale() throws Exception {
2198         testInvalidExpiresHeaderIsTreatedAsStale("0");
2199     }
2200 
2201     @Test
2202     public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exception {
2203         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
2204         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
2205         resp1.setHeader("Cache-Control","public");
2206         resp1.setHeader("ETag","\"etag\"");
2207         resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
2208 
2209         execute(req1);
2210 
2211         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
2212         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
2213 
2214         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
2215         // second request to origin MUST happen
2216         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
2217 
2218         execute(req2);
2219     }
2220 
2221     @Test
2222     public void testDoesNotModifyServerResponseHeader() throws Exception {
2223         final String server = "MockServer/1.0";
2224         originResponse.setHeader("Server", server);
2225 
2226         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
2227 
2228         final ClassicHttpResponse result = execute(request);
2229         Assertions.assertEquals(server, result.getFirstHeader("Server").getValue());
2230     }
2231 
2232 }