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.when;
31  
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.HashMap;
35  import java.util.Map;
36  import java.util.concurrent.atomic.AtomicInteger;
37  
38  import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
39  import org.apache.hc.client5.http.cache.HttpCacheEntry;
40  import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
41  import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
42  import org.apache.hc.client5.http.cache.ResourceIOException;
43  import org.apache.hc.core5.concurrent.Cancellable;
44  import org.apache.hc.core5.concurrent.FutureCallback;
45  import org.hamcrest.CoreMatchers;
46  import org.junit.Assert;
47  import org.junit.Before;
48  import org.junit.Test;
49  import org.junit.runner.RunWith;
50  import org.mockito.Answers;
51  import org.mockito.ArgumentCaptor;
52  import org.mockito.ArgumentMatchers;
53  import org.mockito.Mock;
54  import org.mockito.Mockito;
55  import org.mockito.invocation.InvocationOnMock;
56  import org.mockito.junit.MockitoJUnitRunner;
57  import org.mockito.stubbing.Answer;
58  
59  @RunWith(MockitoJUnitRunner.class)
60  public class TestAbstractSerializingAsyncCacheStorage {
61  
62      @Mock
63      private Cancellable cancellable;
64      @Mock
65      private FutureCallback<Boolean> operationCallback;
66      @Mock
67      private FutureCallback<HttpCacheEntry> cacheEntryCallback;
68      @Mock
69      private FutureCallback<Map<String, HttpCacheEntry>> bulkCacheEntryCallback;
70  
71      private AbstractBinaryAsyncCacheStorage<String> impl;
72  
73      public static byte[] serialize(final String key, final HttpCacheEntry value) throws ResourceIOException {
74          return ByteArrayCacheEntrySerializer.INSTANCE.serialize(new HttpCacheStorageEntry(key, value));
75      }
76  
77      @Before
78      @SuppressWarnings("unchecked")
79      public void setUp() {
80          impl = Mockito.mock(AbstractBinaryAsyncCacheStorage.class,
81                  Mockito.withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS).useConstructor(3));
82      }
83  
84      @Test
85      public void testCachePut() throws Exception {
86          final String key = "foo";
87          final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
88  
89          Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
90          Mockito.when(impl.store(
91                  ArgumentMatchers.eq("bar"),
92                  ArgumentMatchers.<byte[]>any(),
93                  ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
94  
95              @Override
96              public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
97                  final FutureCallback<Boolean> callback = invocation.getArgument(2);
98                  callback.completed(true);
99                  return cancellable;
100             }
101 
102         });
103 
104         impl.putEntry(key, value, operationCallback);
105 
106         final ArgumentCaptor<byte[]> argumentCaptor = ArgumentCaptor.forClass(byte[].class);
107         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), argumentCaptor.capture(), ArgumentMatchers.<FutureCallback<Boolean>>any());
108         Assert.assertArrayEquals(serialize(key, value), argumentCaptor.getValue());
109         Mockito.verify(operationCallback).completed(Boolean.TRUE);
110     }
111 
112     @Test
113     public void testCacheGetNullEntry() throws Exception {
114         final String key = "foo";
115 
116         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
117         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any())).thenAnswer(new Answer<Cancellable>() {
118 
119             @Override
120             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
121                 final FutureCallback<byte[]> callback = invocation.getArgument(1);
122                 callback.completed(null);
123                 return cancellable;
124             }
125 
126         });
127 
128         impl.getEntry(key, cacheEntryCallback);
129         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
130         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
131         Assert.assertThat(argumentCaptor.getValue(), CoreMatchers.nullValue());
132         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any());
133     }
134 
135     @Test
136     public void testCacheGet() throws Exception {
137         final String key = "foo";
138         final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
139 
140         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
141         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any())).thenAnswer(new Answer<Cancellable>() {
142 
143             @Override
144             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
145                 final FutureCallback<byte[]> callback = invocation.getArgument(1);
146                 callback.completed(serialize(key, value));
147                 return cancellable;
148             }
149 
150         });
151 
152         impl.getEntry(key, cacheEntryCallback);
153         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
154         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
155         final HttpCacheEntry resultingEntry = argumentCaptor.getValue();
156         Assert.assertThat(resultingEntry, HttpCacheEntryMatcher.equivalent(value));
157         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any());
158     }
159 
160     @Test
161     public void testCacheGetKeyMismatch() throws Exception {
162         final String key = "foo";
163         final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
164         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
165         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any())).thenAnswer(new Answer<Cancellable>() {
166 
167             @Override
168             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
169                 final FutureCallback<byte[]> callback = invocation.getArgument(1);
170                 callback.completed(serialize("not-foo", value));
171                 return cancellable;
172             }
173 
174         });
175 
176         impl.getEntry(key, cacheEntryCallback);
177         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
178         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
179         Assert.assertThat(argumentCaptor.getValue(), CoreMatchers.nullValue());
180         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<byte[]>>any());
181     }
182 
183     @Test
184     public void testCacheRemove()  throws Exception{
185         final String key = "foo";
186 
187         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
188         Mockito.when(impl.delete(
189                 ArgumentMatchers.eq("bar"),
190                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
191 
192             @Override
193             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
194                 final FutureCallback<Boolean> callback = invocation.getArgument(1);
195                 callback.completed(true);
196                 return cancellable;
197             }
198 
199         });
200         impl.removeEntry(key, operationCallback);
201 
202         Mockito.verify(impl).delete("bar", operationCallback);
203         Mockito.verify(operationCallback).completed(Boolean.TRUE);
204     }
205 
206     @Test
207     public void testCacheUpdateNullEntry() throws Exception {
208         final String key = "foo";
209         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
210 
211         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
212         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any())).thenAnswer(new Answer<Cancellable>() {
213 
214             @Override
215             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
216                 final FutureCallback<byte[]> callback = invocation.getArgument(1);
217                 callback.completed(null);
218                 return cancellable;
219             }
220 
221         });
222         Mockito.when(impl.store(
223                 ArgumentMatchers.eq("bar"),
224                 ArgumentMatchers.<byte[]>any(),
225                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
226 
227             @Override
228             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
229                 final FutureCallback<Boolean> callback = invocation.getArgument(2);
230                 callback.completed(true);
231                 return cancellable;
232             }
233 
234         });
235 
236         impl.updateEntry(key, new HttpCacheCASOperation() {
237 
238             @Override
239             public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
240                 Assert.assertThat(existing, CoreMatchers.nullValue());
241                 return updatedValue;
242             }
243 
244         }, operationCallback);
245 
246         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any());
247         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
248         Mockito.verify(operationCallback).completed(Boolean.TRUE);
249     }
250 
251     @Test
252     public void testCacheCASUpdate() throws Exception {
253         final String key = "foo";
254         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
255         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
256 
257         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
258         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any())).thenAnswer(new Answer<Cancellable>() {
259 
260             @Override
261             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
262                 final FutureCallback<String> callback = invocation.getArgument(1);
263                 callback.completed("stuff");
264                 return cancellable;
265             }
266 
267         });
268         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
269         Mockito.when(impl.updateCAS(
270                 ArgumentMatchers.eq("bar"),
271                 ArgumentMatchers.eq("stuff"),
272                 ArgumentMatchers.<byte[]>any(),
273                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
274 
275             @Override
276             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
277                 final FutureCallback<Boolean> callback = invocation.getArgument(3);
278                 callback.completed(true);
279                 return cancellable;
280             }
281 
282         });
283 
284         impl.updateEntry(key, new HttpCacheCASOperation() {
285 
286             @Override
287             public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
288                 return updatedValue;
289             }
290 
291         }, operationCallback);
292 
293         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any());
294         Mockito.verify(impl).getStorageObject("stuff");
295         Mockito.verify(impl).updateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
296         Mockito.verify(operationCallback).completed(Boolean.TRUE);
297     }
298 
299     @Test
300     public void testCacheCASUpdateKeyMismatch() throws Exception {
301         final String key = "foo";
302         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
303         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
304 
305         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
306         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any())).thenAnswer(
307                 new Answer<Cancellable>() {
308 
309                     @Override
310                     public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
311                         final FutureCallback<String> callback = invocation.getArgument(1);
312                         callback.completed("stuff");
313                         return cancellable;
314                     }
315 
316                 });
317         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize("not-foo", existingValue));
318         Mockito.when(impl.store(
319                 ArgumentMatchers.eq("bar"),
320                 ArgumentMatchers.<byte[]>any(),
321                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
322 
323             @Override
324             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
325                 final FutureCallback<Boolean> callback = invocation.getArgument(2);
326                 callback.completed(true);
327                 return cancellable;
328             }
329 
330         });
331 
332         impl.updateEntry(key, new HttpCacheCASOperation() {
333 
334             @Override
335             public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
336                 Assert.assertThat(existing, CoreMatchers.nullValue());
337                 return updatedValue;
338             }
339 
340         }, operationCallback);
341 
342         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any());
343         Mockito.verify(impl).getStorageObject("stuff");
344         Mockito.verify(impl, Mockito.never()).updateCAS(
345                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
346         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
347         Mockito.verify(operationCallback).completed(Boolean.TRUE);
348     }
349 
350     @Test
351     public void testSingleCacheUpdateRetry() throws Exception {
352         final String key = "foo";
353         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
354         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
355 
356         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
357         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any())).thenAnswer(
358                 new Answer<Cancellable>() {
359 
360                     @Override
361                     public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
362                         final FutureCallback<String> callback = invocation.getArgument(1);
363                         callback.completed("stuff");
364                         return cancellable;
365                     }
366 
367                 });
368         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
369         final AtomicInteger count = new AtomicInteger(0);
370         Mockito.when(impl.updateCAS(
371                 ArgumentMatchers.eq("bar"),
372                 ArgumentMatchers.eq("stuff"),
373                 ArgumentMatchers.<byte[]>any(),
374                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
375 
376             @Override
377             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
378                 final FutureCallback<Boolean> callback = invocation.getArgument(3);
379                 if (count.incrementAndGet() == 1) {
380                     callback.completed(false);
381                 } else {
382                     callback.completed(true);
383                 }
384                 return cancellable;
385             }
386 
387         });
388 
389         impl.updateEntry(key, new HttpCacheCASOperation() {
390 
391             @Override
392             public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
393                 return updatedValue;
394             }
395 
396         }, operationCallback);
397 
398         Mockito.verify(impl, Mockito.times(2)).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any());
399         Mockito.verify(impl, Mockito.times(2)).getStorageObject("stuff");
400         Mockito.verify(impl, Mockito.times(2)).updateCAS(
401                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
402         Mockito.verify(operationCallback).completed(Boolean.TRUE);
403     }
404 
405     @Test
406     public void testCacheUpdateFail() throws Exception {
407         final String key = "foo";
408         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
409         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
410 
411         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
412         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any())).thenAnswer(
413                 new Answer<Cancellable>() {
414 
415                     @Override
416                     public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
417                         final FutureCallback<String> callback = invocation.getArgument(1);
418                         callback.completed("stuff");
419                         return cancellable;
420                     }
421 
422                 });
423         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
424         final AtomicInteger count = new AtomicInteger(0);
425         Mockito.when(impl.updateCAS(
426                 ArgumentMatchers.eq("bar"),
427                 ArgumentMatchers.eq("stuff"),
428                 ArgumentMatchers.<byte[]>any(),
429                 ArgumentMatchers.<FutureCallback<Boolean>>any())).thenAnswer(new Answer<Cancellable>() {
430 
431             @Override
432             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
433                 final FutureCallback<Boolean> callback = invocation.getArgument(3);
434                 if (count.incrementAndGet() <= 3) {
435                     callback.completed(false);
436                 } else {
437                     callback.completed(true);
438                 }
439                 return cancellable;
440             }
441 
442         });
443 
444         impl.updateEntry(key, new HttpCacheCASOperation() {
445 
446             @Override
447             public HttpCacheEntry execute(final HttpCacheEntry existing) throws ResourceIOException {
448                 return updatedValue;
449             }
450 
451         }, operationCallback);
452 
453         Mockito.verify(impl, Mockito.times(3)).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.<FutureCallback<String>>any());
454         Mockito.verify(impl, Mockito.times(3)).getStorageObject("stuff");
455         Mockito.verify(impl, Mockito.times(3)).updateCAS(
456                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.<byte[]>any(), ArgumentMatchers.<FutureCallback<Boolean>>any());
457         Mockito.verify(operationCallback).failed(ArgumentMatchers.<HttpCacheUpdateException>any());
458     }
459 
460     @Test
461     @SuppressWarnings("unchecked")
462     public void testBulkGet() throws Exception {
463         final String key1 = "foo this";
464         final String key2 = "foo that";
465         final String storageKey1 = "bar this";
466         final String storageKey2 = "bar that";
467         final HttpCacheEntry value1 = HttpTestUtils.makeCacheEntry();
468         final HttpCacheEntry value2 = HttpTestUtils.makeCacheEntry();
469 
470         when(impl.digestToStorageKey(key1)).thenReturn(storageKey1);
471         when(impl.digestToStorageKey(key2)).thenReturn(storageKey2);
472 
473         when(impl.bulkRestore(
474                 ArgumentMatchers.<String>anyCollection(),
475                 ArgumentMatchers.<FutureCallback<Map<String, byte[]>>>any())).thenAnswer(new Answer<Cancellable>() {
476 
477             @Override
478             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
479                 final Collection<String> keys = invocation.getArgument(0);
480                 final FutureCallback<Map<String, byte[]>> callback = invocation.getArgument(1);
481                 final Map<String, byte[]> resultMap = new HashMap<>();
482                 if (keys.contains(storageKey1)) {
483                     resultMap.put(storageKey1, serialize(key1, value1));
484                 }
485                 if (keys.contains(storageKey2)) {
486                     resultMap.put(storageKey2, serialize(key2, value2));
487                 }
488                 callback.completed(resultMap);
489                 return cancellable;
490             }
491         });
492 
493         impl.getEntries(Arrays.asList(key1, key2), bulkCacheEntryCallback);
494         final ArgumentCaptor<Map<String, HttpCacheEntry>> argumentCaptor = ArgumentCaptor.forClass(Map.class);
495         Mockito.verify(bulkCacheEntryCallback).completed(argumentCaptor.capture());
496 
497         final Map<String, HttpCacheEntry> entryMap = argumentCaptor.getValue();
498         Assert.assertThat(entryMap, CoreMatchers.notNullValue());
499         Assert.assertThat(entryMap.get(key1), HttpCacheEntryMatcher.equivalent(value1));
500         Assert.assertThat(entryMap.get(key2), HttpCacheEntryMatcher.equivalent(value2));
501 
502         verify(impl, Mockito.times(2)).digestToStorageKey(key1);
503         verify(impl, Mockito.times(2)).digestToStorageKey(key2);
504         verify(impl).bulkRestore(
505                 ArgumentMatchers.eq(Arrays.asList(storageKey1, storageKey2)),
506                 ArgumentMatchers.<FutureCallback<Map<String, byte[]>>>any());
507     }
508 
509     @Test
510     @SuppressWarnings("unchecked")
511     public void testBulkGetKeyMismatch() throws Exception {
512         final String key1 = "foo this";
513         final String key2 = "foo that";
514         final String storageKey1 = "bar this";
515         final String storageKey2 = "bar that";
516         final HttpCacheEntry value1 = HttpTestUtils.makeCacheEntry();
517         final HttpCacheEntry value2 = HttpTestUtils.makeCacheEntry();
518 
519         when(impl.digestToStorageKey(key1)).thenReturn(storageKey1);
520         when(impl.digestToStorageKey(key2)).thenReturn(storageKey2);
521 
522         when(impl.bulkRestore(
523                 ArgumentMatchers.<String>anyCollection(),
524                 ArgumentMatchers.<FutureCallback<Map<String, byte[]>>>any())).thenAnswer(new Answer<Cancellable>() {
525 
526             @Override
527             public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
528                 final Collection<String> keys = invocation.getArgument(0);
529                 final FutureCallback<Map<String, byte[]>> callback = invocation.getArgument(1);
530                 final Map<String, byte[]> resultMap = new HashMap<>();
531                 if (keys.contains(storageKey1)) {
532                     resultMap.put(storageKey1, serialize(key1, value1));
533                 }
534                 if (keys.contains(storageKey2)) {
535                     resultMap.put(storageKey2, serialize("not foo", value2));
536                 }
537                 callback.completed(resultMap);
538                 return cancellable;
539             }
540         });
541 
542         impl.getEntries(Arrays.asList(key1, key2), bulkCacheEntryCallback);
543         final ArgumentCaptor<Map<String, HttpCacheEntry>> argumentCaptor = ArgumentCaptor.forClass(Map.class);
544         Mockito.verify(bulkCacheEntryCallback).completed(argumentCaptor.capture());
545 
546         final Map<String, HttpCacheEntry> entryMap = argumentCaptor.getValue();
547         Assert.assertThat(entryMap, CoreMatchers.notNullValue());
548         Assert.assertThat(entryMap.get(key1), HttpCacheEntryMatcher.equivalent(value1));
549         Assert.assertThat(entryMap.get(key2), CoreMatchers.nullValue());
550 
551         verify(impl, Mockito.times(2)).digestToStorageKey(key1);
552         verify(impl, Mockito.times(2)).digestToStorageKey(key2);
553         verify(impl).bulkRestore(
554                 ArgumentMatchers.eq(Arrays.asList(storageKey1, storageKey2)),
555                 ArgumentMatchers.<FutureCallback<Map<String, byte[]>>>any());
556     }
557 
558 }