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