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.hamcrest.MatcherAssert.assertThat;
30  import static org.mockito.Mockito.verify;
31  import static org.mockito.Mockito.when;
32  
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.HashMap;
36  import java.util.Map;
37  import java.util.concurrent.atomic.AtomicInteger;
38  
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.jupiter.api.Assertions;
47  import org.junit.jupiter.api.BeforeEach;
48  import org.junit.jupiter.api.Test;
49  import org.mockito.Answers;
50  import org.mockito.ArgumentCaptor;
51  import org.mockito.ArgumentMatchers;
52  import org.mockito.Mock;
53  import org.mockito.Mockito;
54  import org.mockito.MockitoAnnotations;
55  import org.mockito.stubbing.Answer;
56  
57  public class TestAbstractSerializingAsyncCacheStorage {
58  
59      @Mock
60      private Cancellable cancellable;
61      @Mock
62      private FutureCallback<Boolean> operationCallback;
63      @Mock
64      private FutureCallback<HttpCacheEntry> cacheEntryCallback;
65      @Mock
66      private FutureCallback<Map<String, HttpCacheEntry>> bulkCacheEntryCallback;
67  
68      private AbstractBinaryAsyncCacheStorage<String> impl;
69  
70      public static byte[] serialize(final String key, final HttpCacheEntry value) throws ResourceIOException {
71          return ByteArrayCacheEntrySerializer.INSTANCE.serialize(new HttpCacheStorageEntry(key, value));
72      }
73  
74      @BeforeEach
75      @SuppressWarnings("unchecked")
76      public void setUp() {
77          MockitoAnnotations.openMocks(this);
78          impl = Mockito.mock(AbstractBinaryAsyncCacheStorage.class,
79                  Mockito.withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS).useConstructor(3));
80      }
81  
82      @Test
83      public void testCachePut() throws Exception {
84          final String key = "foo";
85          final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
86  
87          Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
88          Mockito.when(impl.store(
89                  ArgumentMatchers.eq("bar"),
90                  ArgumentMatchers.any(),
91                  ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
92                      final FutureCallback<Boolean> callback = invocation.getArgument(2);
93                      callback.completed(true);
94                      return cancellable;
95                  });
96  
97          impl.putEntry(key, value, operationCallback);
98  
99          final ArgumentCaptor<byte[]> argumentCaptor = ArgumentCaptor.forClass(byte[].class);
100         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), argumentCaptor.capture(), ArgumentMatchers.any());
101         Assertions.assertArrayEquals(serialize(key, value), argumentCaptor.getValue());
102         Mockito.verify(operationCallback).completed(Boolean.TRUE);
103     }
104 
105     @Test
106     public void testCacheGetNullEntry() throws Exception {
107         final String key = "foo";
108 
109         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
110         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
111             final FutureCallback<byte[]> callback = invocation.getArgument(1);
112             callback.completed(null);
113             return cancellable;
114         });
115 
116         impl.getEntry(key, cacheEntryCallback);
117         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
118         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
119         assertThat(argumentCaptor.getValue(), CoreMatchers.nullValue());
120         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
121     }
122 
123     @Test
124     public void testCacheGet() throws Exception {
125         final String key = "foo";
126         final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
127 
128         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
129         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
130             final FutureCallback<byte[]> callback = invocation.getArgument(1);
131             callback.completed(serialize(key, value));
132             return cancellable;
133         });
134 
135         impl.getEntry(key, cacheEntryCallback);
136         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
137         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
138         final HttpCacheEntry resultingEntry = argumentCaptor.getValue();
139         assertThat(resultingEntry, HttpCacheEntryMatcher.equivalent(value));
140         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
141     }
142 
143     @Test
144     public void testCacheGetKeyMismatch() throws Exception {
145         final String key = "foo";
146         final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
147         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
148         Mockito.when(impl.restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
149             final FutureCallback<byte[]> callback = invocation.getArgument(1);
150             callback.completed(serialize("not-foo", value));
151             return cancellable;
152         });
153 
154         impl.getEntry(key, cacheEntryCallback);
155         final ArgumentCaptor<HttpCacheEntry> argumentCaptor = ArgumentCaptor.forClass(HttpCacheEntry.class);
156         Mockito.verify(cacheEntryCallback).completed(argumentCaptor.capture());
157         assertThat(argumentCaptor.getValue(), CoreMatchers.nullValue());
158         Mockito.verify(impl).restore(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
159     }
160 
161     @Test
162     public void testCacheRemove()  throws Exception{
163         final String key = "foo";
164 
165         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
166         Mockito.when(impl.delete(
167                 ArgumentMatchers.eq("bar"),
168                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
169                     final FutureCallback<Boolean> callback = invocation.getArgument(1);
170                     callback.completed(true);
171                     return cancellable;
172                 });
173         impl.removeEntry(key, operationCallback);
174 
175         Mockito.verify(impl).delete("bar", operationCallback);
176         Mockito.verify(operationCallback).completed(Boolean.TRUE);
177     }
178 
179     @Test
180     public void testCacheUpdateNullEntry() throws Exception {
181         final String key = "foo";
182         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
183 
184         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
185         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
186             final FutureCallback<byte[]> callback = invocation.getArgument(1);
187             callback.completed(null);
188             return cancellable;
189         });
190         Mockito.when(impl.store(
191                 ArgumentMatchers.eq("bar"),
192                 ArgumentMatchers.any(),
193                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
194                     final FutureCallback<Boolean> callback = invocation.getArgument(2);
195                     callback.completed(true);
196                     return cancellable;
197                 });
198 
199         impl.updateEntry(key, existing -> {
200             assertThat(existing, CoreMatchers.nullValue());
201             return updatedValue;
202         }, operationCallback);
203 
204         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
205         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), ArgumentMatchers.any(), ArgumentMatchers.any());
206         Mockito.verify(operationCallback).completed(Boolean.TRUE);
207     }
208 
209     @Test
210     public void testCacheCASUpdate() throws Exception {
211         final String key = "foo";
212         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
213         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
214 
215         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
216         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
217             final FutureCallback<String> callback = invocation.getArgument(1);
218             callback.completed("stuff");
219             return cancellable;
220         });
221         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
222         Mockito.when(impl.updateCAS(
223                 ArgumentMatchers.eq("bar"),
224                 ArgumentMatchers.eq("stuff"),
225                 ArgumentMatchers.any(),
226                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
227                     final FutureCallback<Boolean> callback = invocation.getArgument(3);
228                     callback.completed(true);
229                     return cancellable;
230                 });
231 
232         impl.updateEntry(key, existing -> updatedValue, operationCallback);
233 
234         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
235         Mockito.verify(impl).getStorageObject("stuff");
236         Mockito.verify(impl).updateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.any(), ArgumentMatchers.any());
237         Mockito.verify(operationCallback).completed(Boolean.TRUE);
238     }
239 
240     @Test
241     public void testCacheCASUpdateKeyMismatch() throws Exception {
242         final String key = "foo";
243         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
244         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
245 
246         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
247         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer(
248                 (Answer<Cancellable>) invocation -> {
249                     final FutureCallback<String> callback = invocation.getArgument(1);
250                     callback.completed("stuff");
251                     return cancellable;
252                 });
253         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize("not-foo", existingValue));
254         Mockito.when(impl.store(
255                 ArgumentMatchers.eq("bar"),
256                 ArgumentMatchers.any(),
257                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
258                     final FutureCallback<Boolean> callback = invocation.getArgument(2);
259                     callback.completed(true);
260                     return cancellable;
261                 });
262 
263         impl.updateEntry(key, existing -> {
264             assertThat(existing, CoreMatchers.nullValue());
265             return updatedValue;
266         }, operationCallback);
267 
268         Mockito.verify(impl).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
269         Mockito.verify(impl).getStorageObject("stuff");
270         Mockito.verify(impl, Mockito.never()).updateCAS(
271                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.any(), ArgumentMatchers.any());
272         Mockito.verify(impl).store(ArgumentMatchers.eq("bar"), ArgumentMatchers.any(), ArgumentMatchers.any());
273         Mockito.verify(operationCallback).completed(Boolean.TRUE);
274     }
275 
276     @Test
277     public void testSingleCacheUpdateRetry() throws Exception {
278         final String key = "foo";
279         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
280         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
281 
282         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
283         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer(
284                 (Answer<Cancellable>) invocation -> {
285                     final FutureCallback<String> callback = invocation.getArgument(1);
286                     callback.completed("stuff");
287                     return cancellable;
288                 });
289         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
290         final AtomicInteger count = new AtomicInteger(0);
291         Mockito.when(impl.updateCAS(
292                 ArgumentMatchers.eq("bar"),
293                 ArgumentMatchers.eq("stuff"),
294                 ArgumentMatchers.any(),
295                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
296                     final FutureCallback<Boolean> callback = invocation.getArgument(3);
297                     if (count.incrementAndGet() == 1) {
298                         callback.completed(false);
299                     } else {
300                         callback.completed(true);
301                     }
302                     return cancellable;
303                 });
304 
305         impl.updateEntry(key, existing -> updatedValue, operationCallback);
306 
307         Mockito.verify(impl, Mockito.times(2)).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
308         Mockito.verify(impl, Mockito.times(2)).getStorageObject("stuff");
309         Mockito.verify(impl, Mockito.times(2)).updateCAS(
310                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.any(), ArgumentMatchers.any());
311         Mockito.verify(operationCallback).completed(Boolean.TRUE);
312     }
313 
314     @Test
315     public void testCacheUpdateFail() throws Exception {
316         final String key = "foo";
317         final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
318         final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
319 
320         Mockito.when(impl.digestToStorageKey(key)).thenReturn("bar");
321         Mockito.when(impl.getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any())).thenAnswer(
322                 (Answer<Cancellable>) invocation -> {
323                     final FutureCallback<String> callback = invocation.getArgument(1);
324                     callback.completed("stuff");
325                     return cancellable;
326                 });
327         Mockito.when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
328         final AtomicInteger count = new AtomicInteger(0);
329         Mockito.when(impl.updateCAS(
330                 ArgumentMatchers.eq("bar"),
331                 ArgumentMatchers.eq("stuff"),
332                 ArgumentMatchers.any(),
333                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
334                     final FutureCallback<Boolean> callback = invocation.getArgument(3);
335                     if (count.incrementAndGet() <= 3) {
336                         callback.completed(false);
337                     } else {
338                         callback.completed(true);
339                     }
340                     return cancellable;
341                 });
342 
343         impl.updateEntry(key, existing -> updatedValue, operationCallback);
344 
345         Mockito.verify(impl, Mockito.times(3)).getForUpdateCAS(ArgumentMatchers.eq("bar"), ArgumentMatchers.any());
346         Mockito.verify(impl, Mockito.times(3)).getStorageObject("stuff");
347         Mockito.verify(impl, Mockito.times(3)).updateCAS(
348                 ArgumentMatchers.eq("bar"), ArgumentMatchers.eq("stuff"), ArgumentMatchers.any(), ArgumentMatchers.any());
349         Mockito.verify(operationCallback).failed(ArgumentMatchers.<HttpCacheUpdateException>any());
350     }
351 
352     @Test
353     @SuppressWarnings("unchecked")
354     public void testBulkGet() throws Exception {
355         final String key1 = "foo this";
356         final String key2 = "foo that";
357         final String storageKey1 = "bar this";
358         final String storageKey2 = "bar that";
359         final HttpCacheEntry value1 = HttpTestUtils.makeCacheEntry();
360         final HttpCacheEntry value2 = HttpTestUtils.makeCacheEntry();
361 
362         when(impl.digestToStorageKey(key1)).thenReturn(storageKey1);
363         when(impl.digestToStorageKey(key2)).thenReturn(storageKey2);
364 
365         when(impl.bulkRestore(
366                 ArgumentMatchers.anyCollection(),
367                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
368                     final Collection<String> keys = invocation.getArgument(0);
369                     final FutureCallback<Map<String, byte[]>> callback = invocation.getArgument(1);
370                     final Map<String, byte[]> resultMap = new HashMap<>();
371                     if (keys.contains(storageKey1)) {
372                         resultMap.put(storageKey1, serialize(key1, value1));
373                     }
374                     if (keys.contains(storageKey2)) {
375                         resultMap.put(storageKey2, serialize(key2, value2));
376                     }
377                     callback.completed(resultMap);
378                     return cancellable;
379                 });
380 
381         impl.getEntries(Arrays.asList(key1, key2), bulkCacheEntryCallback);
382         final ArgumentCaptor<Map<String, HttpCacheEntry>> argumentCaptor = ArgumentCaptor.forClass(Map.class);
383         Mockito.verify(bulkCacheEntryCallback).completed(argumentCaptor.capture());
384 
385         final Map<String, HttpCacheEntry> entryMap = argumentCaptor.getValue();
386         assertThat(entryMap, CoreMatchers.notNullValue());
387         assertThat(entryMap.get(key1), HttpCacheEntryMatcher.equivalent(value1));
388         assertThat(entryMap.get(key2), HttpCacheEntryMatcher.equivalent(value2));
389 
390         verify(impl, Mockito.times(2)).digestToStorageKey(key1);
391         verify(impl, Mockito.times(2)).digestToStorageKey(key2);
392         verify(impl).bulkRestore(
393                 ArgumentMatchers.eq(Arrays.asList(storageKey1, storageKey2)),
394                 ArgumentMatchers.any());
395     }
396 
397     @Test
398     @SuppressWarnings("unchecked")
399     public void testBulkGetKeyMismatch() throws Exception {
400         final String key1 = "foo this";
401         final String key2 = "foo that";
402         final String storageKey1 = "bar this";
403         final String storageKey2 = "bar that";
404         final HttpCacheEntry value1 = HttpTestUtils.makeCacheEntry();
405         final HttpCacheEntry value2 = HttpTestUtils.makeCacheEntry();
406 
407         when(impl.digestToStorageKey(key1)).thenReturn(storageKey1);
408         when(impl.digestToStorageKey(key2)).thenReturn(storageKey2);
409 
410         when(impl.bulkRestore(
411                 ArgumentMatchers.anyCollection(),
412                 ArgumentMatchers.any())).thenAnswer((Answer<Cancellable>) invocation -> {
413                     final Collection<String> keys = invocation.getArgument(0);
414                     final FutureCallback<Map<String, byte[]>> callback = invocation.getArgument(1);
415                     final Map<String, byte[]> resultMap = new HashMap<>();
416                     if (keys.contains(storageKey1)) {
417                         resultMap.put(storageKey1, serialize(key1, value1));
418                     }
419                     if (keys.contains(storageKey2)) {
420                         resultMap.put(storageKey2, serialize("not foo", value2));
421                     }
422                     callback.completed(resultMap);
423                     return cancellable;
424                 });
425 
426         impl.getEntries(Arrays.asList(key1, key2), bulkCacheEntryCallback);
427         final ArgumentCaptor<Map<String, HttpCacheEntry>> argumentCaptor = ArgumentCaptor.forClass(Map.class);
428         Mockito.verify(bulkCacheEntryCallback).completed(argumentCaptor.capture());
429 
430         final Map<String, HttpCacheEntry> entryMap = argumentCaptor.getValue();
431         assertThat(entryMap, CoreMatchers.notNullValue());
432         assertThat(entryMap.get(key1), HttpCacheEntryMatcher.equivalent(value1));
433         assertThat(entryMap.get(key2), CoreMatchers.nullValue());
434 
435         verify(impl, Mockito.times(2)).digestToStorageKey(key1);
436         verify(impl, Mockito.times(2)).digestToStorageKey(key2);
437         verify(impl).bulkRestore(
438                 ArgumentMatchers.eq(Arrays.asList(storageKey1, storageKey2)),
439                 ArgumentMatchers.any());
440     }
441 
442 }