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  
30  import static org.mockito.Mockito.mock;
31  
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.net.SocketException;
35  import java.net.SocketTimeoutException;
36  import java.nio.charset.StandardCharsets;
37  import java.time.Duration;
38  import java.time.Instant;
39  import java.time.temporal.ChronoUnit;
40  
41  import org.apache.hc.client5.http.HttpRoute;
42  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
43  import org.apache.hc.client5.http.auth.StandardAuthScheme;
44  import org.apache.hc.client5.http.cache.CacheResponseStatus;
45  import org.apache.hc.client5.http.cache.HttpCacheContext;
46  import org.apache.hc.client5.http.cache.HttpCacheEntry;
47  import org.apache.hc.client5.http.cache.HttpCacheStorage;
48  import org.apache.hc.client5.http.classic.ExecChain;
49  import org.apache.hc.client5.http.classic.ExecRuntime;
50  import org.apache.hc.client5.http.classic.methods.HttpGet;
51  import org.apache.hc.client5.http.classic.methods.HttpOptions;
52  import org.apache.hc.client5.http.utils.DateUtils;
53  import org.apache.hc.client5.http.validator.ETag;
54  import org.apache.hc.core5.http.ClassicHttpRequest;
55  import org.apache.hc.core5.http.ClassicHttpResponse;
56  import org.apache.hc.core5.http.Header;
57  import org.apache.hc.core5.http.HttpException;
58  import org.apache.hc.core5.http.HttpHost;
59  import org.apache.hc.core5.http.HttpStatus;
60  import org.apache.hc.core5.http.io.entity.EntityUtils;
61  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
62  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
63  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
64  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
65  import org.apache.hc.core5.http.message.BasicHeader;
66  import org.apache.hc.core5.net.URIAuthority;
67  import org.junit.jupiter.api.Assertions;
68  import org.junit.jupiter.api.BeforeEach;
69  import org.junit.jupiter.api.Test;
70  import org.mockito.Mock;
71  import org.mockito.Mockito;
72  import org.mockito.MockitoAnnotations;
73  
74  public class TestCachingExecChain {
75  
76      @Mock
77      ExecChain mockExecChain;
78      @Mock
79      ExecRuntime mockExecRuntime;
80      @Mock
81      HttpCacheStorage mockStorage;
82      @Mock
83      DefaultCacheRevalidator cacheRevalidator;
84  
85      HttpRoute route;
86      HttpHost host;
87      ClassicHttpRequest request;
88      HttpCacheContext context;
89      HttpCacheEntry entry;
90      HttpCache cache;
91      CachingExec impl;
92      CacheConfig customConfig;
93      ExecChain.Scope scope;
94  
95      @BeforeEach
96      public void setUp() {
97          MockitoAnnotations.openMocks(this);
98          host = new HttpHost("foo.example.com", 80);
99          route = new HttpRoute(host);
100         request = new BasicClassicHttpRequest("GET", "/stuff");
101         context = HttpCacheContext.create();
102         entry = HttpTestUtils.makeCacheEntry();
103         customConfig = CacheConfig.DEFAULT;
104         scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
105 
106         cache = Mockito.spy(new BasicHttpCache());
107 
108         impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
109 
110     }
111 
112     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
113         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context);
114         return impl.execute(ClassicRequestBuilder.copy(request).build(), scope, mockExecChain);
115     }
116 
117     @Test
118     public void testCacheableResponsesGoIntoCache() throws Exception {
119         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
120         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
121         resp1.setHeader("Cache-Control", "max-age=3600");
122 
123         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
124 
125         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
126 
127         execute(req1);
128         execute(req2);
129 
130         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
131         Mockito.verify(cache).store(Mockito.eq(host), RequestEquivalent.eq(req1),
132                 Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
133     }
134 
135     @Test
136     public void testOlderCacheableResponsesDoNotGoIntoCache() throws Exception {
137         final Instant now = Instant.now();
138         final Instant fiveSecondsAgo = now.minusSeconds(5);
139 
140         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
141         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
142         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
143         resp1.setHeader("Cache-Control", "max-age=3600");
144         resp1.setHeader("Etag", "\"new-etag\"");
145 
146         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
147         req2.setHeader("Cache-Control", "no-cache");
148         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
149         resp2.setHeader("ETag", "\"old-etag\"");
150         resp2.setHeader("Date", DateUtils.formatStandardDate(fiveSecondsAgo));
151         resp2.setHeader("Cache-Control", "max-age=3600");
152 
153         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
154 
155         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
156 
157         execute(req1);
158 
159         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
160 
161         execute(req2);
162         final ClassicHttpResponse result = execute(req3);
163 
164         Assertions.assertEquals("\"new-etag\"", result.getFirstHeader("ETag").getValue());
165     }
166 
167     @Test
168     public void testNewerCacheableResponsesReplaceExistingCacheEntry() throws Exception {
169         final Instant now = Instant.now();
170         final Instant fiveSecondsAgo = now.minusSeconds(5);
171 
172         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
173         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
174         resp1.setHeader("Date", DateUtils.formatStandardDate(fiveSecondsAgo));
175         resp1.setHeader("Cache-Control", "max-age=3600");
176         resp1.setHeader("Etag", "\"old-etag\"");
177 
178         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
179         req2.setHeader("Cache-Control", "max-age=0");
180         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
181         resp2.setHeader("ETag", "\"new-etag\"");
182         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
183         resp2.setHeader("Cache-Control", "max-age=3600");
184 
185         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
186 
187         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
188 
189         execute(req1);
190 
191         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
192 
193         execute(req2);
194         final ClassicHttpResponse result = execute(req3);
195 
196         Assertions.assertEquals("\"new-etag\"", result.getFirstHeader("ETag").getValue());
197     }
198 
199     @Test
200     public void testNonCacheableResponseIsNotCachedAndIsReturnedAsIs() throws Exception {
201         final HttpCache cache = new BasicHttpCache(new HeapResourceFactory(), mockStorage);
202         impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
203 
204         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
205         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
206         resp1.setHeader("Cache-Control", "no-store");
207 
208         Mockito.when(mockStorage.getEntry(Mockito.any())).thenReturn(null);
209         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
210 
211         final ClassicHttpResponse result = execute(req1);
212 
213         Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
214 
215         Mockito.verify(mockStorage, Mockito.never()).putEntry(Mockito.any(), Mockito.any());
216     }
217 
218     @Test
219     public void testSetsModuleGeneratedResponseContextForCacheOptionsResponse() throws Exception {
220         final ClassicHttpRequest req = new BasicClassicHttpRequest("OPTIONS", "*");
221         req.setHeader("Max-Forwards", "0");
222 
223         execute(req);
224         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
225     }
226 
227     @Test
228     public void testSetsCacheMissContextIfRequestNotServableFromCache() throws Exception {
229         final ClassicHttpRequest req = new HttpGet("http://foo.example.com/");
230         req.setHeader("Cache-Control", "no-cache");
231         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
232 
233         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
234 
235         execute(req);
236         Assertions.assertEquals(CacheResponseStatus.CACHE_MISS, context.getCacheResponseStatus());
237     }
238 
239     @Test
240     public void testSetsCacheHitContextIfRequestServedFromCache() throws Exception {
241         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
242         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
243         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
244         resp1.setEntity(HttpTestUtils.makeBody(128));
245         resp1.setHeader("Content-Length", "128");
246         resp1.setHeader("ETag", "\"etag\"");
247         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
248         resp1.setHeader("Cache-Control", "public, max-age=3600");
249 
250         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
251 
252         execute(req1);
253         execute(req2);
254         Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus());
255     }
256 
257     @Test
258     public void testReturns304ForIfModifiedSinceHeaderIfRequestServedFromCache() throws Exception {
259         final Instant now = Instant.now();
260         final Instant tenSecondsAgo = now.minusSeconds(10);
261         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
262         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
263         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
264         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
265         resp1.setEntity(HttpTestUtils.makeBody(128));
266         resp1.setHeader("Content-Length", "128");
267         resp1.setHeader("ETag", "\"etag\"");
268         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
269         resp1.setHeader("Cache-Control", "public, max-age=3600");
270         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
271 
272         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
273 
274         execute(req1);
275         final ClassicHttpResponse result = execute(req2);
276         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
277     }
278 
279     @Test
280     public void testReturns304ForIfModifiedSinceHeaderIf304ResponseInCache() throws Exception {
281         final Instant now = Instant.now();
282         final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
283         final Instant inTenMinutes = now.plus(10, ChronoUnit.MINUTES);
284         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
285         req1.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
286         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
287         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
288 
289         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
290         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
291         resp1.setHeader("Cache-control", "max-age=600");
292         resp1.setHeader("Expires", DateUtils.formatStandardDate(inTenMinutes));
293         resp1.setHeader("ETag", "\"etag\"");
294 
295         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
296 
297         execute(req1);
298 
299         final ClassicHttpResponse result = execute(req2);
300         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
301         Assertions.assertFalse(result.containsHeader("Last-Modified"));
302 
303         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
304     }
305 
306     @Test
307     public void testReturns304ForIfModifiedSinceHeaderIf304ResponseInCacheWithLastModified() throws Exception {
308         final Instant now = Instant.now();
309         final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
310         final Instant inTenMinutes = now.plus(10, ChronoUnit.MINUTES);
311         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
312         req1.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
313         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
314         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(oneHourAgo));
315 
316         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
317         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
318         resp1.setHeader("Cache-control", "max-age=600");
319         resp1.setHeader("Expires", DateUtils.formatStandardDate(inTenMinutes));
320 
321         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
322 
323         execute(req1);
324 
325         final ClassicHttpResponse result = execute(req2);
326         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
327         Assertions.assertTrue(result.containsHeader("Last-Modified"));
328 
329         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
330     }
331 
332     @Test
333     public void testReturns200ForIfModifiedSinceDateIsLess() throws Exception {
334         final Instant now = Instant.now();
335         final Instant tenSecondsAgo = now.minusSeconds(10);
336         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
337         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
338 
339         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
340         resp1.setEntity(HttpTestUtils.makeBody(128));
341         resp1.setHeader("Content-Length", "128");
342         resp1.setHeader("ETag", "\"etag\"");
343         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
344         resp1.setHeader("Cache-Control", "public, max-age=3600");
345         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
346 
347         // The variant has been modified since this date
348         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
349 
350         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
351 
352         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
353 
354         execute(req1);
355 
356         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
357 
358         final ClassicHttpResponse result = execute(req2);
359         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
360     }
361 
362     @Test
363     public void testReturns200ForIfModifiedSinceDateIsInvalid() throws Exception {
364         final Instant now = Instant.now();
365         final Instant tenSecondsAfter = now.plusSeconds(10);
366         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
367         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
368 
369         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
370         resp1.setEntity(HttpTestUtils.makeBody(128));
371         resp1.setHeader("Content-Length", "128");
372         resp1.setHeader("ETag", "\"etag\"");
373         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
374         resp1.setHeader("Cache-Control", "public, max-age=3600");
375         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
376 
377         // invalid date (date in the future)
378         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAfter));
379 
380         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
381 
382         execute(req1);
383         final ClassicHttpResponse result = execute(req2);
384         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
385 
386         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
387     }
388 
389     @Test
390     public void testReturns304ForIfNoneMatchHeaderIfRequestServedFromCache() throws Exception {
391         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
392         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
393         req2.addHeader("If-None-Match", "*");
394         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
395         resp1.setEntity(HttpTestUtils.makeBody(128));
396         resp1.setHeader("Content-Length", "128");
397         resp1.setHeader("ETag", "\"etag\"");
398         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
399         resp1.setHeader("Cache-Control", "public, max-age=3600");
400 
401         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
402 
403         execute(req1);
404         final ClassicHttpResponse result = execute(req2);
405         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
406 
407     }
408 
409     @Test
410     public void testReturns200ForIfNoneMatchHeaderFails() throws Exception {
411         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
412         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
413 
414         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
415         resp1.setEntity(HttpTestUtils.makeBody(128));
416         resp1.setHeader("Content-Length", "128");
417         resp1.setHeader("ETag", "\"etag\"");
418         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
419         resp1.setHeader("Cache-Control", "public, max-age=3600");
420 
421         req2.addHeader("If-None-Match", "\"abc\"");
422 
423         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
424 
425         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
426 
427         execute(req1);
428 
429         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
430 
431         final ClassicHttpResponse result = execute(req2);
432         Assertions.assertEquals(200, result.getCode());
433     }
434 
435     @Test
436     public void testReturns304ForIfNoneMatchHeaderAndIfModifiedSinceIfRequestServedFromCache() throws Exception {
437         final Instant now = Instant.now();
438         final Instant tenSecondsAgo = now.minusSeconds(10);
439         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
440         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
441 
442         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
443         resp1.setEntity(HttpTestUtils.makeBody(128));
444         resp1.setHeader("Content-Length", "128");
445         resp1.setHeader("ETag", "\"etag\"");
446         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
447         resp1.setHeader("Cache-Control", "public, max-age=3600");
448         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
449 
450         req2.addHeader("If-None-Match", "*");
451         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
452 
453         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
454 
455         execute(req1);
456         final ClassicHttpResponse result = execute(req2);
457         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
458     }
459 
460     @Test
461     public void testReturns200ForIfNoneMatchHeaderFailsIfModifiedSinceIgnored() throws Exception {
462         final Instant now = Instant.now();
463         final Instant tenSecondsAgo = now.minusSeconds(10);
464         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
465         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
466         req2.addHeader("If-None-Match", "\"abc\"");
467         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(now));
468         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
469         resp1.setEntity(HttpTestUtils.makeBody(128));
470         resp1.setHeader("Content-Length", "128");
471         resp1.setHeader("ETag", "\"etag\"");
472         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
473         resp1.setHeader("Cache-Control", "public, max-age=3600");
474         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
475 
476         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
477 
478         execute(req1);
479         final ClassicHttpResponse result = execute(req2);
480         Assertions.assertEquals(200, result.getCode());
481     }
482 
483     @Test
484     public void testReturns200ForOptionsFollowedByGetIfAuthorizationHeaderAndSharedCache() throws Exception {
485         impl = new CachingExec(cache, null, CacheConfig.custom().setSharedCache(true).build());
486         final Instant now = Instant.now();
487         final ClassicHttpRequest req1 = new HttpOptions("http://foo.example.com/");
488         req1.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
489         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
490         req2.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
491         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
492         resp1.setHeader("Content-Length", "0");
493         resp1.setHeader("ETag", "\"options-etag\"");
494         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
495         resp1.setHeader("Cache-Control", "public, max-age=3600");
496         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
497         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
498         resp1.setEntity(HttpTestUtils.makeBody(128));
499         resp1.setHeader("Content-Length", "128");
500         resp1.setHeader("ETag", "\"get-etag\"");
501         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
502         resp1.setHeader("Cache-Control", "public, max-age=3600");
503         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
504 
505         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
506         execute(req1);
507 
508         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
509 
510         final ClassicHttpResponse result = execute(req2);
511         Assertions.assertEquals(200, result.getCode());
512     }
513 
514     @Test
515     public void testSetsValidatedContextIfRequestWasSuccessfullyValidated() throws Exception {
516         final Instant now = Instant.now();
517         final Instant tenSecondsAgo = now.minusSeconds(10);
518 
519         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
520         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
521 
522         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
523         resp1.setEntity(HttpTestUtils.makeBody(128));
524         resp1.setHeader("Content-Length", "128");
525         resp1.setHeader("ETag", "\"etag\"");
526         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
527         resp1.setHeader("Cache-Control", "public, max-age=5");
528 
529         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
530         resp2.setEntity(HttpTestUtils.makeBody(128));
531         resp2.setHeader("Content-Length", "128");
532         resp2.setHeader("ETag", "\"etag\"");
533         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
534         resp2.setHeader("Cache-Control", "public, max-age=5");
535 
536         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
537         execute(req1);
538 
539         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
540 
541         execute(req2);
542         Assertions.assertEquals(CacheResponseStatus.VALIDATED, context.getCacheResponseStatus());
543     }
544 
545     @Test
546     public void testSetsModuleResponseContextIfValidationRequiredButFailed() throws Exception {
547         final Instant now = Instant.now();
548         final Instant tenSecondsAgo = now.minusSeconds(10);
549 
550         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
551         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
552 
553         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
554         resp1.setEntity(HttpTestUtils.makeBody(128));
555         resp1.setHeader("Content-Length", "128");
556         resp1.setHeader("ETag", "\"etag\"");
557         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
558         resp1.setHeader("Cache-Control", "public, max-age=5, must-revalidate");
559 
560         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
561 
562         execute(req1);
563 
564         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
565 
566         execute(req2);
567         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE,
568                 context.getCacheResponseStatus());
569     }
570 
571     @Test
572     public void testSetsModuleResponseContextIfValidationFailsButNotRequired() throws Exception {
573         final Instant now = Instant.now();
574         final Instant tenSecondsAgo = now.minusSeconds(10);
575 
576         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
577         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
578 
579         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
580         resp1.setEntity(HttpTestUtils.makeBody(128));
581         resp1.setHeader("Content-Length", "128");
582         resp1.setHeader("ETag", "\"etag\"");
583         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
584         resp1.setHeader("Cache-Control", "public, max-age=5");
585 
586         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
587 
588         execute(req1);
589 
590         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
591 
592         execute(req2);
593         Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
594     }
595 
596     @Test
597     public void testReturns304ForIfNoneMatchPassesIfRequestServedFromOrigin() throws Exception {
598 
599         final Instant now = Instant.now();
600         final Instant tenSecondsAgo = now.minusSeconds(10);
601 
602         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
603         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
604 
605         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
606         resp1.setEntity(HttpTestUtils.makeBody(128));
607         resp1.setHeader("Content-Length", "128");
608         resp1.setHeader("ETag", "\"etag\"");
609         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
610         resp1.setHeader("Cache-Control", "public, max-age=5");
611 
612         req2.addHeader("If-None-Match", "\"etag\"");
613         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
614         resp2.setHeader("ETag", "\"etag\"");
615         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
616         resp2.setHeader("Cache-Control", "public, max-age=5");
617 
618         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
619         execute(req1);
620 
621         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
622 
623         final ClassicHttpResponse result = execute(req2);
624 
625         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
626     }
627 
628     @Test
629     public void testReturns200ForIfNoneMatchFailsIfRequestServedFromOrigin() throws Exception {
630 
631         final Instant now = Instant.now();
632         final Instant tenSecondsAgo = now.minusSeconds(10);
633 
634         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
635         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
636 
637         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
638         resp1.setEntity(HttpTestUtils.makeBody(128));
639         resp1.setHeader("Content-Length", "128");
640         resp1.setHeader("ETag", "\"etag\"");
641         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
642         resp1.setHeader("Cache-Control", "public, max-age=5");
643 
644         req2.addHeader("If-None-Match", "\"etag\"");
645         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
646         resp2.setEntity(HttpTestUtils.makeBody(128));
647         resp2.setHeader("Content-Length", "128");
648         resp2.setHeader("ETag", "\"newetag\"");
649         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
650         resp2.setHeader("Cache-Control", "public, max-age=5");
651 
652         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
653         execute(req1);
654 
655         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
656 
657         final ClassicHttpResponse result = execute(req2);
658 
659         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
660     }
661 
662     @Test
663     public void testReturns304ForIfModifiedSincePassesIfRequestServedFromOrigin() throws Exception {
664         final Instant now = Instant.now();
665         final Instant tenSecondsAgo = now.minusSeconds(10);
666 
667         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
668         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
669 
670         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
671         resp1.setEntity(HttpTestUtils.makeBody(128));
672         resp1.setHeader("Content-Length", "128");
673         resp1.setHeader("ETag", "\"etag\"");
674         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
675         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
676         resp1.setHeader("Cache-Control", "public, max-age=5");
677 
678         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
679         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
680         resp2.setHeader("ETag", "\"etag\"");
681         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
682         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
683         resp2.setHeader("Cache-Control", "public, max-age=5");
684 
685         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
686 
687         execute(req1);
688 
689         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
690 
691         final ClassicHttpResponse result = execute(req2);
692 
693         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
694     }
695 
696     @Test
697     public void testReturns200ForIfModifiedSinceFailsIfRequestServedFromOrigin() throws Exception {
698         final Instant now = Instant.now();
699         final Instant tenSecondsAgo = now.minusSeconds(10);
700 
701         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
702         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
703 
704         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
705         resp1.setEntity(HttpTestUtils.makeBody(128));
706         resp1.setHeader("Content-Length", "128");
707         resp1.setHeader("ETag", "\"etag\"");
708         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
709         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
710         resp1.setHeader("Cache-Control", "public, max-age=5");
711 
712         req2.addHeader("If-Modified-Since", DateUtils.formatStandardDate(tenSecondsAgo));
713         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
714         resp2.setEntity(HttpTestUtils.makeBody(128));
715         resp2.setHeader("Content-Length", "128");
716         resp2.setHeader("ETag", "\"newetag\"");
717         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
718         resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
719         resp2.setHeader("Cache-Control", "public, max-age=5");
720 
721         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
722 
723         execute(req1);
724 
725         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
726 
727         final ClassicHttpResponse result = execute(req2);
728 
729         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
730     }
731 
732     @Test
733     public void testVariantMissServerIfReturns304CacheReturns200() throws Exception {
734         final Instant now = Instant.now();
735 
736         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
737         req1.addHeader("Accept-Encoding", "gzip");
738 
739         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
740         resp1.setEntity(HttpTestUtils.makeBody(128));
741         resp1.setHeader("Content-Length", "128");
742         resp1.setHeader("Etag", "\"gzip_etag\"");
743         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
744         resp1.setHeader("Vary", "Accept-Encoding");
745         resp1.setHeader("Cache-Control", "public, max-age=3600");
746 
747         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com");
748         req2.addHeader("Accept-Encoding", "deflate");
749 
750         final ClassicHttpRequest req2Server = new HttpGet("http://foo.example.com");
751         req2Server.addHeader("Accept-Encoding", "deflate");
752         req2Server.addHeader("If-None-Match", "\"gzip_etag\"");
753 
754         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
755         resp2.setEntity(HttpTestUtils.makeBody(128));
756         resp2.setHeader("Content-Length", "128");
757         resp2.setHeader("Etag", "\"deflate_etag\"");
758         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
759         resp2.setHeader("Vary", "Accept-Encoding");
760         resp2.setHeader("Cache-Control", "public, max-age=3600");
761 
762         final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
763         req3.addHeader("Accept-Encoding", "gzip,deflate");
764 
765         final ClassicHttpRequest req3Server = new HttpGet("http://foo.example.com");
766         req3Server.addHeader("Accept-Encoding", "gzip,deflate");
767         req3Server.addHeader("If-None-Match", "\"gzip_etag\",\"deflate_etag\"");
768 
769         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
770         resp3.setEntity(HttpTestUtils.makeBody(128));
771         resp3.setHeader("Content-Length", "128");
772         resp3.setHeader("Etag", "\"gzip_etag\"");
773         resp3.setHeader("Date", DateUtils.formatStandardDate(now));
774         resp3.setHeader("Vary", "Accept-Encoding");
775         resp3.setHeader("Cache-Control", "public, max-age=3600");
776 
777         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
778 
779         final ClassicHttpResponse result1 = execute(req1);
780 
781         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
782 
783         final ClassicHttpResponse result2 = execute(req2);
784 
785         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
786 
787         final ClassicHttpResponse result3 = execute(req3);
788 
789         Assertions.assertEquals(HttpStatus.SC_OK, result1.getCode());
790         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
791         Assertions.assertEquals(HttpStatus.SC_OK, result3.getCode());
792     }
793 
794     @Test
795     public void testVariantsMissServerReturns304CacheReturns304() throws Exception {
796         final Instant now = Instant.now();
797 
798         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
799         req1.addHeader("Accept-Encoding", "gzip");
800 
801         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
802         resp1.setEntity(HttpTestUtils.makeBody(128));
803         resp1.setHeader("Content-Length", "128");
804         resp1.setHeader("Etag", "\"gzip_etag\"");
805         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
806         resp1.setHeader("Vary", "Accept-Encoding");
807         resp1.setHeader("Cache-Control", "public, max-age=3600");
808 
809         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com");
810         req2.addHeader("Accept-Encoding", "deflate");
811 
812         final ClassicHttpRequest req2Server = new HttpGet("http://foo.example.com");
813         req2Server.addHeader("Accept-Encoding", "deflate");
814         req2Server.addHeader("If-None-Match", "\"gzip_etag\"");
815 
816         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
817         resp2.setEntity(HttpTestUtils.makeBody(128));
818         resp2.setHeader("Content-Length", "128");
819         resp2.setHeader("Etag", "\"deflate_etag\"");
820         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
821         resp2.setHeader("Vary", "Accept-Encoding");
822         resp2.setHeader("Cache-Control", "public, max-age=3600");
823 
824         final ClassicHttpRequest req4 = new HttpGet("http://foo.example.com");
825         req4.addHeader("Accept-Encoding", "gzip,identity");
826         req4.addHeader("If-None-Match", "\"gzip_etag\"");
827 
828         final ClassicHttpRequest req4Server = new HttpGet("http://foo.example.com");
829         req4Server.addHeader("Accept-Encoding", "gzip,identity");
830         req4Server.addHeader("If-None-Match", "\"gzip_etag\"");
831 
832         final ClassicHttpResponse resp4 = HttpTestUtils.make304Response();
833         resp4.setHeader("Etag", "\"gzip_etag\"");
834         resp4.setHeader("Date", DateUtils.formatStandardDate(now));
835         resp4.setHeader("Vary", "Accept-Encoding");
836         resp4.setHeader("Cache-Control", "public, max-age=3600");
837 
838         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
839 
840         final ClassicHttpResponse result1 = execute(req1);
841 
842         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
843 
844         final ClassicHttpResponse result2 = execute(req2);
845 
846         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp4);
847 
848         final ClassicHttpResponse result4 = execute(req4);
849         Assertions.assertEquals(HttpStatus.SC_OK, result1.getCode());
850         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
851         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result4.getCode());
852 
853     }
854 
855     @Test
856     public void testSocketTimeoutExceptionIsNotSilentlyCatched() throws Exception {
857         final Instant now = Instant.now();
858 
859         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com");
860 
861         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
862         resp1.setEntity(new InputStreamEntity(new InputStream() {
863             private boolean closed;
864 
865             @Override
866             public void close() throws IOException {
867                 closed = true;
868             }
869 
870             @Override
871             public int read() throws IOException {
872                 if (closed) {
873                     throw new SocketException("Socket closed");
874                 }
875                 throw new SocketTimeoutException("Read timed out");
876             }
877         }, 128, null));
878         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
879 
880         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
881 
882         Assertions.assertThrows(SocketTimeoutException.class, () -> {
883             final ClassicHttpResponse result1 = execute(req1);
884             EntityUtils.toString(result1.getEntity());
885         });
886     }
887 
888     @Test
889     public void testTooLargeResponsesAreNotCached() throws Exception {
890         final HttpHost host = new HttpHost("foo.example.com");
891         final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
892 
893         final Instant now = Instant.now();
894         final Instant requestSent = now.plusSeconds(3);
895         final Instant responseGenerated = now.plusSeconds(2);
896         final Instant responseReceived = now.plusSeconds(1);
897 
898         final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
899         originResponse.setEntity(HttpTestUtils.makeBody(CacheConfig.DEFAULT_MAX_OBJECT_SIZE_BYTES + 1));
900         originResponse.setHeader("Cache-Control","public, max-age=3600");
901         originResponse.setHeader("Date", DateUtils.formatStandardDate(responseGenerated));
902         originResponse.setHeader("ETag", "\"etag\"");
903 
904         impl.cacheAndReturnResponse(host, request, scope, originResponse, requestSent, responseReceived);
905 
906         Mockito.verify(cache, Mockito.never()).store(
907                 Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
908     }
909 
910     @Test
911     public void testSmallEnoughResponsesAreCached() throws Exception {
912         final HttpCache mockCache = mock(HttpCache.class);
913         impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
914 
915         final HttpHost host = new HttpHost("foo.example.com");
916         final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
917 
918         final Instant now = Instant.now();
919         final Instant requestSent = now.plusSeconds(3);
920         final Instant responseGenerated = now.plusSeconds(2);
921         final Instant responseReceived = now.plusSeconds(1);
922 
923         final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
924         originResponse.setEntity(HttpTestUtils.makeBody(CacheConfig.DEFAULT_MAX_OBJECT_SIZE_BYTES - 1));
925         originResponse.setHeader("Cache-Control","public, max-age=3600");
926         originResponse.setHeader("Date", DateUtils.formatStandardDate(responseGenerated));
927         originResponse.setHeader("ETag", "\"etag\"");
928 
929         final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
930         final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
931 
932         Mockito.when(mockCache.store(
933                 Mockito.eq(host),
934                 RequestEquivalent.eq(request),
935                 ResponseEquivalent.eq(response),
936                 Mockito.any(),
937                 Mockito.eq(requestSent),
938                 Mockito.eq(responseReceived))).thenReturn(new CacheHit("key", httpCacheEntry));
939 
940         impl.cacheAndReturnResponse(host, request, scope, originResponse, requestSent, responseReceived);
941 
942         Mockito.verify(mockCache).store(
943                 Mockito.any(),
944                 Mockito.any(),
945                 Mockito.any(),
946                 Mockito.any(),
947                 Mockito.any(),
948                 Mockito.any());
949     }
950 
951     @Test
952     public void testIfOnlyIfCachedAndNoCacheEntryBackendNotCalled() throws Exception {
953         request.addHeader("Cache-Control", "only-if-cached");
954 
955         final ClassicHttpResponse resp = execute(request);
956 
957         Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, resp.getCode());
958     }
959 
960     @Test
961     public void testCanCacheAResponseWithoutABody() throws Exception {
962         final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
963         response.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
964         response.setHeader("Cache-Control", "max-age=300");
965         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response);
966 
967         impl.execute(request, scope, mockExecChain);
968         impl.execute(request, scope, mockExecChain);
969 
970         Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any());
971     }
972 
973     @Test
974     public void testNoEntityForIfNoneMatchRequestNotYetInCache() throws Exception {
975 
976         final Instant now = Instant.now();
977         final Instant tenSecondsAgo = now.minusSeconds(10);
978 
979         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
980         req1.addHeader("If-None-Match", "\"etag\"");
981 
982         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
983         resp1.setHeader("Content-Length", "128");
984         resp1.setHeader("ETag", "\"etag\"");
985         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
986         resp1.setHeader("Cache-Control", "public, max-age=5");
987 
988         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
989         final ClassicHttpResponse result = execute(req1);
990 
991         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
992         Assertions.assertNull(result.getEntity(), "The 304 response messages MUST NOT contain a message-body");
993     }
994 
995     @Test
996     public void testNotModifiedResponseUpdatesCacheEntryWhenNoEntity() throws Exception {
997 
998         final Instant now = Instant.now();
999 
1000         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1001         req1.addHeader("If-None-Match", "\"etag\"");
1002 
1003         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1004         req2.addHeader("If-None-Match", "\"etag\"");
1005 
1006         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1007         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1008         resp1.setHeader("Cache-Control", "max-age=1");
1009         resp1.setHeader("Etag", "\"etag\"");
1010 
1011         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
1012         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1013         resp2.setHeader("Cache-Control", "max-age=1");
1014         resp1.setHeader("Etag", "\"etag\"");
1015 
1016         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1017 
1018         final ClassicHttpResponse result1 = execute(req1);
1019         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1020 
1021         final ClassicHttpResponse result2 = execute(req2);
1022 
1023         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1024         Assertions.assertEquals(new ETag("etag"), ETag.get(result1));
1025         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
1026         Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
1027     }
1028 
1029     @Test
1030     public void testNotModifiedResponseWithVaryUpdatesCacheEntryWhenNoEntity() throws Exception {
1031 
1032         final Instant now = Instant.now();
1033 
1034         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1035         req1.addHeader("If-None-Match", "\"etag\"");
1036 
1037         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1038         req2.addHeader("If-None-Match", "\"etag\"");
1039 
1040         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1041         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1042         resp1.setHeader("Cache-Control", "max-age=1");
1043         resp1.setHeader("Etag", "\"etag\"");
1044         resp1.setHeader("Vary", "Accept-Encoding");
1045 
1046         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
1047         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1048         resp2.setHeader("Cache-Control", "max-age=1");
1049         resp1.setHeader("Etag", "\"etag\"");
1050         resp1.setHeader("Vary", "Accept-Encoding");
1051 
1052         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1053 
1054         final ClassicHttpResponse result1 = execute(req1);
1055 
1056         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1057 
1058         final ClassicHttpResponse result2 = execute(req2);
1059 
1060         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1061         Assertions.assertEquals(new ETag("etag"), ETag.get(result1));
1062         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result2.getCode());
1063         Assertions.assertEquals(new ETag("etag"), ETag.get(result2));
1064     }
1065 
1066     @Test
1067     public void testDoesNotSend304ForNonConditionalRequest() throws Exception {
1068 
1069         final Instant now = Instant.now();
1070         final Instant inOneMinute = now.plus(1, ChronoUnit.MINUTES);
1071 
1072         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1073         req1.addHeader("If-None-Match", "etag");
1074 
1075         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1076 
1077         final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
1078         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1079         resp1.setHeader("Cache-Control", "public, max-age=60");
1080         resp1.setHeader("Expires", DateUtils.formatStandardDate(inOneMinute));
1081         resp1.setHeader("Etag", "etag");
1082         resp1.setHeader("Vary", "Accept-Encoding");
1083 
1084         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK,
1085                 "Ok");
1086         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1087         resp2.setHeader("Cache-Control", "public, max-age=60");
1088         resp2.setHeader("Expires", DateUtils.formatStandardDate(inOneMinute));
1089         resp2.setHeader("Etag", "etag");
1090         resp2.setHeader("Vary", "Accept-Encoding");
1091         resp2.setEntity(HttpTestUtils.makeBody(128));
1092 
1093         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1094 
1095         final ClassicHttpResponse result1 = execute(req1);
1096 
1097         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1098 
1099         final ClassicHttpResponse result2 = execute(req2);
1100 
1101         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result1.getCode());
1102         Assertions.assertNull(result1.getEntity());
1103         Assertions.assertEquals(HttpStatus.SC_OK, result2.getCode());
1104         Assertions.assertNotNull(result2.getEntity());
1105     }
1106 
1107     @Test
1108     public void testUsesVirtualHostForCacheKey() throws Exception {
1109         final ClassicHttpResponse response = HttpTestUtils.make200Response();
1110         response.setHeader("Cache-Control", "max-age=3600");
1111         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(response);
1112 
1113         impl.execute(request, scope, mockExecChain);
1114 
1115         Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
1116 
1117         request.setAuthority(new URIAuthority("bar.example.com"));
1118         impl.execute(request, scope, mockExecChain);
1119 
1120         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1121 
1122         impl.execute(request, scope, mockExecChain);
1123 
1124         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
1125     }
1126 
1127     @Test
1128     public void testReturnssetStaleIfErrorNotEnabled() throws Exception {
1129 
1130         // Create the first request and response
1131         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1132         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
1133 
1134         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1135         resp1.setEntity(HttpTestUtils.makeBody(128));
1136         resp1.setHeader("Content-Length", "128");
1137         resp1.setHeader("ETag", "\"etag\"");
1138         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1139         resp1.setHeader("Cache-Control", "public");
1140 
1141         req2.addHeader("If-None-Match", "\"abc\"");
1142 
1143         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1144 
1145         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1146 
1147         execute(req1);
1148 
1149         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1150         Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
1151         final ClassicHttpResponse result = execute(req2);
1152         Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
1153 
1154         Mockito.verify(cacheRevalidator, Mockito.never()).revalidateCacheEntry(Mockito.any(), Mockito.any());
1155     }
1156 
1157 
1158     @Test
1159     public void testReturnssetStaleIfErrorEnabled() throws Exception {
1160         final CacheConfig customConfig = CacheConfig.custom()
1161                 .setMaxCacheEntries(100)
1162                 .setMaxObjectSize(1024)
1163                 .setSharedCache(false)
1164                 .setStaleIfErrorEnabled(true)
1165                 .build();
1166 
1167         impl = new CachingExec(cache, cacheRevalidator, customConfig);
1168 
1169         // Create the first request and response
1170         final BasicClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
1171         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1172         resp1.setEntity(HttpTestUtils.makeBody(128));
1173         resp1.setHeader("Content-Length", "128");
1174         resp1.setHeader("ETag", "\"abc\"");
1175         resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now().minus(Duration.ofHours(10))));
1176         resp1.setHeader("Cache-Control", "public, stale-while-revalidate=1");
1177 
1178         final BasicClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
1179         req2.addHeader("If-None-Match", "\"abc\"");
1180         final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
1181 
1182         // Set up the mock response chain
1183         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1184 
1185         // Execute the first request and assert the response
1186         final ClassicHttpResponse response1 = execute(req1);
1187         Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
1188 
1189         // Execute the second request and assert the response
1190         Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
1191         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1192         final ClassicHttpResponse response2 = execute(req2);
1193         Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
1194 
1195         Mockito.verify(cacheRevalidator, Mockito.never()).revalidateCacheEntry(Mockito.any(), Mockito.any());
1196     }
1197 
1198     @Test
1199     public void testNotModifiedResponseUpdatesCacheEntry() throws Exception {
1200         final HttpCache mockCache = mock(HttpCache.class);
1201         impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
1202         // Prepare request and host
1203         final HttpHost host = new HttpHost("foo.example.com");
1204         final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
1205 
1206         // Prepare original cache entry
1207         final HttpCacheEntry originalEntry = HttpTestUtils.makeCacheEntry();
1208         Mockito.when(mockCache.match(host, request)).thenReturn(
1209                 new CacheMatch(new CacheHit("key", originalEntry), null));
1210 
1211         // Prepare 304 Not Modified response
1212         final Instant now = Instant.now();
1213         final Instant requestSent = now.plusSeconds(3);
1214         final Instant responseReceived = now.plusSeconds(1);
1215 
1216         final ClassicHttpResponse backendResponse = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1217         backendResponse.setHeader("Cache-Control", "public, max-age=3600");
1218         backendResponse.setHeader("ETag", "\"etag\"");
1219 
1220         final Header[] headers = new Header[5];
1221         for (int i = 0; i < headers.length; i++) {
1222             headers[i] = new BasicHeader("header" + i, "value" + i);
1223         }
1224         final String body = "Lorem ipsum dolor sit amet";
1225 
1226         final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry(
1227                 Instant.now(),
1228                 Instant.now(),
1229                 HttpStatus.SC_NOT_MODIFIED,
1230                 headers,
1231                 new HeapResource(body.getBytes(StandardCharsets.UTF_8)));
1232 
1233         Mockito.when(mockCache.update(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
1234                 .thenReturn(new CacheHit("key", cacheEntry));
1235 
1236         // Call cacheAndReturnResponse with 304 Not Modified response
1237         final ClassicHttpResponse cachedResponse = impl.cacheAndReturnResponse(host, request, scope, backendResponse, requestSent, responseReceived);
1238 
1239         // Verify cache entry is updated
1240         Mockito.verify(mockCache).update(
1241                 Mockito.any(),
1242                 Mockito.same(host),
1243                 Mockito.same(request),
1244                 Mockito.same(backendResponse),
1245                 Mockito.eq(requestSent),
1246                 Mockito.eq(responseReceived)
1247         );
1248 
1249         // Verify response is generated from the updated cache entry
1250         Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, cachedResponse.getCode());
1251     }
1252 
1253     @Test
1254     public void testNoCacheFieldsRevalidation() throws Exception {
1255         final Instant now = Instant.now();
1256         final Instant fiveSecondsAgo = now.minusSeconds(5);
1257 
1258         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1259         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1260         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1261         resp1.setHeader("Cache-Control", "max-age=3100, no-cache=\"Set-Cookie, Content-Language\"");
1262         resp1.setHeader("Content-Language", "en-US");
1263         resp1.setHeader("Etag", "\"new-etag\"");
1264 
1265         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1266         //req2.setHeader("Cache-Control", "no-cache=\"etag\"");
1267         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1268         resp2.setHeader("ETag", "\"old-etag\"");
1269         resp2.setHeader("Date", DateUtils.formatStandardDate(fiveSecondsAgo));
1270         resp2.setHeader("Cache-Control", "max-age=3600");
1271 
1272         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
1273 
1274         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1275 
1276 
1277         execute(req1);
1278 
1279         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1280 
1281         execute(req2);
1282         final ClassicHttpResponse result = execute(req3);
1283 
1284         // Verify that the backend was called to revalidate the response, as per the new logic
1285         Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any());
1286     }
1287 
1288 }