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 static org.mockito.Mockito.verify;
30  import static org.mockito.Mockito.verifyNoMoreInteractions;
31  import static org.mockito.Mockito.when;
32  
33  import java.net.URI;
34  import java.time.Instant;
35  import java.util.HashMap;
36  import java.util.Map;
37  
38  import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
39  import org.apache.hc.client5.http.cache.HttpCacheEntry;
40  import org.apache.hc.client5.http.utils.DateUtils;
41  import org.apache.hc.core5.concurrent.Cancellable;
42  import org.apache.hc.core5.concurrent.FutureCallback;
43  import org.apache.hc.core5.function.Resolver;
44  import org.apache.hc.core5.http.Header;
45  import org.apache.hc.core5.http.HttpHost;
46  import org.apache.hc.core5.http.HttpRequest;
47  import org.apache.hc.core5.http.HttpResponse;
48  import org.apache.hc.core5.http.HttpStatus;
49  import org.apache.hc.core5.http.message.BasicHeader;
50  import org.apache.hc.core5.http.message.BasicHttpRequest;
51  import org.apache.hc.core5.http.message.BasicHttpResponse;
52  import org.junit.jupiter.api.BeforeEach;
53  import org.junit.jupiter.api.Test;
54  import org.mockito.ArgumentMatchers;
55  import org.mockito.Mock;
56  import org.mockito.Mockito;
57  import org.mockito.MockitoAnnotations;
58  import org.mockito.stubbing.Answer;
59  
60  public class TestDefaultAsyncCacheInvalidator {
61  
62      private DefaultAsyncCacheInvalidator impl;
63      private HttpHost host;
64      @Mock
65      private HttpCacheEntry mockEntry;
66      @Mock
67      private Resolver<URI, String> cacheKeyResolver;
68      @Mock
69      private HttpAsyncCacheStorage mockStorage;
70      @Mock
71      private FutureCallback<Boolean> operationCallback;
72      @Mock
73      private Cancellable cancellable;
74  
75      private Instant now;
76      private Instant tenSecondsAgo;
77  
78      @BeforeEach
79      public void setUp() {
80          MockitoAnnotations.openMocks(this);
81          now = Instant.now();
82          tenSecondsAgo = now.minusSeconds(10);
83  
84          when(cacheKeyResolver.resolve(ArgumentMatchers.any())).thenAnswer((Answer<String>) invocation -> {
85              final URI uri = invocation.getArgument(0);
86              return HttpCacheSupport.normalize(uri).toASCIIString();
87          });
88  
89          host = new HttpHost("foo.example.com");
90          impl = new DefaultAsyncCacheInvalidator();
91      }
92  
93      // Tests
94      @Test
95      public void testInvalidatesRequestsThatArentGETorHEAD() throws Exception {
96          final HttpRequest request = new BasicHttpRequest("POST","/path");
97          final String key = "http://foo.example.com:80/path";
98  
99          final Map<String,String> variantMap = new HashMap<>();
100         cacheEntryHasVariantMap(variantMap);
101         cacheReturnsEntryForUri(key, mockEntry);
102 
103         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
104 
105         verify(mockEntry).getVariantMap();
106         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
107         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
108     }
109 
110     @Test
111     public void testInvalidatesUrisInContentLocationHeadersOnPUTs() throws Exception {
112         final HttpRequest request = new BasicHttpRequest("PUT","/");
113         request.setHeader("Content-Length","128");
114 
115         final String contentLocation = "http://foo.example.com/content";
116         request.setHeader("Content-Location", contentLocation);
117 
118         final URI uri = new URI("http://foo.example.com:80/");
119         final String key = uri.toASCIIString();
120         cacheEntryHasVariantMap(new HashMap<>());
121 
122         cacheReturnsEntryForUri(key, mockEntry);
123 
124         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
125 
126         verify(mockEntry).getVariantMap();
127         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
128         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
129         verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.any());
130     }
131 
132     @Test
133     public void testInvalidatesUrisInLocationHeadersOnPUTs() throws Exception {
134         final HttpRequest request = new BasicHttpRequest("PUT","/");
135         request.setHeader("Content-Length","128");
136 
137         final String contentLocation = "http://foo.example.com/content";
138         request.setHeader("Location",contentLocation);
139 
140         final URI uri = new URI("http://foo.example.com:80/");
141         final String key = uri.toASCIIString();
142         cacheEntryHasVariantMap(new HashMap<>());
143 
144         cacheReturnsEntryForUri(key, mockEntry);
145 
146         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
147 
148         verify(mockEntry).getVariantMap();
149         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
150         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
151         verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.any());
152     }
153 
154     @Test
155     public void testInvalidatesRelativeUrisInContentLocationHeadersOnPUTs() throws Exception {
156         final HttpRequest request = new BasicHttpRequest("PUT","/");
157         request.setHeader("Content-Length","128");
158 
159         final String relativePath = "/content";
160         request.setHeader("Content-Location",relativePath);
161 
162         final URI uri = new URI("http://foo.example.com:80/");
163         final String key = uri.toASCIIString();
164         cacheEntryHasVariantMap(new HashMap<>());
165 
166         cacheReturnsEntryForUri(key, mockEntry);
167 
168         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
169 
170         verify(mockEntry).getVariantMap();
171         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
172         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
173         verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.any());
174     }
175 
176     @Test
177     public void testDoesNotInvalidateUrisInContentLocationHeadersOnPUTsToDifferentHosts() throws Exception {
178         final HttpRequest request = new BasicHttpRequest("PUT","/");
179         request.setHeader("Content-Length","128");
180 
181         final String contentLocation = "http://bar.example.com/content";
182         request.setHeader("Content-Location",contentLocation);
183 
184         final URI uri = new URI("http://foo.example.com:80/");
185         final String key = uri.toASCIIString();
186         cacheEntryHasVariantMap(new HashMap<>());
187 
188         cacheReturnsEntryForUri(key, mockEntry);
189 
190         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
191 
192         verify(mockEntry).getVariantMap();
193         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
194         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
195     }
196 
197     @Test
198     public void testDoesNotInvalidateGETRequest() throws Exception {
199         final HttpRequest request = new BasicHttpRequest("GET","/");
200         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
201 
202         verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any());
203         verifyNoMoreInteractions(mockStorage);
204     }
205 
206     @Test
207     public void testDoesNotInvalidateHEADRequest() throws Exception {
208         final HttpRequest request = new BasicHttpRequest("HEAD","/");
209         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
210 
211         verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any());
212         verifyNoMoreInteractions(mockStorage);
213     }
214 
215     @Test
216     public void testInvalidatesHEADCacheEntryIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
217         final URI uri = new URI("http://foo.example.com:80/");
218         final String key = uri.toASCIIString();
219         final HttpRequest request = new BasicHttpRequest("GET", uri);
220 
221         cacheEntryisForMethod("HEAD");
222         cacheEntryHasVariantMap(new HashMap<>());
223         cacheReturnsEntryForUri(key, mockEntry);
224 
225         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
226 
227         verify(mockEntry).getRequestMethod();
228         verify(mockEntry).getVariantMap();
229         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
230         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
231     }
232 
233     @Test
234     public void testInvalidatesVariantHEADCacheEntriesIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
235         final URI uri = new URI("http://foo.example.com:80/");
236         final String key = uri.toASCIIString();
237         final HttpRequest request = new BasicHttpRequest("GET", uri);
238         final String theVariantKey = "{Accept-Encoding=gzip%2Cdeflate&User-Agent=Apache-HttpClient}";
239         final String theVariantURI = "{Accept-Encoding=gzip%2Cdeflate&User-Agent=Apache-HttpClient}http://foo.example.com:80/";
240         final Map<String, String> variants = HttpTestUtils.makeDefaultVariantMap(theVariantKey, theVariantURI);
241 
242         cacheEntryisForMethod("HEAD");
243         cacheEntryHasVariantMap(variants);
244         cacheReturnsEntryForUri(key, mockEntry);
245 
246         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
247 
248         verify(mockEntry).getRequestMethod();
249         verify(mockEntry).getVariantMap();
250         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
251         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
252         verify(mockStorage).removeEntry(ArgumentMatchers.eq(theVariantURI), ArgumentMatchers.any());
253     }
254 
255     @Test
256     public void testDoesNotInvalidateHEADCacheEntry() throws Exception {
257         final URI uri = new URI("http://foo.example.com:80/");
258         final String key = uri.toASCIIString();
259         final HttpRequest request = new BasicHttpRequest("HEAD", uri);
260 
261         cacheReturnsEntryForUri(key, mockEntry);
262 
263         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
264 
265         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
266         verifyNoMoreInteractions(mockStorage);
267     }
268 
269     @Test
270     public void testDoesNotInvalidateHEADCacheEntryIfSubsequentHEADRequestsAreMadeToTheSameURI() throws Exception {
271         final URI uri = new URI("http://foo.example.com:80/");
272         final String key = uri.toASCIIString();
273         final HttpRequest request = new BasicHttpRequest("HEAD", uri);
274 
275         cacheReturnsEntryForUri(key, mockEntry);
276 
277         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
278 
279         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
280         verifyNoMoreInteractions(mockStorage);
281     }
282 
283     @Test
284     public void testDoesNotInvalidateGETCacheEntryIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
285         final URI uri = new URI("http://foo.example.com:80/");
286         final String key = uri.toASCIIString();
287         final HttpRequest request = new BasicHttpRequest("GET", uri);
288 
289         cacheEntryisForMethod("GET");
290         cacheReturnsEntryForUri(key, mockEntry);
291 
292         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
293 
294         verify(mockEntry).getRequestMethod();
295         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
296         verifyNoMoreInteractions(mockStorage);
297     }
298 
299     @Test
300     public void testDoesNotInvalidateRequestsWithClientCacheControlHeaders() throws Exception {
301         final HttpRequest request = new BasicHttpRequest("GET","/");
302         request.setHeader("Cache-Control","no-cache");
303 
304         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
305 
306         verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any());
307         verifyNoMoreInteractions(mockStorage);
308     }
309 
310     @Test
311     public void testDoesNotInvalidateRequestsWithClientPragmaHeaders() throws Exception {
312         final HttpRequest request = new BasicHttpRequest("GET","/");
313         request.setHeader("Pragma","no-cache");
314 
315         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
316 
317         verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any());
318         verifyNoMoreInteractions(mockStorage);
319     }
320 
321     @Test
322     public void testVariantURIsAreFlushedAlso() throws Exception {
323         final HttpRequest request = new BasicHttpRequest("POST","/");
324         final URI uri = new URI("http://foo.example.com:80/");
325         final String key = uri.toASCIIString();
326         final String variantUri = "theVariantURI";
327         final Map<String,String> mapOfURIs = HttpTestUtils.makeDefaultVariantMap(variantUri, variantUri);
328 
329         cacheReturnsEntryForUri(key, mockEntry);
330         cacheEntryHasVariantMap(mapOfURIs);
331 
332         impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback);
333 
334         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
335         verify(mockEntry).getVariantMap();
336         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
337         verify(mockStorage).removeEntry(ArgumentMatchers.eq(variantUri), ArgumentMatchers.any());
338     }
339 
340     @Test
341     public void doesNotFlushForResponsesWithoutContentLocation() throws Exception {
342         final HttpRequest request = new BasicHttpRequest("POST","/");
343         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
344         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
345 
346         verifyNoMoreInteractions(mockStorage);
347     }
348 
349     @Test
350     public void flushesEntryIfFresherAndSpecifiedByContentLocation() throws Exception {
351         final HttpRequest request = new BasicHttpRequest("GET", "/");
352         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
353         response.setHeader("ETag","\"new-etag\"");
354         response.setHeader("Date", DateUtils.formatStandardDate(now));
355         final String key = "http://foo.example.com:80/bar";
356         response.setHeader("Content-Location", key);
357 
358         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
359            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
360            new BasicHeader("ETag", "\"old-etag\"")
361         });
362 
363         cacheReturnsEntryForUri(key, entry);
364 
365         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
366 
367         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
368         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
369     }
370 
371     @Test
372     public void flushesEntryIfFresherAndSpecifiedByLocation() throws Exception {
373         final HttpRequest request = new BasicHttpRequest("GET", "/");
374         final HttpResponse response = new BasicHttpResponse(201);
375         response.setHeader("ETag","\"new-etag\"");
376         response.setHeader("Date", DateUtils.formatStandardDate(now));
377         final String key = "http://foo.example.com:80/bar";
378         response.setHeader("Location", key);
379 
380         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
381            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
382            new BasicHeader("ETag", "\"old-etag\"")
383         });
384 
385         cacheReturnsEntryForUri(key, entry);
386 
387         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
388 
389         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
390         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
391     }
392 
393     @Test
394     public void doesNotFlushEntryForUnsuccessfulResponse() throws Exception {
395         final HttpRequest request = new BasicHttpRequest("GET", "/");
396         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_BAD_REQUEST, "Bad Request");
397         response.setHeader("ETag","\"new-etag\"");
398         response.setHeader("Date", DateUtils.formatStandardDate(now));
399         final String key = "http://foo.example.com:80/bar";
400         response.setHeader("Content-Location", key);
401 
402         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
403 
404         verifyNoMoreInteractions(mockStorage);
405     }
406 
407     @Test
408     public void flushesEntryIfFresherAndSpecifiedByNonCanonicalContentLocation() throws Exception {
409         final HttpRequest request = new BasicHttpRequest("GET", "/");
410         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
411         response.setHeader("ETag","\"new-etag\"");
412         response.setHeader("Date", DateUtils.formatStandardDate(now));
413         final String key = "http://foo.example.com:80/bar";
414         response.setHeader("Content-Location", "http://foo.example.com/bar");
415 
416         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
417            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
418            new BasicHeader("ETag", "\"old-etag\"")
419         });
420 
421         cacheReturnsEntryForUri(key, entry);
422 
423         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
424 
425         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
426         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
427     }
428 
429     @Test
430     public void flushesEntryIfFresherAndSpecifiedByRelativeContentLocation() throws Exception {
431         final HttpRequest request = new BasicHttpRequest("GET", "/");
432         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
433         response.setHeader("ETag","\"new-etag\"");
434         response.setHeader("Date", DateUtils.formatStandardDate(now));
435         final String key = "http://foo.example.com:80/bar";
436         response.setHeader("Content-Location", "/bar");
437 
438         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
439            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
440            new BasicHeader("ETag", "\"old-etag\"")
441         });
442 
443         cacheReturnsEntryForUri(key, entry);
444 
445         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
446 
447         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
448         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
449     }
450 
451     @Test
452     public void doesNotFlushEntryIfContentLocationFromDifferentHost() throws Exception {
453         final HttpRequest request = new BasicHttpRequest("GET", "/");
454         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
455         response.setHeader("ETag","\"new-etag\"");
456         response.setHeader("Date", DateUtils.formatStandardDate(now));
457         final String key = "http://baz.example.com:80/bar";
458         response.setHeader("Content-Location", key);
459 
460         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
461            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
462            new BasicHeader("ETag", "\"old-etag\"")
463         });
464 
465         cacheReturnsEntryForUri(key, entry);
466 
467         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
468 
469         verifyNoMoreInteractions(mockStorage);
470     }
471 
472     @Test
473     public void doesNotFlushEntrySpecifiedByContentLocationIfEtagsMatch() throws Exception {
474         final HttpRequest request = new BasicHttpRequest("GET", "/");
475         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
476         response.setHeader("ETag","\"same-etag\"");
477         response.setHeader("Date", DateUtils.formatStandardDate(now));
478         final String key = "http://foo.example.com:80/bar";
479         response.setHeader("Content-Location", key);
480 
481         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
482            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
483            new BasicHeader("ETag", "\"same-etag\"")
484         });
485 
486         cacheReturnsEntryForUri(key, entry);
487         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
488 
489         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
490         verifyNoMoreInteractions(mockStorage);
491     }
492 
493     @Test
494     public void doesNotFlushEntrySpecifiedByContentLocationIfOlder() throws Exception {
495         final HttpRequest request = new BasicHttpRequest("GET", "/");
496         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
497         response.setHeader("ETag","\"new-etag\"");
498         response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
499         final String key = "http://foo.example.com:80/bar";
500         response.setHeader("Content-Location", key);
501 
502         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
503            new BasicHeader("Date", DateUtils.formatStandardDate(now)),
504            new BasicHeader("ETag", "\"old-etag\"")
505         });
506 
507         cacheReturnsEntryForUri(key, entry);
508 
509         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
510 
511         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
512         verifyNoMoreInteractions(mockStorage);
513     }
514 
515     @Test
516     public void doesNotFlushEntryIfNotInCache() throws Exception {
517         final HttpRequest request = new BasicHttpRequest("GET", "/");
518         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
519         response.setHeader("ETag","\"new-etag\"");
520         response.setHeader("Date", DateUtils.formatStandardDate(now));
521         final String key = "http://foo.example.com:80/bar";
522         response.setHeader("Content-Location", key);
523 
524         cacheReturnsEntryForUri(key, null);
525 
526         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
527 
528         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
529         verifyNoMoreInteractions(mockStorage);
530     }
531 
532     @Test
533     public void doesNotFlushEntrySpecifiedByContentLocationIfResponseHasNoEtag() throws Exception {
534         final HttpRequest request = new BasicHttpRequest("GET", "/");
535         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
536         response.removeHeaders("ETag");
537         response.setHeader("Date", DateUtils.formatStandardDate(now));
538         final String key = "http://foo.example.com:80/bar";
539         response.setHeader("Content-Location", key);
540 
541         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
542            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
543            new BasicHeader("ETag", "\"old-etag\"")
544         });
545 
546         cacheReturnsEntryForUri(key, entry);
547 
548         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
549 
550         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
551         verifyNoMoreInteractions(mockStorage);
552     }
553 
554     @Test
555     public void doesNotFlushEntrySpecifiedByContentLocationIfEntryHasNoEtag() throws Exception {
556         final HttpRequest request = new BasicHttpRequest("GET", "/");
557         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
558         response.setHeader("ETag", "\"some-etag\"");
559         response.setHeader("Date", DateUtils.formatStandardDate(now));
560         final String key = "http://foo.example.com:80/bar";
561         response.setHeader("Content-Location", key);
562 
563         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
564            new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
565         });
566 
567         cacheReturnsEntryForUri(key, entry);
568 
569         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
570 
571         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
572         verifyNoMoreInteractions(mockStorage);
573     }
574 
575     @Test
576     public void flushesEntrySpecifiedByContentLocationIfResponseHasNoDate() throws Exception {
577         final HttpRequest request = new BasicHttpRequest("GET", "/");
578         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
579         response.setHeader("ETag", "\"new-etag\"");
580         response.removeHeaders("Date");
581         final String key = "http://foo.example.com:80/bar";
582         response.setHeader("Content-Location", key);
583 
584         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
585                 new BasicHeader("ETag", "\"old-etag\""),
586                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
587         });
588 
589         cacheReturnsEntryForUri(key, entry);
590 
591         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
592 
593         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
594         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
595         verifyNoMoreInteractions(mockStorage);
596     }
597 
598     @Test
599     public void flushesEntrySpecifiedByContentLocationIfEntryHasNoDate() throws Exception {
600         final HttpRequest request = new BasicHttpRequest("GET", "/");
601         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
602         response.setHeader("ETag","\"new-etag\"");
603         response.setHeader("Date", DateUtils.formatStandardDate(now));
604         final String key = "http://foo.example.com:80/bar";
605         response.setHeader("Content-Location", key);
606 
607         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
608            new BasicHeader("ETag", "\"old-etag\"")
609         });
610 
611         cacheReturnsEntryForUri(key, entry);
612 
613         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
614 
615         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
616         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
617         verifyNoMoreInteractions(mockStorage);
618     }
619 
620     @Test
621     public void flushesEntrySpecifiedByContentLocationIfResponseHasMalformedDate() throws Exception {
622         final HttpRequest request = new BasicHttpRequest("GET", "/");
623         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
624         response.setHeader("ETag","\"new-etag\"");
625         response.setHeader("Date", "blarg");
626         final String key = "http://foo.example.com:80/bar";
627         response.setHeader("Content-Location", key);
628 
629         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
630                 new BasicHeader("ETag", "\"old-etag\""),
631                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
632         });
633 
634         cacheReturnsEntryForUri(key, entry);
635 
636         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
637 
638         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
639         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
640         verifyNoMoreInteractions(mockStorage);
641     }
642 
643     @Test
644     public void flushesEntrySpecifiedByContentLocationIfEntryHasMalformedDate() throws Exception {
645         final HttpRequest request = new BasicHttpRequest("GET", "/");
646         final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
647         response.setHeader("ETag","\"new-etag\"");
648         response.setHeader("Date", DateUtils.formatStandardDate(now));
649         final String key = "http://foo.example.com:80/bar";
650         response.setHeader("Content-Location", key);
651 
652         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
653                 new BasicHeader("ETag", "\"old-etag\""),
654                 new BasicHeader("Date", "foo")
655         });
656 
657         cacheReturnsEntryForUri(key, entry);
658 
659         impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
660 
661         verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
662         verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any());
663         verifyNoMoreInteractions(mockStorage);
664     }
665 
666 
667     // Expectations
668     private void cacheEntryHasVariantMap(final Map<String,String> variantMap) {
669         when(mockEntry.getVariantMap()).thenReturn(variantMap);
670     }
671 
672     private void cacheReturnsEntryForUri(final String key, final HttpCacheEntry cacheEntry) {
673         Mockito.when(mockStorage.getEntry(
674                 ArgumentMatchers.eq(key),
675                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
676                     final FutureCallback<HttpCacheEntry> callback = invocation.getArgument(1);
677                     callback.completed(cacheEntry);
678                     return cancellable;
679                 });
680     }
681 
682     private void cacheEntryisForMethod(final String httpMethod) {
683         when(mockEntry.getRequestMethod()).thenReturn(httpMethod);
684     }
685 }