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.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") // test code
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         // Fail on an unexpected request, rather than causing a later NPE
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 }