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