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.assertEquals;
31 import static org.junit.jupiter.api.Assertions.assertFalse;
32 import static org.junit.jupiter.api.Assertions.assertSame;
33 import static org.junit.jupiter.api.Assertions.assertTrue;
34
35 import java.time.Instant;
36
37 import org.apache.hc.client5.http.cache.HttpCacheEntry;
38 import org.apache.hc.client5.http.utils.DateUtils;
39 import org.apache.hc.core5.http.Header;
40 import org.apache.hc.core5.http.HttpHeaders;
41 import org.apache.hc.core5.http.HttpRequest;
42 import org.apache.hc.core5.http.message.BasicHeader;
43 import org.apache.hc.core5.http.message.BasicHttpRequest;
44 import org.apache.hc.core5.util.TimeValue;
45 import org.junit.jupiter.api.BeforeEach;
46 import org.junit.jupiter.api.Test;
47
48 public class TestCacheValidityPolicy {
49
50 private CacheValidityPolicy impl;
51 private Instant now;
52 private Instant oneSecondAgo;
53 private Instant sixSecondsAgo;
54 private Instant tenSecondsAgo;
55 private Instant elevenSecondsAgo;
56
57 @BeforeEach
58 public void setUp() {
59 impl = new CacheValidityPolicy();
60 now = Instant.now();
61 oneSecondAgo = now.minusSeconds(1);
62 sixSecondsAgo = now.minusSeconds(6);
63 tenSecondsAgo = now.minusSeconds(10);
64 elevenSecondsAgo = now.minusSeconds(11);
65 }
66
67 @Test
68 public void testApparentAgeIsMaxIntIfDateHeaderNotPresent() {
69 final Header[] headers = {
70 new BasicHeader("Server", "MockServer/1.0")
71 };
72 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
73 assertEquals(CacheValidityPolicy.MAX_AGE, impl.getApparentAge(entry));
74 }
75
76 @Test
77 public void testApparentAgeIsResponseReceivedTimeLessDateHeader() {
78 final Header[] headers = new Header[] { new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)) };
79
80 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, sixSecondsAgo, headers);
81 assertEquals(TimeValue.ofSeconds(4), impl.getApparentAge(entry));
82 }
83
84 @Test
85 public void testNegativeApparentAgeIsBroughtUpToZero() {
86 final Header[] headers = new Header[] { new BasicHeader("Date", DateUtils.formatStandardDate(sixSecondsAgo)) };
87 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, tenSecondsAgo, headers);
88 assertEquals(TimeValue.ofSeconds(0), impl.getApparentAge(entry));
89 }
90
91 @Test
92 public void testCorrectedReceivedAgeIsAgeHeaderIfLarger() {
93 final Header[] headers = new Header[] { new BasicHeader("Age", "10"), };
94 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
95 impl = new CacheValidityPolicy() {
96 @Override
97 protected TimeValue getApparentAge(final HttpCacheEntry ent) {
98 return TimeValue.ofSeconds(6);
99 }
100 };
101 assertEquals(TimeValue.ofSeconds(10), impl.getCorrectedReceivedAge(entry));
102 }
103
104 @Test
105 public void testCorrectedReceivedAgeIsApparentAgeIfLarger() {
106 final Header[] headers = new Header[] { new BasicHeader("Age", "6"), };
107 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
108 impl = new CacheValidityPolicy() {
109 @Override
110 protected TimeValue getApparentAge(final HttpCacheEntry ent) {
111 return TimeValue.ofSeconds(10);
112 }
113 };
114 assertEquals(TimeValue.ofSeconds(10), impl.getCorrectedReceivedAge(entry));
115 }
116
117 @Test
118 public void testResponseDelayIsDifferenceBetweenResponseAndRequestTimes() {
119 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, sixSecondsAgo);
120 assertEquals(TimeValue.ofSeconds(4), impl.getResponseDelay(entry));
121 }
122
123 @Test
124 public void testCorrectedInitialAgeIsCorrectedReceivedAgePlusResponseDelay() {
125 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
126 impl = new CacheValidityPolicy() {
127 @Override
128 protected TimeValue getCorrectedReceivedAge(final HttpCacheEntry ent) {
129 return TimeValue.ofSeconds(7);
130 }
131
132 @Override
133 protected TimeValue getResponseDelay(final HttpCacheEntry ent) {
134 return TimeValue.ofSeconds(13);
135 }
136 };
137 assertEquals(TimeValue.ofSeconds(20), impl.getCorrectedInitialAge(entry));
138 }
139
140 @Test
141 public void testResidentTimeSecondsIsTimeSinceResponseTime() {
142 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, sixSecondsAgo);
143 assertEquals(TimeValue.ofSeconds(6), impl.getResidentTime(entry, now));
144 }
145
146 @Test
147 public void testCurrentAgeIsCorrectedInitialAgePlusResidentTime() {
148 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
149 impl = new CacheValidityPolicy() {
150 @Override
151 protected TimeValue getCorrectedInitialAge(final HttpCacheEntry ent) {
152 return TimeValue.ofSeconds(11);
153 }
154 @Override
155 protected TimeValue getResidentTime(final HttpCacheEntry ent, final Instant d) {
156 return TimeValue.ofSeconds(17);
157 }
158 };
159 assertEquals(TimeValue.ofSeconds(28), impl.getCurrentAge(entry, Instant.now()));
160 }
161
162 @Test
163 public void testFreshnessLifetimeIsSMaxAgeIfPresent() {
164 final Header[] headers = new Header[] { new BasicHeader("Cache-Control", "s-maxage=10") };
165 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
166 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
167 }
168
169 @Test
170 public void testFreshnessLifetimeIsMaxAgeIfPresent() {
171 final Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10") };
172 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
173 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
174 }
175
176 @Test
177 public void testFreshnessLifetimeIsMostRestrictiveOfMaxAgeAndSMaxAge() {
178 Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10"),
179 new BasicHeader("Cache-Control", "s-maxage=20") };
180 HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
181 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
182
183 headers = new Header[] { new BasicHeader("Cache-Control", "max-age=20"),
184 new BasicHeader("Cache-Control", "s-maxage=10") };
185 entry = HttpTestUtils.makeCacheEntry(headers);
186 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
187 }
188
189 @Test
190 public void testFreshnessLifetimeIsMaxAgeEvenIfExpiresIsPresent() {
191 final Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=10"),
192 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
193 new BasicHeader("Expires", DateUtils.formatStandardDate(sixSecondsAgo)) };
194
195 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
196 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
197 }
198
199 @Test
200 public void testFreshnessLifetimeIsSMaxAgeEvenIfExpiresIsPresent() {
201 final Header[] headers = new Header[] { new BasicHeader("Cache-Control", "s-maxage=10"),
202 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
203 new BasicHeader("Expires", DateUtils.formatStandardDate(sixSecondsAgo)) };
204
205 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
206 assertEquals(TimeValue.ofSeconds(10), impl.getFreshnessLifetime(entry));
207 }
208
209 @Test
210 public void testFreshnessLifetimeIsFromExpiresHeaderIfNoMaxAge() {
211 final Header[] headers = new Header[] {
212 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
213 new BasicHeader("Expires", DateUtils.formatStandardDate(sixSecondsAgo)) };
214
215 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
216 assertEquals(TimeValue.ofSeconds(4), impl.getFreshnessLifetime(entry));
217 }
218
219 @Test
220 public void testHeuristicFreshnessLifetime() {
221 final Header[] headers = new Header[] {
222 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
223 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(elevenSecondsAgo))
224 };
225 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
226 assertEquals(TimeValue.ofSeconds(1), impl.getHeuristicFreshnessLifetime(entry, 0.1f, TimeValue.ZERO_MILLISECONDS));
227 }
228
229 @Test
230 public void testHeuristicFreshnessLifetimeDefaultsProperly() {
231 final TimeValue defaultFreshness = TimeValue.ofSeconds(10);
232 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
233 assertEquals(defaultFreshness, impl.getHeuristicFreshnessLifetime(entry, 0.1f, defaultFreshness));
234 }
235
236 @Test
237 public void testHeuristicFreshnessLifetimeIsNonNegative() {
238 final Header[] headers = new Header[] {
239 new BasicHeader("Date", DateUtils.formatStandardDate(elevenSecondsAgo)),
240 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(oneSecondAgo))
241 };
242
243 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
244 assertTrue(TimeValue.isNonNegative(impl.getHeuristicFreshnessLifetime(entry, 0.1f, TimeValue.ofSeconds(10))));
245 }
246
247 @Test
248 public void testResponseIsFreshIfFreshnessLifetimeExceedsCurrentAge() {
249 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
250 impl = new CacheValidityPolicy() {
251 @Override
252 public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
253 assertSame(entry, e);
254 assertEquals(now, d);
255 return TimeValue.ofSeconds(6);
256 }
257 @Override
258 public TimeValue getFreshnessLifetime(final HttpCacheEntry e) {
259 assertSame(entry, e);
260 return TimeValue.ofSeconds(10);
261 }
262 };
263 assertTrue(impl.isResponseFresh(entry, now));
264 }
265
266 @Test
267 public void testResponseIsNotFreshIfFreshnessLifetimeEqualsCurrentAge() {
268 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
269 impl = new CacheValidityPolicy() {
270 @Override
271 public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
272 assertEquals(now, d);
273 assertSame(entry, e);
274 return TimeValue.ofSeconds(6);
275 }
276 @Override
277 public TimeValue getFreshnessLifetime(final HttpCacheEntry e) {
278 assertSame(entry, e);
279 return TimeValue.ofSeconds(6);
280 }
281 };
282 assertFalse(impl.isResponseFresh(entry, now));
283 }
284
285 @Test
286 public void testResponseIsNotFreshIfCurrentAgeExceedsFreshnessLifetime() {
287 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
288 impl = new CacheValidityPolicy() {
289 @Override
290 public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
291 assertEquals(now, d);
292 assertSame(entry, e);
293 return TimeValue.ofSeconds(10);
294 }
295 @Override
296 public TimeValue getFreshnessLifetime(final HttpCacheEntry e) {
297 assertSame(entry, e);
298 return TimeValue.ofSeconds(6);
299 }
300 };
301 assertFalse(impl.isResponseFresh(entry, now));
302 }
303
304 @Test
305 public void testCacheEntryIsRevalidatableIfHeadersIncludeETag() {
306 final Header[] headers = {
307 new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
308 new BasicHeader("ETag", "somevalue")};
309 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
310 assertTrue(impl.isRevalidatable(entry));
311 }
312
313 @Test
314 public void testCacheEntryIsRevalidatableIfHeadersIncludeLastModifiedDate() {
315 final Header[] headers = {
316 new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
317 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now())) };
318 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
319 assertTrue(impl.isRevalidatable(entry));
320 }
321
322 @Test
323 public void testCacheEntryIsNotRevalidatableIfNoAppropriateHeaders() {
324 final Header[] headers = {
325 new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
326 new BasicHeader("Cache-Control", "public") };
327 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
328 assertFalse(impl.isRevalidatable(entry));
329 }
330
331 @Test
332 public void testMissingContentLengthDoesntInvalidateEntry() {
333 final int contentLength = 128;
334 final Header[] headers = {};
335 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers, HttpTestUtils.getRandomBytes(contentLength));
336 assertTrue(impl.contentLengthHeaderMatchesActualLength(entry));
337 }
338
339 @Test
340 public void testCorrectContentLengthDoesntInvalidateEntry() {
341 final int contentLength = 128;
342 final Header[] headers = { new BasicHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(contentLength)) };
343 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers, HttpTestUtils.getRandomBytes(contentLength));
344 assertTrue(impl.contentLengthHeaderMatchesActualLength(entry));
345 }
346
347 @Test
348 public void testWrongContentLengthInvalidatesEntry() {
349 final int contentLength = 128;
350 final Header[] headers = {new BasicHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(contentLength+1))};
351 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers, HttpTestUtils.getRandomBytes(contentLength));
352 assertFalse(impl.contentLengthHeaderMatchesActualLength(entry));
353 }
354
355 @Test
356 public void testNullResourceInvalidatesEntry() {
357 final int contentLength = 128;
358 final Header[] headers = {new BasicHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(contentLength))};
359 final HttpCacheEntry entry = HttpTestUtils.makeHeadCacheEntry(headers);
360 assertFalse(impl.contentLengthHeaderMatchesActualLength(entry));
361 }
362
363 @Test
364 public void testNegativeAgeHeaderValueReturnsMaxAge() {
365 final Header[] headers = new Header[] { new BasicHeader("Age", "-100") };
366 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
367
368 assertEquals(CacheValidityPolicy.MAX_AGE.toSeconds(), impl.getAgeValue(entry));
369 }
370
371 @Test
372 public void testMalformedAgeHeaderValueReturnsMaxAge() {
373 final Header[] headers = new Header[] { new BasicHeader("Age", "asdf") };
374 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
375
376 assertEquals(CacheValidityPolicy.MAX_AGE.toSeconds(), impl.getAgeValue(entry));
377 }
378
379 @Test
380 public void testMalformedCacheControlMaxAgeHeaderReturnsZero() {
381 final Header[] headers = new Header[] { new BasicHeader("Cache-Control", "max-age=asdf") };
382 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
383
384 assertEquals(0L, impl.getMaxAge(entry));
385 }
386
387 @Test
388 public void testMustRevalidateIsFalseIfDirectiveNotPresent() {
389 final Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") };
390 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
391 assertFalse(impl.mustRevalidate(entry));
392 }
393
394 @Test
395 public void testMustRevalidateIsTrueWhenDirectiveIsPresent() {
396 final Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, must-revalidate") };
397 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
398 assertTrue(impl.mustRevalidate(entry));
399 }
400
401 @Test
402 public void testProxyRevalidateIsFalseIfDirectiveNotPresent() {
403 final Header[] headers = new Header[] { new BasicHeader("Cache-Control","public") };
404 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
405 assertFalse(impl.proxyRevalidate(entry));
406 }
407
408 @Test
409 public void testProxyRevalidateIsTrueWhenDirectiveIsPresent() {
410 final Header[] headers = new Header[] { new BasicHeader("Cache-Control","public, proxy-revalidate") };
411 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
412 assertTrue(impl.proxyRevalidate(entry));
413 }
414
415 @Test
416 public void testMayReturnStaleIfErrorInResponseIsTrueWithinStaleness(){
417 final Header[] headers = new Header[] {
418 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
419 new BasicHeader("Cache-Control", "max-age=5, stale-if-error=15")
420 };
421 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
422 final HttpRequest req = new BasicHttpRequest("GET","/");
423 assertTrue(impl.mayReturnStaleIfError(req, entry, now));
424 }
425
426 @Test
427 public void testMayReturnStaleIfErrorInRequestIsTrueWithinStaleness(){
428 final Header[] headers = new Header[] {
429 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
430 new BasicHeader("Cache-Control", "max-age=5")
431 };
432 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
433 final HttpRequest req = new BasicHttpRequest("GET","/");
434 req.setHeader("Cache-Control","stale-if-error=15");
435 assertTrue(impl.mayReturnStaleIfError(req, entry, now));
436 }
437
438 @Test
439 public void testMayNotReturnStaleIfErrorInResponseAndAfterResponseWindow(){
440 final Header[] headers = new Header[] {
441 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
442 new BasicHeader("Cache-Control", "max-age=5, stale-if-error=1")
443 };
444 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
445 final HttpRequest req = new BasicHttpRequest("GET","/");
446 assertFalse(impl.mayReturnStaleIfError(req, entry, now));
447 }
448
449 @Test
450 public void testMayNotReturnStaleIfErrorInResponseAndAfterRequestWindow(){
451 final Header[] headers = new Header[] {
452 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
453 new BasicHeader("Cache-Control", "max-age=5")
454 };
455 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
456 final HttpRequest req = new BasicHttpRequest("GET","/");
457 req.setHeader("Cache-Control","stale-if-error=1");
458 assertFalse(impl.mayReturnStaleIfError(req, entry, now));
459 }
460
461 @Test
462 public void testMayReturnStaleWhileRevalidatingIsFalseWhenDirectiveIsAbsent() {
463 final Header[] headers = new Header[] { new BasicHeader("Cache-control", "public") };
464 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
465
466 assertFalse(impl.mayReturnStaleWhileRevalidating(entry, now));
467 }
468
469 @Test
470 public void testMayReturnStaleWhileRevalidatingIsTrueWhenWithinStaleness() {
471 final Header[] headers = new Header[] {
472 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
473 new BasicHeader("Cache-Control", "max-age=5, stale-while-revalidate=15")
474 };
475 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
476
477 assertTrue(impl.mayReturnStaleWhileRevalidating(entry, now));
478 }
479
480 @Test
481 public void testMayReturnStaleWhileRevalidatingIsFalseWhenPastStaleness() {
482 final Instant twentyFiveSecondsAgo = now.minusSeconds(25);
483 final Header[] headers = new Header[] {
484 new BasicHeader("Date", DateUtils.formatStandardDate(twentyFiveSecondsAgo)),
485 new BasicHeader("Cache-Control", "max-age=5, stale-while-revalidate=15")
486 };
487 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
488
489 assertFalse(impl.mayReturnStaleWhileRevalidating(entry, now));
490 }
491
492 @Test
493 public void testMayReturnStaleWhileRevalidatingIsFalseWhenDirectiveEmpty() {
494 final Header[] headers = new Header[] {
495 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
496 new BasicHeader("Cache-Control", "max-age=5, stale-while-revalidate=")
497 };
498 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now, headers);
499
500 assertFalse(impl.mayReturnStaleWhileRevalidating(entry, now));
501 }
502 }