1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 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 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 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 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 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 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 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 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 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 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 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 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 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
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 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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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() {
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 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 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 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 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 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 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 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 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 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 void testReturnssetStaleIfErrorNotEnabled() throws Exception {
1129
1130
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 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
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
1183 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1184
1185
1186 final ClassicHttpResponse response1 = execute(req1);
1187 Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
1188
1189
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 void testNotModifiedResponseUpdatesCacheEntry() throws Exception {
1200 final HttpCache mockCache = mock(HttpCache.class);
1201 impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
1202
1203 final HttpHost host = new HttpHost("foo.example.com");
1204 final ClassicHttpRequest request = new HttpGet("http://foo.example.com/bar");
1205
1206
1207 final HttpCacheEntry originalEntry = HttpTestUtils.makeCacheEntry();
1208 Mockito.when(mockCache.match(host, request)).thenReturn(
1209 new CacheMatch(new CacheHit("key", originalEntry), null));
1210
1211
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
1237 final ClassicHttpResponse cachedResponse = impl.cacheAndReturnResponse(host, request, scope, backendResponse, requestSent, responseReceived);
1238
1239
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
1250 Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, cachedResponse.getCode());
1251 }
1252
1253 @Test
1254 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
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 execute(req3);
1283
1284
1285 Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any());
1286 }
1287
1288 }