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