1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 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 HttpByteArrayCacheEntrySerializer.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 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 void testCacheGetNullEntry() {
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 void testCacheGet() {
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 void testCacheGetKeyMismatch() {
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 void testCacheRemove() {
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 void testCacheUpdateNullEntry() {
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 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 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 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 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 void testBulkGet() {
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 void testBulkGetKeyMismatch() {
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 }