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.easymock.EasyMock.anyObject;
30 import static org.easymock.EasyMock.eq;
31 import static org.easymock.EasyMock.expect;
32 import static org.easymock.EasyMock.isA;
33 import static org.easymock.EasyMock.same;
34 import static org.easymock.EasyMock.createMockBuilder;
35 import static org.easymock.EasyMock.createNiceMock;
36 import static org.easymock.EasyMock.replay;
37 import static org.easymock.EasyMock.verify;
38 import static org.junit.Assert.assertTrue;
39
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.util.Date;
43 import java.util.HashMap;
44 import java.util.Map;
45 import java.util.concurrent.atomic.AtomicInteger;
46
47 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
48 import org.apache.hc.client5.http.cache.HttpCacheEntry;
49 import org.apache.hc.client5.http.classic.ExecChain;
50 import org.apache.hc.client5.http.classic.methods.HttpGet;
51 import org.apache.hc.client5.http.utils.DateUtils;
52 import org.apache.hc.core5.http.ClassicHttpRequest;
53 import org.apache.hc.core5.http.ClassicHttpResponse;
54 import org.apache.hc.core5.http.HeaderElements;
55 import org.apache.hc.core5.http.HttpHeaders;
56 import org.apache.hc.core5.http.HttpHost;
57 import org.apache.hc.core5.http.HttpRequest;
58 import org.apache.hc.core5.http.HttpResponse;
59 import org.apache.hc.core5.http.HttpStatus;
60 import org.apache.hc.core5.http.HttpVersion;
61 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
62 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
63 import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
64 import org.easymock.EasyMock;
65 import org.easymock.IExpectationSetters;
66 import org.junit.Assert;
67 import org.junit.Before;
68 import org.junit.Test;
69
70 @SuppressWarnings("boxing")
71 public class TestCachingExec extends TestCachingExecChain {
72
73 private static final String GET_CURRENT_DATE = "getCurrentDate";
74
75 private static final String HANDLE_BACKEND_RESPONSE = "handleBackendResponse";
76
77 private static final String CALL_BACKEND = "callBackend";
78
79 private static final String REVALIDATE_CACHE_ENTRY = "revalidateCacheEntry";
80
81 private CachingExec impl;
82 private boolean mockedImpl;
83
84 private ExecChain.Scope scope;
85 private ClassicHttpResponse mockBackendResponse;
86
87 private Date requestDate;
88 private Date responseDate;
89
90 @Override
91 @Before
92 public void setUp() {
93 super.setUp();
94
95 scope = new ExecChain.Scope("test", route, request, mockEndpoint, context);
96 mockBackendResponse = createNiceMock(ClassicHttpResponse.class);
97
98 requestDate = new Date(System.currentTimeMillis() - 1000);
99 responseDate = new Date();
100 }
101
102 @Override
103 public CachingExec createCachingExecChain(
104 final HttpCache mockCache, final CacheValidityPolicy mockValidityPolicy,
105 final ResponseCachingPolicy mockResponsePolicy,
106 final CachedHttpResponseGenerator mockResponseGenerator,
107 final CacheableRequestPolicy mockRequestPolicy,
108 final CachedResponseSuitabilityChecker mockSuitabilityChecker,
109 final ResponseProtocolCompliance mockResponseProtocolCompliance,
110 final RequestProtocolCompliance mockRequestProtocolCompliance,
111 final DefaultCacheRevalidator mockCacheRevalidator,
112 final ConditionalRequestBuilder<ClassicHttpRequest> mockConditionalRequestBuilder,
113 final CacheConfig config) {
114 return impl = new CachingExec(
115 mockCache,
116 mockValidityPolicy,
117 mockResponsePolicy,
118 mockResponseGenerator,
119 mockRequestPolicy,
120 mockSuitabilityChecker,
121 mockResponseProtocolCompliance,
122 mockRequestProtocolCompliance,
123 mockCacheRevalidator,
124 mockConditionalRequestBuilder,
125 config);
126 }
127
128 @Override
129 public CachingExec createCachingExecChain(final HttpCache cache, final CacheConfig config) {
130 return impl = new CachingExec(cache, null, config);
131 }
132
133 @Override
134 protected void replayMocks() {
135 super.replayMocks();
136 replay(mockBackendResponse);
137 if (mockedImpl) {
138 replay(impl);
139 }
140 }
141
142 @Override
143 protected void verifyMocks() {
144 super.verifyMocks();
145 verify(mockBackendResponse);
146 if (mockedImpl) {
147 verify(impl);
148 }
149 }
150
151
152 @Test
153 public void testRequestThatCannotBeServedFromCacheCausesBackendRequest() throws Exception {
154 cacheInvalidatorWasCalled();
155 requestPolicyAllowsCaching(false);
156 mockImplMethods(CALL_BACKEND);
157
158 implExpectsAnyRequestAndReturn(mockBackendResponse);
159 requestIsFatallyNonCompliant(null);
160
161 replayMocks();
162 final HttpResponse result = impl.execute(request, scope, mockExecChain);
163 verifyMocks();
164
165 Assert.assertSame(mockBackendResponse, result);
166 }
167
168 @Test
169 public void testCacheMissCausesBackendRequest() throws Exception {
170 mockImplMethods(CALL_BACKEND);
171 requestPolicyAllowsCaching(true);
172 getCacheEntryReturns(null);
173 getVariantCacheEntriesReturns(new HashMap<String,Variant>());
174
175 requestIsFatallyNonCompliant(null);
176
177 implExpectsAnyRequestAndReturn(mockBackendResponse);
178
179 replayMocks();
180 final HttpResponse result = impl.execute(request, scope, mockExecChain);
181 verifyMocks();
182
183 Assert.assertSame(mockBackendResponse, result);
184 Assert.assertEquals(1, impl.getCacheMisses());
185 Assert.assertEquals(0, impl.getCacheHits());
186 Assert.assertEquals(0, impl.getCacheUpdates());
187 }
188
189 @Test
190 public void testUnsuitableUnvalidatableCacheEntryCausesBackendRequest() throws Exception {
191 mockImplMethods(CALL_BACKEND);
192 requestPolicyAllowsCaching(true);
193 requestIsFatallyNonCompliant(null);
194
195 getCacheEntryReturns(mockCacheEntry);
196 cacheEntrySuitable(false);
197 cacheEntryValidatable(false);
198 expect(mockConditionalRequestBuilder.buildConditionalRequest(request, mockCacheEntry))
199 .andReturn(request);
200 backendExpectsRequestAndReturn(request, mockBackendResponse);
201 expect(mockBackendResponse.getVersion()).andReturn(HttpVersion.HTTP_1_1).anyTimes();
202 expect(mockBackendResponse.getCode()).andReturn(200);
203
204 replayMocks();
205 final HttpResponse result = impl.execute(request, scope, mockExecChain);
206 verifyMocks();
207
208 Assert.assertSame(mockBackendResponse, result);
209 Assert.assertEquals(0, impl.getCacheMisses());
210 Assert.assertEquals(1, impl.getCacheHits());
211 Assert.assertEquals(1, impl.getCacheUpdates());
212 }
213
214 @Test
215 public void testUnsuitableValidatableCacheEntryCausesRevalidation() throws Exception {
216 mockImplMethods(REVALIDATE_CACHE_ENTRY);
217 requestPolicyAllowsCaching(true);
218 requestIsFatallyNonCompliant(null);
219
220 getCacheEntryReturns(mockCacheEntry);
221 cacheEntrySuitable(false);
222 cacheEntryValidatable(true);
223 cacheEntryMustRevalidate(false);
224 cacheEntryProxyRevalidate(false);
225 mayReturnStaleWhileRevalidating(false);
226
227 expect(impl.revalidateCacheEntry(
228 isA(HttpHost.class),
229 isA(ClassicHttpRequest.class),
230 isA(ExecChain.Scope.class),
231 isA(ExecChain.class),
232 isA(HttpCacheEntry.class))).andReturn(mockBackendResponse);
233
234 replayMocks();
235 final HttpResponse result = impl.execute(request, scope, mockExecChain);
236 verifyMocks();
237
238 Assert.assertSame(mockBackendResponse, result);
239 Assert.assertEquals(0, impl.getCacheMisses());
240 Assert.assertEquals(1, impl.getCacheHits());
241 Assert.assertEquals(0, impl.getCacheUpdates());
242 }
243
244 @Test
245 public void testRevalidationCallsHandleBackEndResponseWhenNot200Or304() throws Exception {
246 mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
247
248 final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/");
249 final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NOT_FOUND, "Not Found");
250 final ClassicHttpResponse finalResponse = HttpTestUtils.make200Response();
251
252 conditionalRequestBuilderReturns(validate);
253 getCurrentDateReturns(requestDate);
254 backendExpectsRequestAndReturn(validate, originResponse);
255 getCurrentDateReturns(responseDate);
256 expect(impl.handleBackendResponse(
257 same(host),
258 same(validate),
259 same(scope),
260 eq(requestDate),
261 eq(responseDate),
262 same(originResponse))).andReturn(finalResponse);
263
264 replayMocks();
265 final HttpResponse result =
266 impl.revalidateCacheEntry(host, request, scope, mockExecChain, entry);
267 verifyMocks();
268
269 Assert.assertSame(finalResponse, result);
270 }
271
272 @Test
273 public void testRevalidationUpdatesCacheEntryAndPutsItToCacheWhen304ReturningCachedResponse()
274 throws Exception {
275
276 mockImplMethods(GET_CURRENT_DATE);
277
278 final ClassicHttpRequest validate = new BasicClassicHttpRequest("GET", "/");
279 final ClassicHttpResponse originResponse = HttpTestUtils.make304Response();
280 final HttpCacheEntry updatedEntry = HttpTestUtils.makeCacheEntry();
281
282 conditionalRequestBuilderReturns(validate);
283 getCurrentDateReturns(requestDate);
284 backendExpectsRequestAndReturn(validate, originResponse);
285 getCurrentDateReturns(responseDate);
286 expect(mockCache.updateCacheEntry(
287 eq(host),
288 same(request),
289 same(entry),
290 same(originResponse),
291 eq(requestDate),
292 eq(responseDate)))
293 .andReturn(updatedEntry);
294 expect(mockSuitabilityChecker.isConditional(request)).andReturn(false);
295 responseIsGeneratedFromCache(SimpleHttpResponse.create(HttpStatus.SC_OK));
296
297 replayMocks();
298 impl.revalidateCacheEntry(host, request, scope, mockExecChain, entry);
299 verifyMocks();
300 }
301
302 @Test
303 public void testRevalidationRewritesAbsoluteUri() throws Exception {
304
305 mockImplMethods(GET_CURRENT_DATE);
306
307
308 EasyMock.resetToStrict(mockExecChain);
309
310 final ClassicHttpRequest validate = new HttpGet("http://foo.example.com/resource");
311 final ClassicHttpRequest relativeValidate = new BasicClassicHttpRequest("GET", "/resource");
312 final ClassicHttpResponse originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "Okay");
313
314 conditionalRequestBuilderReturns(validate);
315 getCurrentDateReturns(requestDate);
316
317 final ClassicHttpResponse resp = mockExecChain.proceed(
318 eqRequest(relativeValidate), isA(ExecChain.Scope.class));
319 expect(resp).andReturn(originResponse);
320
321 getCurrentDateReturns(responseDate);
322
323 replayMocks();
324 impl.revalidateCacheEntry(host, request, scope, mockExecChain, entry);
325 verifyMocks();
326 }
327
328 @Test
329 public void testEndlessResponsesArePassedThrough() throws Exception {
330 impl = createCachingExecChain(new BasicHttpCache(), CacheConfig.DEFAULT);
331
332 final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
333 resp1.setHeader("Date", DateUtils.formatDate(new Date()));
334 resp1.setHeader("Server", "MockOrigin/1.0");
335 resp1.setHeader(HttpHeaders.TRANSFER_ENCODING, HeaderElements.CHUNKED_ENCODING);
336
337 final AtomicInteger size = new AtomicInteger();
338 final AtomicInteger maxlength = new AtomicInteger(Integer.MAX_VALUE);
339 resp1.setEntity(new InputStreamEntity(new InputStream() {
340 private Throwable closed;
341
342 @Override
343 public void close() throws IOException {
344 closed = new Throwable();
345 super.close();
346 }
347
348 @Override
349 public int read() throws IOException {
350 Thread.yield();
351 if (closed != null) {
352 throw new IOException("Response has been closed");
353
354 }
355 if (size.incrementAndGet() > maxlength.get()) {
356 return -1;
357 }
358 return 'y';
359 }
360 }, -1, null));
361
362 final ClassicHttpResponse resp = mockExecChain.proceed(
363 isA(ClassicHttpRequest.class), isA(ExecChain.Scope.class));
364 EasyMock.expect(resp).andReturn(resp1);
365
366 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
367
368 replayMocks();
369 final ClassicHttpResponse resp2 = impl.execute(req1, scope, mockExecChain);
370 maxlength.set(size.get() * 2);
371 verifyMocks();
372 assertTrue(HttpTestUtils.semanticallyTransparent(resp1, resp2));
373 }
374
375 @Test
376 public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
377 mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
378 final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
379 getCurrentDateReturns(requestDate);
380 backendExpectsRequestAndReturn(request, resp);
381 getCurrentDateReturns(responseDate);
382 handleBackendResponseReturnsResponse(request, resp);
383
384 replayMocks();
385
386 impl.callBackend(host, request, scope, mockExecChain);
387
388 verifyMocks();
389 }
390
391 @Test
392 public void testDoesNotFlushCachesOnCacheHit() throws Exception {
393 requestPolicyAllowsCaching(true);
394 requestIsFatallyNonCompliant(null);
395
396 getCacheEntryReturns(mockCacheEntry);
397 doesNotFlushCache();
398 cacheEntrySuitable(true);
399 cacheEntryValidatable(true);
400
401 responseIsGeneratedFromCache(SimpleHttpResponse.create(HttpStatus.SC_OK));
402
403 replayMocks();
404 impl.execute(request, scope, mockExecChain);
405 verifyMocks();
406 }
407
408 private IExpectationSetters<ClassicHttpResponse> implExpectsAnyRequestAndReturn(
409 final ClassicHttpResponse response) throws Exception {
410 final ClassicHttpResponse resp = impl.callBackend(
411 same(host),
412 isA(ClassicHttpRequest.class),
413 isA(ExecChain.Scope.class),
414 isA(ExecChain.class));
415 return EasyMock.expect(resp).andReturn(response);
416 }
417
418 private void getVariantCacheEntriesReturns(final Map<String,Variant> result) {
419 expect(mockCache.getVariantCacheEntriesWithEtags(host, request)).andReturn(result);
420 }
421
422 private void cacheInvalidatorWasCalled() {
423 mockCache.flushCacheEntriesInvalidatedByRequest((HttpHost)anyObject(), (HttpRequest)anyObject());
424 }
425
426 private void getCurrentDateReturns(final Date date) {
427 expect(impl.getCurrentDate()).andReturn(date);
428 }
429
430 private void handleBackendResponseReturnsResponse(final ClassicHttpRequest request, final ClassicHttpResponse response)
431 throws IOException {
432 expect(
433 impl.handleBackendResponse(
434 same(host),
435 same(request),
436 same(scope),
437 isA(Date.class),
438 isA(Date.class),
439 isA(ClassicHttpResponse.class))).andReturn(response);
440 }
441
442 private void mockImplMethods(final String... methods) {
443 mockedImpl = true;
444 impl = createMockBuilder(CachingExec.class).withConstructor(
445 mockCache,
446 mockValidityPolicy,
447 mockResponsePolicy,
448 mockResponseGenerator,
449 mockRequestPolicy,
450 mockSuitabilityChecker,
451 mockResponseProtocolCompliance,
452 mockRequestProtocolCompliance,
453 mockCacheRevalidator,
454 mockConditionalRequestBuilder,
455 config).addMockedMethods(methods).createNiceMock();
456 }
457
458 }