View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.time.Instant;
30  
31  import org.apache.hc.client5.http.cache.HttpCacheEntry;
32  import org.apache.hc.client5.http.utils.DateUtils;
33  import org.apache.hc.core5.http.Header;
34  import org.apache.hc.core5.http.HttpHost;
35  import org.apache.hc.core5.http.HttpRequest;
36  import org.apache.hc.core5.http.message.BasicHeader;
37  import org.apache.hc.core5.http.message.BasicHttpRequest;
38  import org.apache.hc.core5.util.TimeValue;
39  import org.junit.jupiter.api.Assertions;
40  import org.junit.jupiter.api.BeforeEach;
41  import org.junit.jupiter.api.Test;
42  
43  public class TestCachedResponseSuitabilityChecker {
44  
45      private Instant now;
46      private Instant elevenSecondsAgo;
47      private Instant tenSecondsAgo;
48      private Instant nineSecondsAgo;
49  
50      private HttpHost host;
51      private HttpRequest request;
52      private HttpCacheEntry entry;
53      private CachedResponseSuitabilityChecker impl;
54  
55      @BeforeEach
56      public void setUp() {
57          now = Instant.now();
58          elevenSecondsAgo = now.minusSeconds(11);
59          tenSecondsAgo = now.minusSeconds(10);
60          nineSecondsAgo = now.minusSeconds(9);
61  
62          host = new HttpHost("foo.example.com");
63          request = new BasicHttpRequest("GET", "/foo");
64          entry = HttpTestUtils.makeCacheEntry();
65  
66          impl = new CachedResponseSuitabilityChecker(CacheConfig.DEFAULT);
67      }
68  
69      private HttpCacheEntry getEntry(final Header[] headers) {
70          return HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo, headers);
71      }
72  
73      @Test
74      public void testNotSuitableIfContentLengthHeaderIsWrong() {
75          final Header[] headers = {
76                  new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
77                  new BasicHeader("Cache-Control", "max-age=3600"),
78                  new BasicHeader("Content-Length","1")
79          };
80          entry = getEntry(headers);
81          Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
82      }
83  
84      @Test
85      public void testSuitableIfCacheEntryIsFresh() {
86          final Header[] headers = {
87                  new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
88                  new BasicHeader("Cache-Control", "max-age=3600"),
89                  new BasicHeader("Content-Length","128")
90          };
91          entry = getEntry(headers);
92          Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
93      }
94  
95      @Test
96      public void testNotSuitableIfCacheEntryIsNotFresh() {
97          final Header[] headers = {
98                  new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
99                  new BasicHeader("Cache-Control", "max-age=5"),
100                 new BasicHeader("Content-Length","128")
101         };
102         entry = getEntry(headers);
103         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
104     }
105 
106     @Test
107     public void testNotSuitableIfRequestHasNoCache() {
108         request.addHeader("Cache-Control", "no-cache");
109         final Header[] headers = {
110                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
111                 new BasicHeader("Cache-Control", "max-age=3600"),
112                 new BasicHeader("Content-Length","128")
113         };
114         entry = getEntry(headers);
115         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
116     }
117 
118     @Test
119     public void testNotSuitableIfAgeExceedsRequestMaxAge() {
120         request.addHeader("Cache-Control", "max-age=10");
121         final Header[] headers = {
122                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
123                 new BasicHeader("Cache-Control", "max-age=3600"),
124                 new BasicHeader("Content-Length","128")
125         };
126         entry = getEntry(headers);
127         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
128     }
129 
130     @Test
131     public void testSuitableIfFreshAndAgeIsUnderRequestMaxAge() {
132         request.addHeader("Cache-Control", "max-age=15");
133         final Header[] headers = {
134                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
135                 new BasicHeader("Cache-Control", "max-age=3600"),
136                 new BasicHeader("Content-Length","128")
137         };
138         entry = getEntry(headers);
139         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
140     }
141 
142     @Test
143     public void testSuitableIfFreshAndFreshnessLifetimeGreaterThanRequestMinFresh() {
144         request.addHeader("Cache-Control", "min-fresh=10");
145         final Header[] headers = {
146                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
147                 new BasicHeader("Cache-Control", "max-age=3600"),
148                 new BasicHeader("Content-Length","128")
149         };
150         entry = getEntry(headers);
151         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
152     }
153 
154     @Test
155     public void testNotSuitableIfFreshnessLifetimeLessThanRequestMinFresh() {
156         request.addHeader("Cache-Control", "min-fresh=10");
157         final Header[] headers = {
158                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
159                 new BasicHeader("Cache-Control", "max-age=15"),
160                 new BasicHeader("Content-Length","128")
161         };
162         entry = getEntry(headers);
163         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
164     }
165 
166     @Test
167     public void testSuitableEvenIfStaleButPermittedByRequestMaxStale() {
168         request.addHeader("Cache-Control", "max-stale=10");
169         final Header[] headers = {
170                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
171                 new BasicHeader("Cache-Control", "max-age=5"),
172                 new BasicHeader("Content-Length","128")
173         };
174         entry = getEntry(headers);
175         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
176     }
177 
178     @Test
179     public void testNotSuitableIfStaleButTooStaleForRequestMaxStale() {
180         request.addHeader("Cache-Control", "max-stale=2");
181         final Header[] headers = {
182                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
183                 new BasicHeader("Cache-Control", "max-age=5"),
184                 new BasicHeader("Content-Length","128")
185         };
186         entry = getEntry(headers);
187         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
188     }
189 
190 
191     @Test
192     public void testMalformedCacheControlMaxAgeRequestHeaderCausesUnsuitableEntry() {
193         request.addHeader(new BasicHeader("Cache-Control", "max-age=foo"));
194         final Header[] headers = {
195                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
196                 new BasicHeader("Cache-Control", "max-age=3600"),
197                 new BasicHeader("Content-Length","128")
198         };
199         entry = getEntry(headers);
200         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
201     }
202 
203     @Test
204     public void testMalformedCacheControlMinFreshRequestHeaderCausesUnsuitableEntry() {
205         request.addHeader(new BasicHeader("Cache-Control", "min-fresh=foo"));
206         final Header[] headers = {
207                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
208                 new BasicHeader("Cache-Control", "max-age=3600"),
209                 new BasicHeader("Content-Length","128")
210         };
211         entry = getEntry(headers);
212         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
213     }
214 
215     @Test
216     public void testSuitableIfCacheEntryIsHeuristicallyFreshEnough() {
217         final Instant oneSecondAgo = now.minusSeconds(1);
218         final Instant twentyOneSecondsAgo = now.minusSeconds(21);
219 
220         final Header[] headers = {
221                 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
222                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(twentyOneSecondsAgo)),
223                 new BasicHeader("Content-Length", "128")
224         };
225 
226         entry = HttpTestUtils.makeCacheEntry(oneSecondAgo, oneSecondAgo, headers);
227 
228         final CacheConfig config = CacheConfig.custom()
229             .setHeuristicCachingEnabled(true)
230             .setHeuristicCoefficient(0.1f).build();
231         impl = new CachedResponseSuitabilityChecker(config);
232 
233         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
234     }
235 
236     @Test
237     public void testSuitableIfCacheEntryIsHeuristicallyFreshEnoughByDefault() {
238         final Header[] headers = {
239                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
240                 new BasicHeader("Content-Length", "128")
241         };
242 
243         entry = getEntry(headers);
244 
245         final CacheConfig config = CacheConfig.custom()
246             .setHeuristicCachingEnabled(true)
247             .setHeuristicDefaultLifetime(TimeValue.ofSeconds(20L))
248             .build();
249         impl = new CachedResponseSuitabilityChecker(config);
250 
251         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
252     }
253 
254     @Test
255     public void testSuitableIfRequestMethodisHEAD() {
256         final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
257         final Header[] headers = {
258                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
259                 new BasicHeader("Cache-Control", "max-age=3600"),
260                 new BasicHeader("Content-Length","128")
261         };
262         entry = getEntry(headers);
263 
264         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, headRequest, entry, now));
265     }
266 
267     @Test
268     public void testNotSuitableIfRequestMethodIsGETAndEntryResourceIsNull() {
269         final Header[] headers = {
270                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
271                 new BasicHeader("Cache-Control", "max-age=3600"),
272                 new BasicHeader("Content-Length","128")
273         };
274         entry = HttpTestUtils.makeHeadCacheEntry(headers);
275 
276         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
277     }
278 
279     @Test
280     public void testNotSuitableForGETIfEntryDoesNotSpecifyARequestMethodOrEntity() {
281         impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
282         final Header[] headers = {
283                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
284                 new BasicHeader("Cache-Control", "max-age=3600"),
285                 new BasicHeader("Content-Length","128")
286         };
287         entry = HttpTestUtils.makeCacheEntryWithNoRequestMethodOrEntity(headers);
288 
289         Assertions.assertFalse(impl.canCachedResponseBeUsed(host, request, entry, now));
290     }
291 
292     @Test
293     public void testSuitableForGETIfEntryDoesNotSpecifyARequestMethodButContainsEntity() {
294         impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
295         final Header[] headers = {
296                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
297                 new BasicHeader("Cache-Control", "max-age=3600"),
298                 new BasicHeader("Content-Length","128")
299         };
300         entry = HttpTestUtils.makeCacheEntryWithNoRequestMethod(headers);
301 
302         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
303     }
304 
305     @Test
306     public void testSuitableForGETIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethodButContains204Response() {
307         impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
308         final Header[] headers = {
309                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
310                 new BasicHeader("Cache-Control", "max-age=3600")
311         };
312         entry = HttpTestUtils.make204CacheEntryWithNoRequestMethod(headers);
313 
314         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, request, entry, now));
315     }
316 
317     @Test
318     public void testSuitableForHEADIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethod() {
319         final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
320         impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
321         final Header[] headers = {
322                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
323                 new BasicHeader("Cache-Control", "max-age=3600"),
324                 new BasicHeader("Content-Length","128")
325         };
326         entry = HttpTestUtils.makeHeadCacheEntryWithNoRequestMethod(headers);
327 
328         Assertions.assertTrue(impl.canCachedResponseBeUsed(host, headRequest, entry, now));
329     }
330 }