hc-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ol...@apache.org
Subject svn commit: r939814 [4/6] - in /httpcomponents/httpclient/trunk: ./ httpclient-cache/ httpclient-cache/src/ httpclient-cache/src/main/ httpclient-cache/src/main/java/ httpclient-cache/src/main/java/org/ httpclient-cache/src/main/java/org/apache/ httpcl...
Date Fri, 30 Apr 2010 21:00:10 GMT
Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachedResponseSuitabilityChecker.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachedResponseSuitabilityChecker.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachedResponseSuitabilityChecker.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachedResponseSuitabilityChecker.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,257 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.cache.impl.CacheEntry;
+import org.apache.http.client.cache.impl.CachedResponseSuitabilityChecker;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpRequest;
+import org.easymock.classextension.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestCachedResponseSuitabilityChecker {
+
+    private CachedResponseSuitabilityChecker impl;
+    private HttpHost host;
+    private HttpRequest request;
+    private CacheEntry mockEntry;
+    private HttpRequest mockRequest;
+
+    @Before
+    public void setUp() {
+        host = new HttpHost("foo.example.com");
+        request = new BasicHttpRequest("GET", "/foo");
+        mockEntry = EasyMock.createMock(CacheEntry.class);
+        mockRequest = EasyMock.createMock(HttpRequest.class);
+
+        impl = new CachedResponseSuitabilityChecker();
+    }
+
+    public void replayMocks() {
+        EasyMock.replay(mockEntry, mockRequest);
+    }
+
+    public void verifyMocks() {
+        EasyMock.verify(mockEntry, mockRequest);
+    }
+
+    @Test
+    public void testNotSuitableIfContentLengthHeaderIsWrong() {
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(false);
+
+        replayMocks();
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testSuitableIfContentLengthHeaderIsRight() {
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        replayMocks();
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertTrue(result);
+    }
+
+    @Test
+    public void testSuitableIfCacheEntryIsFresh() {
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertTrue(result);
+    }
+
+    @Test
+    public void testNotSuitableIfCacheEntryIsNotFresh() {
+        responseIsFresh(false);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testNotSuitableIfRequestHasNoCache() {
+        request.addHeader("Cache-Control", "no-cache");
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testNotSuitableIfAgeExceedsRequestMaxAge() {
+        request.addHeader("Cache-Control", "max-age=10");
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        org.easymock.EasyMock.expect(mockEntry.getCurrentAgeSecs()).andReturn(20L);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testSuitableIfFreshAndAgeIsUnderRequestMaxAge() {
+        request.addHeader("Cache-Control", "max-age=10");
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        org.easymock.EasyMock.expect(mockEntry.getCurrentAgeSecs()).andReturn(5L);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertTrue(result);
+    }
+
+    @Test
+    public void testSuitableIfFreshAndFreshnessLifetimeGreaterThanRequestMinFresh() {
+        request.addHeader("Cache-Control", "min-fresh=10");
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        org.easymock.EasyMock.expect(mockEntry.getFreshnessLifetimeSecs()).andReturn(15L);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertTrue(result);
+    }
+
+    @Test
+    public void testNotSuitableIfFreshnessLifetimeLessThanRequestMinFresh() {
+        request.addHeader("Cache-Control", "min-fresh=10");
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, request);
+
+        org.easymock.EasyMock.expect(mockEntry.getFreshnessLifetimeSecs()).andReturn(5L);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertFalse(result);
+    }
+
+    // this is compliant but possibly misses some cache hits; would
+    // need to change logic to add Warning header if we allowed this
+    @Test
+    public void testNotSuitableEvenIfStaleButPermittedByRequestMaxStale() {
+        request.addHeader("Cache-Control", "max-stale=10");
+        responseIsFresh(false);
+
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, request, mockEntry);
+        verifyMocks();
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testMalformedCacheControlMaxAgeRequestHeaderCausesUnsuitableEntry() {
+
+        Header[] hdrs = new Header[] { new BasicHeader("Cache-Control", "max-age=foo") };
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, mockRequest);
+
+        org.easymock.EasyMock.expect(mockRequest.getHeaders("Cache-Control")).andReturn(hdrs);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, mockRequest, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testMalformedCacheControlMinFreshRequestHeaderCausesUnsuitableEntry() {
+
+        Header[] hdrs = new Header[] { new BasicHeader("Cache-Control", "min-fresh=foo") };
+        responseIsFresh(true);
+        contentLengthMatchesActualLength(true);
+        modifiedSince(false, mockRequest);
+
+        org.easymock.EasyMock.expect(mockRequest.getHeaders("Cache-Control")).andReturn(hdrs);
+        replayMocks();
+
+        boolean result = impl.canCachedResponseBeUsed(host, mockRequest, mockEntry);
+
+        verifyMocks();
+
+        Assert.assertFalse(result);
+    }
+
+    private void responseIsFresh(boolean fresh) {
+        org.easymock.EasyMock.expect(mockEntry.isResponseFresh()).andReturn(fresh);
+    }
+
+    private void modifiedSince(boolean modified, HttpRequest request) {
+        org.easymock.EasyMock.expect(mockEntry.modifiedSince(request)).andReturn(modified);
+    }
+
+    private void contentLengthMatchesActualLength(boolean b) {
+        org.easymock.EasyMock.expect(mockEntry.contentLengthHeaderMatchesActualLength()).andReturn(
+                b);
+    }
+}
\ No newline at end of file

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachingHttpClient.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachingHttpClient.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachingHttpClient.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCachingHttpClient.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,1126 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolException;
+import org.apache.http.RequestLine;
+import org.apache.http.StatusLine;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.cache.HttpCacheUpdateCallback;
+import org.apache.http.client.cache.HttpCache;
+import org.apache.http.client.cache.impl.BasicHttpCache;
+import org.apache.http.client.cache.impl.CacheEntry;
+import org.apache.http.client.cache.impl.CacheEntryGenerator;
+import org.apache.http.client.cache.impl.CacheEntryUpdater;
+import org.apache.http.client.cache.impl.CacheInvalidator;
+import org.apache.http.client.cache.impl.CacheableRequestPolicy;
+import org.apache.http.client.cache.impl.CachedHttpResponseGenerator;
+import org.apache.http.client.cache.impl.CachedResponseSuitabilityChecker;
+import org.apache.http.client.cache.impl.CachingHttpClient;
+import org.apache.http.client.cache.impl.ConditionalRequestBuilder;
+import org.apache.http.client.cache.impl.RequestProtocolCompliance;
+import org.apache.http.client.cache.impl.RequestProtocolError;
+import org.apache.http.client.cache.impl.ResponseCachingPolicy;
+import org.apache.http.client.cache.impl.ResponseProtocolCompliance;
+import org.apache.http.client.cache.impl.SizeLimitedResponseReader;
+import org.apache.http.client.cache.impl.URIExtractor;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+import org.easymock.classextension.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class TestCachingHttpClient {
+
+    private static final String GET_CURRENT_DATE = "getCurrentDate";
+
+    private static final String HANDLE_BACKEND_RESPONSE = "handleBackendResponse";
+
+    private static final String CALL_BACKEND = "callBackend";
+
+    private static final String REVALIDATE_CACHE_ENTRY = "revalidateCacheEntry";
+
+    private static final String GET_CACHE_ENTRY = "getCacheEntry";
+
+    private static final String STORE_IN_CACHE = "storeInCache";
+
+    private static final String GET_RESPONSE_READER = "getResponseReader";
+
+    private CachingHttpClient impl;
+
+    private CacheInvalidator mockInvalidator;
+    private CacheableRequestPolicy mockRequestPolicy;
+    private HttpClient mockBackend;
+    private HttpCache<CacheEntry> mockCache;
+    private CachedResponseSuitabilityChecker mockSuitabilityChecker;
+    private ResponseCachingPolicy mockResponsePolicy;
+    private HttpRequest mockRequest;
+    private HttpResponse mockBackendResponse;
+    private CacheEntry mockCacheEntry;
+    private CacheEntry mockVariantCacheEntry;
+    private URIExtractor mockExtractor;
+    private CacheEntryGenerator mockEntryGenerator;
+    private CachedHttpResponseGenerator mockResponseGenerator;
+
+    private SizeLimitedResponseReader mockResponseReader;
+    private HttpHost host;
+    private ClientConnectionManager mockConnectionManager;
+    private HttpContext mockContext;
+    private ResponseHandler<Object> mockHandler;
+    private HttpParams mockParams;
+    private HttpUriRequest mockUriRequest;
+    private HttpResponse mockCachedResponse;
+    private HttpResponse mockReconstructedResponse;
+
+    private ConditionalRequestBuilder mockConditionalRequestBuilder;
+
+    private HttpRequest mockConditionalRequest;
+
+    private StatusLine mockStatusLine;
+    private Date requestDate;
+    private Date responseDate;
+
+    private boolean mockedImpl;
+
+    private CacheEntryUpdater mockCacheEntryUpdater;
+    private ResponseProtocolCompliance mockResponseProtocolCompliance;
+    private RequestProtocolCompliance mockRequestProtocolCompliance;
+    private RequestLine mockRequestLine;
+
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setUp() {
+
+        mockInvalidator = EasyMock.createMock(CacheInvalidator.class);
+        mockRequestPolicy = EasyMock.createMock(CacheableRequestPolicy.class);
+        mockBackend = EasyMock.createMock(HttpClient.class);
+        mockCache = EasyMock.createMock(HttpCache.class);
+        mockSuitabilityChecker = EasyMock.createMock(CachedResponseSuitabilityChecker.class);
+        mockResponsePolicy = EasyMock.createMock(ResponseCachingPolicy.class);
+        mockConnectionManager = EasyMock.createMock(ClientConnectionManager.class);
+        mockContext = EasyMock.createMock(HttpContext.class);
+        mockHandler = EasyMock.createMock(ResponseHandler.class);
+        mockParams = EasyMock.createMock(HttpParams.class);
+        mockRequest = EasyMock.createMock(HttpRequest.class);
+        mockBackendResponse = EasyMock.createMock(HttpResponse.class);
+        mockUriRequest = EasyMock.createMock(HttpUriRequest.class);
+        mockCacheEntry = EasyMock.createMock(CacheEntry.class);
+        mockVariantCacheEntry = EasyMock.createMock(CacheEntry.class);
+        mockExtractor = EasyMock.createMock(URIExtractor.class);
+        mockEntryGenerator = EasyMock.createMock(CacheEntryGenerator.class);
+        mockResponseGenerator = EasyMock.createMock(CachedHttpResponseGenerator.class);
+        mockCachedResponse = EasyMock.createMock(HttpResponse.class);
+        mockConditionalRequestBuilder = EasyMock.createMock(ConditionalRequestBuilder.class);
+        mockConditionalRequest = EasyMock.createMock(HttpRequest.class);
+        mockStatusLine = EasyMock.createMock(StatusLine.class);
+        mockCacheEntryUpdater = EasyMock.createMock(CacheEntryUpdater.class);
+        mockResponseReader = EasyMock.createMock(SizeLimitedResponseReader.class);
+        mockReconstructedResponse = EasyMock.createMock(HttpResponse.class);
+        mockResponseProtocolCompliance = EasyMock.createMock(ResponseProtocolCompliance.class);
+        mockRequestProtocolCompliance = EasyMock.createMock(RequestProtocolCompliance.class);
+        mockRequestLine = EasyMock.createMock(RequestLine.class);
+
+        requestDate = new Date(System.currentTimeMillis() - 1000);
+        responseDate = new Date();
+        host = new HttpHost("foo.example.com");
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance);
+    }
+
+    private void replayMocks() {
+
+        EasyMock.replay(mockInvalidator);
+        EasyMock.replay(mockRequestPolicy);
+        EasyMock.replay(mockSuitabilityChecker);
+        EasyMock.replay(mockResponsePolicy);
+        EasyMock.replay(mockCacheEntry);
+        EasyMock.replay(mockVariantCacheEntry);
+        EasyMock.replay(mockEntryGenerator);
+        EasyMock.replay(mockResponseGenerator);
+        EasyMock.replay(mockExtractor);
+        EasyMock.replay(mockBackend);
+        EasyMock.replay(mockCache);
+        EasyMock.replay(mockConnectionManager);
+        EasyMock.replay(mockContext);
+        EasyMock.replay(mockHandler);
+        EasyMock.replay(mockParams);
+        EasyMock.replay(mockRequest);
+        EasyMock.replay(mockBackendResponse);
+        EasyMock.replay(mockUriRequest);
+        EasyMock.replay(mockCachedResponse);
+        EasyMock.replay(mockConditionalRequestBuilder);
+        EasyMock.replay(mockConditionalRequest);
+        EasyMock.replay(mockStatusLine);
+        EasyMock.replay(mockCacheEntryUpdater);
+        EasyMock.replay(mockResponseReader);
+        EasyMock.replay(mockReconstructedResponse);
+        EasyMock.replay(mockResponseProtocolCompliance);
+        EasyMock.replay(mockRequestProtocolCompliance);
+
+        if (mockedImpl) {
+            EasyMock.replay(impl);
+        }
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mockInvalidator);
+        EasyMock.verify(mockRequestPolicy);
+        EasyMock.verify(mockSuitabilityChecker);
+        EasyMock.verify(mockResponsePolicy);
+        EasyMock.verify(mockCacheEntry);
+        EasyMock.verify(mockVariantCacheEntry);
+        EasyMock.verify(mockEntryGenerator);
+        EasyMock.verify(mockResponseGenerator);
+        EasyMock.verify(mockExtractor);
+        EasyMock.verify(mockBackend);
+        EasyMock.verify(mockCache);
+        EasyMock.verify(mockConnectionManager);
+        EasyMock.verify(mockContext);
+        EasyMock.verify(mockHandler);
+        EasyMock.verify(mockParams);
+        EasyMock.verify(mockRequest);
+        EasyMock.verify(mockBackendResponse);
+        EasyMock.verify(mockUriRequest);
+        EasyMock.verify(mockCachedResponse);
+        EasyMock.verify(mockConditionalRequestBuilder);
+        EasyMock.verify(mockConditionalRequest);
+        EasyMock.verify(mockStatusLine);
+        EasyMock.verify(mockCacheEntryUpdater);
+        EasyMock.verify(mockResponseReader);
+        EasyMock.verify(mockReconstructedResponse);
+        EasyMock.verify(mockResponseProtocolCompliance);
+        EasyMock.verify(mockRequestProtocolCompliance);
+
+        if (mockedImpl) {
+            EasyMock.verify(impl);
+        }
+    }
+
+    @Test
+    public void testCacheableResponsesGoIntoCache() throws Exception {
+        mockImplMethods(STORE_IN_CACHE, GET_RESPONSE_READER);
+        responsePolicyAllowsCaching(true);
+
+        responseProtocolValidationIsCalled();
+
+        getMockResponseReader();
+        responseIsTooLarge(false);
+        byte[] buf = responseReaderReturnsBufferOfSize(100);
+
+        generateCacheEntry(requestDate, responseDate, buf);
+        storeInCacheWasCalled();
+        responseIsGeneratedFromCache();
+
+        replayMocks();
+        HttpResponse result = impl.handleBackendResponse(host, mockRequest, requestDate,
+                responseDate, mockBackendResponse);
+        verifyMocks();
+
+        Assert.assertSame(mockCachedResponse, result);
+    }
+
+    @Test
+    public void testRequestThatCannotBeServedFromCacheCausesBackendRequest() throws Exception {
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(false);
+        mockImplMethods(CALL_BACKEND);
+
+        callBackendReturnsResponse(mockBackendResponse);
+        requestProtocolValidationIsCalled();
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertSame(mockBackendResponse, result);
+    }
+
+    private void requestInspectsRequestLine() {
+        org.easymock.EasyMock.expect(mockRequest.getRequestLine()).andReturn(mockRequestLine);
+    }
+
+    private void requestIsFatallyNonCompliant(RequestProtocolError error) {
+        List<RequestProtocolError> errors = new ArrayList<RequestProtocolError>();
+        if (error != null) {
+            errors.add(error);
+        }
+        org.easymock.EasyMock.expect(
+                mockRequestProtocolCompliance.requestIsFatallyNonCompliant(mockRequest)).andReturn(
+                errors);
+    }
+
+    @Test
+    public void testStoreInCachePutsNonVariantEntryInPlace() throws Exception {
+
+        final String theURI = "theURI";
+
+        cacheEntryHasVariants(false);
+        extractTheURI(theURI);
+        putInCache(theURI);
+
+        replayMocks();
+        impl.storeInCache(host, mockRequest, mockCacheEntry);
+        verifyMocks();
+    }
+
+    @Test
+    public void testCacheUpdateCallbackCreatesNewParentEntryWhenParentEntryNull() throws Exception {
+
+        final String variantURI = "variantURI";
+
+        extractVariantURI(variantURI);
+        putInCache(variantURI);
+
+        variantURIAddedToCacheEntry(variantURI);
+
+        replayMocks();
+        HttpCacheUpdateCallback<CacheEntry> callbackImpl = impl
+                .storeVariantEntry(host, mockRequest, mockCacheEntry);
+        callbackImpl.getUpdatedEntry(null);
+        verifyMocks();
+    }
+
+    private void variantURIAddedToCacheEntry(String variantURI) {
+        mockCacheEntry.addVariantURI(variantURI);
+    }
+
+    @Test
+    public void testCacheMissCausesBackendRequest() throws Exception {
+        mockImplMethods(GET_CACHE_ENTRY, CALL_BACKEND);
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(true);
+        getCacheEntryReturns(null);
+        requestProtocolValidationIsCalled();
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+
+        callBackendReturnsResponse(mockBackendResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertSame(mockBackendResponse, result);
+        Assert.assertEquals(1, impl.getCacheMisses());
+        Assert.assertEquals(0, impl.getCacheHits());
+        Assert.assertEquals(0, impl.getCacheUpdates());
+    }
+
+    @Test
+    public void testUnsuitableUnvalidatableCacheEntryCausesBackendRequest() throws Exception {
+        mockImplMethods(GET_CACHE_ENTRY, CALL_BACKEND);
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(true);
+        requestProtocolValidationIsCalled();
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+
+        getCacheEntryReturns(mockCacheEntry);
+        cacheEntrySuitable(false);
+        cacheEntryValidatable(false);
+        callBackendReturnsResponse(mockBackendResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertSame(mockBackendResponse, result);
+        Assert.assertEquals(0, impl.getCacheMisses());
+        Assert.assertEquals(1, impl.getCacheHits());
+        Assert.assertEquals(0, impl.getCacheUpdates());
+    }
+
+    @Test
+    public void testUnsuitableValidatableCacheEntryCausesRevalidation() throws Exception {
+        mockImplMethods(GET_CACHE_ENTRY, REVALIDATE_CACHE_ENTRY);
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(true);
+        requestProtocolValidationIsCalled();
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+
+        getCacheEntryReturns(mockCacheEntry);
+        cacheEntrySuitable(false);
+        cacheEntryValidatable(true);
+        revalidateCacheEntryReturns(mockBackendResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertSame(mockBackendResponse, result);
+        Assert.assertEquals(0, impl.getCacheMisses());
+        Assert.assertEquals(1, impl.getCacheHits());
+        Assert.assertEquals(0, impl.getCacheUpdates());
+    }
+
+    @Test
+    public void testRevalidationCallsHandleBackEndResponseWhenNot304() throws Exception {
+        mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
+
+        conditionalRequestBuilderCalled();
+        getCurrentDateReturns(requestDate);
+        backendCallWasMadeWithRequest(mockConditionalRequest);
+        getCurrentDateReturns(responseDate);
+        backendResponseCodeIs(HttpStatus.SC_OK);
+        cacheEntryUpdaterCalled();
+        cacheEntryHasVariants(false);
+        extractTheURI("http://foo.example.com");
+        putInCache("http://foo.example.com");
+        responseIsGeneratedFromCache();
+
+        replayMocks();
+
+        HttpResponse result = impl.revalidateCacheEntry(host, mockRequest, mockContext,
+                mockCacheEntry);
+
+        verifyMocks();
+
+        Assert.assertEquals(mockCachedResponse, result);
+        Assert.assertEquals(0, impl.getCacheMisses());
+        Assert.assertEquals(0, impl.getCacheHits());
+        Assert.assertEquals(1, impl.getCacheUpdates());
+    }
+
+    @Test
+    public void testRevalidationUpdatesCacheEntryAndPutsItToCacheWhen304ReturningCachedResponse()
+            throws Exception {
+        mockImplMethods(GET_CURRENT_DATE, STORE_IN_CACHE);
+        conditionalRequestBuilderCalled();
+        getCurrentDateReturns(requestDate);
+        backendCallWasMadeWithRequest(mockConditionalRequest);
+        getCurrentDateReturns(responseDate);
+        backendResponseCodeIs(HttpStatus.SC_NOT_MODIFIED);
+
+        cacheEntryUpdaterCalled();
+        storeInCacheWasCalled();
+
+        responseIsGeneratedFromCache();
+
+        replayMocks();
+
+        HttpResponse result = impl.revalidateCacheEntry(host, mockRequest, mockContext,
+                mockCacheEntry);
+
+        verifyMocks();
+
+        Assert.assertEquals(mockCachedResponse, result);
+        Assert.assertEquals(0, impl.getCacheMisses());
+        Assert.assertEquals(0, impl.getCacheHits());
+        Assert.assertEquals(1, impl.getCacheUpdates());
+    }
+
+    @Test
+    public void testSuitableCacheEntryDoesNotCauseBackendRequest() throws Exception {
+        mockImplMethods(GET_CACHE_ENTRY);
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(true);
+        requestProtocolValidationIsCalled();
+        getCacheEntryReturns(mockCacheEntry);
+        cacheEntrySuitable(true);
+        responseIsGeneratedFromCache();
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertSame(mockCachedResponse, result);
+    }
+
+    @Test
+    public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
+        mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
+        getCurrentDateReturns(requestDate);
+        backendCallWasMadeWithRequest(mockRequest);
+        getCurrentDateReturns(responseDate);
+        handleBackendResponseReturnsResponse(mockRequest, mockBackendResponse);
+
+        replayMocks();
+
+        impl.callBackend(host, mockRequest, mockContext);
+
+        verifyMocks();
+    }
+
+    @Test
+    public void testNonCacheableResponseIsNotCachedAndIsReturnedAsIs() throws Exception {
+        final String theURI = "theURI";
+        Date currentDate = new Date();
+        responsePolicyAllowsCaching(false);
+        responseProtocolValidationIsCalled();
+
+        extractTheURI(theURI);
+        removeFromCache(theURI);
+
+        replayMocks();
+        HttpResponse result = impl.handleBackendResponse(host, mockRequest, currentDate,
+                currentDate, mockBackendResponse);
+        verifyMocks();
+
+        Assert.assertSame(mockBackendResponse, result);
+    }
+
+    @Test
+    public void testGetCacheEntryReturnsNullOnCacheMiss() throws Exception {
+
+        final String theURI = "theURI";
+        extractTheURI(theURI);
+        gotCacheMiss(theURI);
+
+        replayMocks();
+        CacheEntry result = impl.getCacheEntry(host, mockRequest);
+        verifyMocks();
+        Assert.assertNull(result);
+    }
+
+    @Test
+    public void testGetCacheEntryFetchesFromCacheOnCacheHitIfNoVariants() throws Exception {
+
+        final String theURI = "theURI";
+        extractTheURI(theURI);
+        gotCacheHit(theURI);
+        cacheEntryHasVariants(false);
+
+        replayMocks();
+        CacheEntry result = impl.getCacheEntry(host, mockRequest);
+        verifyMocks();
+        Assert.assertSame(mockCacheEntry, result);
+    }
+
+    @Test
+    public void testGetCacheEntryReturnsNullIfNoVariantInCache() throws Exception {
+
+        final String theURI = "theURI";
+        final String variantURI = "variantURI";
+        extractTheURI(theURI);
+        gotCacheHit(theURI);
+        cacheEntryHasVariants(true);
+        extractVariantURI(variantURI);
+        gotCacheMiss(variantURI);
+
+        replayMocks();
+        CacheEntry result = impl.getCacheEntry(host, mockRequest);
+        verifyMocks();
+        Assert.assertNull(result);
+    }
+
+    @Test
+    public void testGetCacheEntryReturnsVariantIfPresentInCache() throws Exception {
+
+        final String theURI = "theURI";
+        final String variantURI = "variantURI";
+        extractTheURI(theURI);
+        gotCacheHit(theURI, mockCacheEntry);
+        cacheEntryHasVariants(true);
+        extractVariantURI(variantURI);
+        gotCacheHit(variantURI, mockVariantCacheEntry);
+
+        replayMocks();
+        CacheEntry result = impl.getCacheEntry(host, mockRequest);
+        verifyMocks();
+        Assert.assertSame(mockVariantCacheEntry, result);
+    }
+
+    @Test
+    public void testTooLargeResponsesAreNotCached() throws Exception {
+        mockImplMethods(GET_CURRENT_DATE, GET_RESPONSE_READER, STORE_IN_CACHE);
+        getCurrentDateReturns(requestDate);
+        backendCallWasMadeWithRequest(mockRequest);
+        responseProtocolValidationIsCalled();
+
+        getCurrentDateReturns(responseDate);
+        responsePolicyAllowsCaching(true);
+        getMockResponseReader();
+        responseIsTooLarge(true);
+        readerReturnsReconstructedResponse();
+
+        replayMocks();
+
+        impl.callBackend(host, mockRequest, mockContext);
+
+        verifyMocks();
+    }
+
+    @Test
+    public void testSmallEnoughResponsesAreCached() throws Exception {
+        requestDate = new Date();
+        responseDate = new Date();
+        mockImplMethods(GET_CURRENT_DATE, GET_RESPONSE_READER, STORE_IN_CACHE);
+        getCurrentDateReturns(requestDate);
+        responseProtocolValidationIsCalled();
+
+        backendCallWasMadeWithRequest(mockRequest);
+        getCurrentDateReturns(responseDate);
+        responsePolicyAllowsCaching(true);
+        getMockResponseReader();
+        responseIsTooLarge(false);
+        byte[] buf = responseReaderReturnsBufferOfSize(100);
+        generateCacheEntry(requestDate, responseDate, buf);
+        storeInCacheWasCalled();
+        responseIsGeneratedFromCache();
+
+        replayMocks();
+
+        impl.callBackend(host, mockRequest, mockContext);
+
+        verifyMocks();
+    }
+
+    @Test
+    public void testCallsSelfForExecuteOnHostRequestWithNullContext() throws Exception {
+        final Counter c = new Counter();
+        final HttpHost theHost = host;
+        final HttpRequest theRequest = mockRequest;
+        final HttpResponse theResponse = mockBackendResponse;
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) {
+                Assert.assertSame(theHost, target);
+                Assert.assertSame(theRequest, request);
+                Assert.assertNull(context);
+                c.incr();
+                return theResponse;
+            }
+        };
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, mockRequest);
+        verifyMocks();
+        Assert.assertSame(mockBackendResponse, result);
+        Assert.assertEquals(1, c.getCount());
+    }
+
+    @Test
+    public void testCallsSelfWithDefaultContextForExecuteOnHostRequestWithHandler()
+            throws Exception {
+
+        final Counter c = new Counter();
+        final HttpHost theHost = host;
+        final HttpRequest theRequest = mockRequest;
+        final HttpResponse theResponse = mockBackendResponse;
+        final ResponseHandler<Object> theHandler = mockHandler;
+        final Object value = new Object();
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public <T> T execute(HttpHost target, HttpRequest request,
+                    ResponseHandler<? extends T> rh, HttpContext context) {
+                Assert.assertSame(theHost, target);
+                Assert.assertSame(theRequest, request);
+                Assert.assertSame(theHandler, rh);
+                Assert.assertNull(context);
+                c.incr();
+                try {
+                    return rh.handleResponse(theResponse);
+                } catch (Exception wrong) {
+                    throw new RuntimeException("unexpected exn", wrong);
+                }
+            }
+        };
+
+        org.easymock.EasyMock.expect(mockHandler.handleResponse(mockBackendResponse)).andReturn(
+                value);
+
+        replayMocks();
+        Object result = impl.execute(host, mockRequest, mockHandler);
+        verifyMocks();
+
+        Assert.assertSame(value, result);
+        Assert.assertEquals(1, c.getCount());
+    }
+
+    @Test
+    public void testCallsSelfOnExecuteHostRequestWithHandlerAndContext() throws Exception {
+
+        final Counter c = new Counter();
+        final HttpHost theHost = host;
+        final HttpRequest theRequest = mockRequest;
+        final HttpResponse theResponse = mockBackendResponse;
+        final HttpContext theContext = mockContext;
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) {
+                Assert.assertSame(theHost, target);
+                Assert.assertSame(theRequest, request);
+                Assert.assertSame(theContext, context);
+                c.incr();
+                return theResponse;
+            }
+        };
+
+        final Object theObject = new Object();
+
+        org.easymock.EasyMock.expect(mockHandler.handleResponse(mockBackendResponse)).andReturn(
+                theObject);
+
+        replayMocks();
+        Object result = impl.execute(host, mockRequest, mockHandler, mockContext);
+        verifyMocks();
+        Assert.assertEquals(1, c.getCount());
+        Assert.assertSame(theObject, result);
+    }
+
+    @Test
+    public void testCallsSelfWithNullContextOnExecuteUriRequest() throws Exception {
+        final Counter c = new Counter();
+        final HttpUriRequest theRequest = mockUriRequest;
+        final HttpResponse theResponse = mockBackendResponse;
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public HttpResponse execute(HttpUriRequest request, HttpContext context) {
+                Assert.assertSame(theRequest, request);
+                Assert.assertNull(context);
+                c.incr();
+                return theResponse;
+            }
+        };
+
+        replayMocks();
+        HttpResponse result = impl.execute(mockUriRequest);
+        verifyMocks();
+
+        Assert.assertEquals(1, c.getCount());
+        Assert.assertSame(theResponse, result);
+    }
+
+    @Test
+    public void testCallsSelfWithExtractedHostOnExecuteUriRequestWithContext() throws Exception {
+
+        final URI uri = new URI("sch://host:8888");
+        final Counter c = new Counter();
+        final HttpRequest theRequest = mockUriRequest;
+        final HttpContext theContext = mockContext;
+        final HttpResponse theResponse = mockBackendResponse;
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public HttpResponse execute(HttpHost hh, HttpRequest req, HttpContext ctx) {
+                Assert.assertEquals("sch", hh.getSchemeName());
+                Assert.assertEquals("host", hh.getHostName());
+                Assert.assertEquals(8888, hh.getPort());
+                Assert.assertSame(theRequest, req);
+                Assert.assertSame(theContext, ctx);
+                c.incr();
+                return theResponse;
+            }
+        };
+
+        org.easymock.EasyMock.expect(mockUriRequest.getURI()).andReturn(uri);
+
+        replayMocks();
+        HttpResponse result = impl.execute(mockUriRequest, mockContext);
+        verifyMocks();
+
+        Assert.assertEquals(1, c.getCount());
+        Assert.assertSame(mockBackendResponse, result);
+    }
+
+    @Test
+    public void testCallsSelfWithNullContextOnExecuteUriRequestWithHandler() throws Exception {
+        final Counter c = new Counter();
+        final HttpUriRequest theRequest = mockUriRequest;
+        final HttpResponse theResponse = mockBackendResponse;
+        final Object theValue = new Object();
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> handler,
+                    HttpContext context) throws IOException {
+                Assert.assertSame(theRequest, request);
+                Assert.assertNull(context);
+                c.incr();
+                return handler.handleResponse(theResponse);
+            }
+        };
+
+        org.easymock.EasyMock.expect(mockHandler.handleResponse(mockBackendResponse)).andReturn(
+                theValue);
+
+        replayMocks();
+        Object result = impl.execute(mockUriRequest, mockHandler);
+        verifyMocks();
+
+        Assert.assertEquals(1, c.getCount());
+        Assert.assertSame(theValue, result);
+    }
+
+    @Test
+    public void testCallsSelfAndRunsHandlerOnExecuteUriRequestWithHandlerAndContext()
+            throws Exception {
+
+        final Counter c = new Counter();
+        final HttpUriRequest theRequest = mockUriRequest;
+        final HttpContext theContext = mockContext;
+        final HttpResponse theResponse = mockBackendResponse;
+        final Object theValue = new Object();
+        impl = new CachingHttpClient(mockBackend, mockResponsePolicy, mockEntryGenerator,
+                mockExtractor, mockCache, mockResponseGenerator, mockInvalidator,
+                mockRequestPolicy, mockSuitabilityChecker, mockConditionalRequestBuilder,
+                mockCacheEntryUpdater, mockResponseProtocolCompliance,
+                mockRequestProtocolCompliance) {
+            @Override
+            public HttpResponse execute(HttpUriRequest request, HttpContext context)
+                    throws IOException {
+                Assert.assertSame(theRequest, request);
+                Assert.assertSame(theContext, context);
+                c.incr();
+                return theResponse;
+            }
+        };
+
+        org.easymock.EasyMock.expect(mockHandler.handleResponse(mockBackendResponse)).andReturn(
+                theValue);
+
+        replayMocks();
+        Object result = impl.execute(mockUriRequest, mockHandler, mockContext);
+        verifyMocks();
+        Assert.assertEquals(1, c.getCount());
+        Assert.assertSame(theValue, result);
+    }
+
+    @Test
+    public void testUsesBackendsConnectionManager() {
+        org.easymock.EasyMock.expect(mockBackend.getConnectionManager()).andReturn(
+                mockConnectionManager);
+        replayMocks();
+        ClientConnectionManager result = impl.getConnectionManager();
+        verifyMocks();
+        Assert.assertSame(result, mockConnectionManager);
+    }
+
+    @Test
+    public void testUsesBackendsHttpParams() {
+        org.easymock.EasyMock.expect(mockBackend.getParams()).andReturn(mockParams);
+        replayMocks();
+        HttpParams result = impl.getParams();
+        verifyMocks();
+        Assert.assertSame(mockParams, result);
+    }
+
+    @Test
+    @Ignore
+    public void testRealResultsMatch() throws IOException {
+
+        SchemeRegistry schemeRegistry = new SchemeRegistry();
+        schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
+        schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
+
+        ClientConnectionManager cm = new ThreadSafeClientConnManager(schemeRegistry);
+        HttpClient httpClient = new DefaultHttpClient(cm);
+
+        HttpCache<CacheEntry> cacheImpl = new BasicHttpCache(100);
+
+        CachingHttpClient cachingClient = new CachingHttpClient(httpClient, cacheImpl, 8192);
+
+        HttpUriRequest request = new HttpGet("http://www.fancast.com/static-28262/styles/base.css");
+
+        HttpClient baseClient = new DefaultHttpClient();
+
+        HttpResponse cachedResponse = cachingClient.execute(request);
+        HttpResponse realResponse = baseClient.execute(request);
+
+        byte[] cached = readResponse(cachedResponse);
+        byte[] real = readResponse(realResponse);
+
+        Assert.assertArrayEquals(cached, real);
+    }
+
+    @Test
+    public void testResponseIsGeneratedWhenCacheEntryIsUsable() throws Exception {
+
+        final String theURI = "http://foo";
+
+        requestIsFatallyNonCompliant(null);
+        requestInspectsRequestLine();
+        requestProtocolValidationIsCalled();
+        cacheInvalidatorWasCalled();
+        requestPolicyAllowsCaching(true);
+        cacheEntrySuitable(true);
+        extractTheURI(theURI);
+        gotCacheHit(theURI);
+        responseIsGeneratedFromCache();
+        cacheEntryHasVariants(false);
+
+        replayMocks();
+        impl.execute(host, mockRequest, mockContext);
+        verifyMocks();
+    }
+
+    @Test
+    public void testNonCompliantRequestWrapsAndReThrowsProtocolException() throws Exception {
+
+        ProtocolException expected = new ProtocolException("ouch");
+
+        requestInspectsRequestLine();
+        requestIsFatallyNonCompliant(null);
+        requestCannotBeMadeCompliantThrows(expected);
+
+        boolean gotException = false;
+        replayMocks();
+        try {
+            impl.execute(host, mockRequest, mockContext);
+        } catch (ClientProtocolException ex) {
+            Assert.assertTrue(ex.getCause().getMessage().equals(expected.getMessage()));
+            gotException = true;
+        }
+        verifyMocks();
+        Assert.assertTrue(gotException);
+    }
+
+    private byte[] readResponse(HttpResponse response) {
+        try {
+            ByteArrayOutputStream s1 = new ByteArrayOutputStream();
+            response.getEntity().writeTo(s1);
+            return s1.toByteArray();
+        } catch (Exception ex) {
+            return new byte[] {};
+
+        }
+
+    }
+
+    private void cacheInvalidatorWasCalled() {
+        mockInvalidator.flushInvalidatedCacheEntries(host, mockRequest);
+    }
+
+    private void callBackendReturnsResponse(HttpResponse response) throws IOException {
+        org.easymock.EasyMock.expect(impl.callBackend(host, mockRequest, mockContext)).andReturn(
+                response);
+    }
+
+    private void revalidateCacheEntryReturns(HttpResponse response) throws IOException,
+            ProtocolException {
+        org.easymock.EasyMock.expect(
+                impl.revalidateCacheEntry(host, mockRequest, mockContext, mockCacheEntry))
+                .andReturn(response);
+    }
+
+    private void cacheEntryValidatable(boolean b) {
+        org.easymock.EasyMock.expect(mockCacheEntry.isRevalidatable()).andReturn(b);
+    }
+
+    private void cacheEntryUpdaterCalled() {
+        mockCacheEntryUpdater.updateCacheEntry(mockCacheEntry, requestDate, responseDate,
+                mockBackendResponse);
+    }
+
+    private void getCacheEntryReturns(CacheEntry entry) {
+        org.easymock.EasyMock.expect(impl.getCacheEntry(host, mockRequest)).andReturn(entry);
+    }
+
+    private void backendResponseCodeIs(int code) {
+        org.easymock.EasyMock.expect(mockBackendResponse.getStatusLine()).andReturn(mockStatusLine);
+        org.easymock.EasyMock.expect(mockStatusLine.getStatusCode()).andReturn(code);
+    }
+
+    private void conditionalRequestBuilderCalled() throws ProtocolException {
+        org.easymock.EasyMock.expect(
+                mockConditionalRequestBuilder.buildConditionalRequest(mockRequest, mockCacheEntry))
+                .andReturn(mockConditionalRequest);
+    }
+
+    private void getCurrentDateReturns(Date date) {
+        org.easymock.EasyMock.expect(impl.getCurrentDate()).andReturn(date);
+    }
+
+    private void getMockResponseReader() throws IOException {
+        org.easymock.EasyMock.expect(impl.getResponseReader(mockBackendResponse)).andReturn(
+                mockResponseReader);
+    }
+
+    private void removeFromCache(String theURI) throws Exception {
+        mockCache.removeEntry(theURI);
+    }
+
+    private void requestPolicyAllowsCaching(boolean allow) {
+        org.easymock.EasyMock.expect(mockRequestPolicy.isServableFromCache(mockRequest)).andReturn(
+                allow);
+    }
+
+    private byte[] responseReaderReturnsBufferOfSize(int bufferSize) {
+        byte[] buffer = new byte[bufferSize];
+        org.easymock.EasyMock.expect(mockResponseReader.getResponseBytes()).andReturn(buffer);
+        return buffer;
+    }
+
+    private void readerReturnsReconstructedResponse() throws IOException {
+        org.easymock.EasyMock.expect(mockResponseReader.getReconstructedResponse()).andReturn(
+                mockReconstructedResponse);
+    }
+
+    private void responseIsTooLarge(boolean tooLarge) throws Exception {
+        org.easymock.EasyMock.expect(mockResponseReader.isResponseTooLarge()).andReturn(tooLarge);
+    }
+
+    private void backendCallWasMadeWithRequest(HttpRequest request) throws IOException {
+        org.easymock.EasyMock.expect(mockBackend.execute(host, request, mockContext)).andReturn(
+                mockBackendResponse);
+    }
+
+    private void responsePolicyAllowsCaching(boolean allow) {
+        org.easymock.EasyMock.expect(
+                mockResponsePolicy.isResponseCacheable(mockRequest, mockBackendResponse))
+                .andReturn(allow);
+    }
+
+    private void gotCacheMiss(String theURI) throws Exception {
+        org.easymock.EasyMock.expect(mockCache.getEntry(theURI)).andReturn(null);
+    }
+
+    private void cacheEntrySuitable(boolean suitable) {
+        org.easymock.EasyMock.expect(
+                mockSuitabilityChecker.canCachedResponseBeUsed(host, mockRequest, mockCacheEntry))
+                .andReturn(suitable);
+    }
+
+    private void gotCacheHit(String theURI) throws Exception {
+        org.easymock.EasyMock.expect(mockCache.getEntry(theURI)).andReturn(mockCacheEntry);
+    }
+
+    private void gotCacheHit(String theURI, CacheEntry entry) throws Exception {
+        org.easymock.EasyMock.expect(mockCache.getEntry(theURI)).andReturn(entry);
+    }
+
+    private void cacheEntryHasVariants(boolean b) {
+        org.easymock.EasyMock.expect(mockCacheEntry.hasVariants()).andReturn(b);
+    }
+
+    private void responseIsGeneratedFromCache() {
+        org.easymock.EasyMock.expect(mockResponseGenerator.generateResponse(mockCacheEntry))
+                .andReturn(mockCachedResponse);
+    }
+
+    private void extractTheURI(String theURI) {
+        org.easymock.EasyMock.expect(mockExtractor.getURI(host, mockRequest)).andReturn(theURI);
+    }
+
+    private void extractVariantURI(String variantURI) {
+        org.easymock.EasyMock
+                .expect(mockExtractor.getVariantURI(host, mockRequest, mockCacheEntry)).andReturn(
+                        variantURI);
+    }
+
+    private void putInCache(String theURI) throws Exception {
+        mockCache.putEntry(theURI, mockCacheEntry);
+    }
+
+    private void generateCacheEntry(Date requestDate, Date responseDate, byte[] bytes)
+            throws IOException {
+        org.easymock.EasyMock.expect(
+                mockEntryGenerator.generateEntry(requestDate, responseDate, mockBackendResponse,
+                        bytes)).andReturn(mockCacheEntry);
+    }
+
+    private void handleBackendResponseReturnsResponse(HttpRequest request, HttpResponse response)
+            throws IOException {
+        org.easymock.EasyMock.expect(
+                impl.handleBackendResponse(host, request, requestDate, responseDate,
+                        mockBackendResponse)).andReturn(response);
+    }
+
+    private void storeInCacheWasCalled() {
+        impl.storeInCache(host, mockRequest, mockCacheEntry);
+    }
+
+    private void responseProtocolValidationIsCalled() throws ClientProtocolException {
+        mockResponseProtocolCompliance.ensureProtocolCompliance(mockRequest, mockBackendResponse);
+    }
+
+    private void requestProtocolValidationIsCalled() throws Exception {
+        org.easymock.EasyMock.expect(
+                mockRequestProtocolCompliance.makeRequestCompliant(mockRequest)).andReturn(
+                mockRequest);
+    }
+
+    private void requestCannotBeMadeCompliantThrows(ProtocolException exception) throws Exception {
+        org.easymock.EasyMock.expect(
+                mockRequestProtocolCompliance.makeRequestCompliant(mockRequest))
+                .andThrow(exception);
+    }
+
+    private void mockImplMethods(String... methods) {
+        mockedImpl = true;
+        impl = EasyMock.createMockBuilder(CachingHttpClient.class).withConstructor(mockBackend,
+                mockResponsePolicy, mockEntryGenerator, mockExtractor, mockCache,
+                mockResponseGenerator, mockInvalidator, mockRequestPolicy, mockSuitabilityChecker,
+                mockConditionalRequestBuilder, mockCacheEntryUpdater,
+                mockResponseProtocolCompliance, mockRequestProtocolCompliance).addMockedMethods(
+                methods).createMock();
+    }
+}

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCombinedInputStream.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCombinedInputStream.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCombinedInputStream.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestCombinedInputStream.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,119 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.http.client.cache.impl.CombinedInputStream;
+import org.easymock.classextension.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class TestCombinedInputStream {
+
+    private InputStream mockInputStream1;
+    private InputStream mockInputStream2;
+    private CombinedInputStream impl;
+
+    @Before
+    public void setUp() {
+        mockInputStream1 = EasyMock.createMock(InputStream.class);
+        mockInputStream2 = EasyMock.createMock(InputStream.class);
+
+        impl = new CombinedInputStream(mockInputStream1, mockInputStream2);
+    }
+
+    @Test
+    public void testCreatingInputStreamWithNullInputFails() {
+
+        boolean gotex1 = false;
+        boolean gotex2 = false;
+
+        try {
+            impl = new CombinedInputStream(null, mockInputStream2);
+        } catch (Exception ex) {
+            gotex1 = true;
+        }
+
+        try {
+            impl = new CombinedInputStream(mockInputStream1, null);
+        } catch (Exception ex) {
+            gotex2 = true;
+        }
+
+        Assert.assertTrue(gotex1);
+        Assert.assertTrue(gotex2);
+
+    }
+
+    @Test
+    public void testAvailableReturnsCorrectSize() throws Exception {
+        ByteArrayInputStream s1 = new ByteArrayInputStream(new byte[] { 1, 1, 1, 1, 1 });
+        ByteArrayInputStream s2 = new ByteArrayInputStream(new byte[] { 1, 1, 1, 1, 1 });
+
+        impl = new CombinedInputStream(s1, s2);
+        int avail = impl.available();
+
+        Assert.assertEquals(10, avail);
+    }
+
+    @Test
+    public void testFirstEmptyStreamReadsFromOtherStream() throws Exception {
+        org.easymock.EasyMock.expect(mockInputStream1.read()).andReturn(-1);
+        org.easymock.EasyMock.expect(mockInputStream2.read()).andReturn(500);
+
+        replayMocks();
+        int result = impl.read();
+        verifyMocks();
+
+        Assert.assertEquals(500, result);
+    }
+
+    @Test
+    public void testThatWeReadTheFirstInputStream() throws Exception {
+        org.easymock.EasyMock.expect(mockInputStream1.read()).andReturn(500);
+
+        replayMocks();
+        int result = impl.read();
+        verifyMocks();
+
+        Assert.assertEquals(500, result);
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mockInputStream1, mockInputStream2);
+    }
+
+    private void replayMocks() {
+        EasyMock.replay(mockInputStream1, mockInputStream2);
+    }
+
+}

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestConditionalRequestBuilder.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestConditionalRequestBuilder.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestConditionalRequestBuilder.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestConditionalRequestBuilder.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,116 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.util.Date;
+
+import org.apache.http.Header;
+import org.apache.http.HttpRequest;
+import org.apache.http.ProtocolException;
+import org.apache.http.client.cache.impl.CacheEntry;
+import org.apache.http.client.cache.impl.ConditionalRequestBuilder;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpRequest;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestConditionalRequestBuilder {
+
+    private ConditionalRequestBuilder impl;
+
+    @Before
+    public void setUp() throws Exception {
+        impl = new ConditionalRequestBuilder();
+    }
+
+    @Test
+    public void testBuildConditionalRequestWithLastModified() throws ProtocolException {
+        String theMethod = "GET";
+        String theUri = "/theuri";
+        String lastModified = "this is my last modified date";
+
+        HttpRequest request = new BasicHttpRequest(theMethod, theUri);
+        request.addHeader("Accept-Encoding", "gzip");
+
+        CacheEntry cacheEntry = new CacheEntry();
+        cacheEntry.setResponseHeaders(new Header[] {
+                new BasicHeader("Date", DateUtils.formatDate(new Date())),
+                new BasicHeader("Last-Modified", lastModified) });
+
+        HttpRequest newRequest = impl.buildConditionalRequest(request, cacheEntry);
+
+        Assert.assertNotSame(request, newRequest);
+
+        Assert.assertEquals(theMethod, newRequest.getRequestLine().getMethod());
+        Assert.assertEquals(theUri, newRequest.getRequestLine().getUri());
+        Assert.assertEquals(request.getRequestLine().getProtocolVersion(), newRequest
+                .getRequestLine().getProtocolVersion());
+        Assert.assertEquals(2, newRequest.getAllHeaders().length);
+
+        Assert.assertEquals("Accept-Encoding", newRequest.getAllHeaders()[0].getName());
+        Assert.assertEquals("gzip", newRequest.getAllHeaders()[0].getValue());
+
+        Assert.assertEquals("If-Modified-Since", newRequest.getAllHeaders()[1].getName());
+        Assert.assertEquals(lastModified, newRequest.getAllHeaders()[1].getValue());
+    }
+
+    @Test
+    public void testBuildConditionalRequestWithETag() throws ProtocolException {
+        String theMethod = "GET";
+        String theUri = "/theuri";
+        String theETag = "this is my eTag";
+
+        HttpRequest request = new BasicHttpRequest(theMethod, theUri);
+        request.addHeader("Accept-Encoding", "gzip");
+
+        CacheEntry cacheEntry = new CacheEntry();
+        cacheEntry.setResponseHeaders(new Header[] {
+                new BasicHeader("Date", DateUtils.formatDate(new Date())),
+                new BasicHeader("Last-Modified", DateUtils.formatDate(new Date())),
+                new BasicHeader("ETag", theETag) });
+
+        HttpRequest newRequest = impl.buildConditionalRequest(request, cacheEntry);
+
+        Assert.assertNotSame(request, newRequest);
+
+        Assert.assertEquals(theMethod, newRequest.getRequestLine().getMethod());
+        Assert.assertEquals(theUri, newRequest.getRequestLine().getUri());
+        Assert.assertEquals(request.getRequestLine().getProtocolVersion(), newRequest
+                .getRequestLine().getProtocolVersion());
+
+        Assert.assertEquals(2, newRequest.getAllHeaders().length);
+
+        Assert.assertEquals("Accept-Encoding", newRequest.getAllHeaders()[0].getName());
+        Assert.assertEquals("gzip", newRequest.getAllHeaders()[0].getValue());
+
+        Assert.assertEquals("If-None-Match", newRequest.getAllHeaders()[1].getName());
+        Assert.assertEquals(theETag, newRequest.getAllHeaders()[1].getValue());
+    }
+
+}

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestDefaultCacheEntrySerializer.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestDefaultCacheEntrySerializer.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestDefaultCacheEntrySerializer.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestDefaultCacheEntrySerializer.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,112 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.Date;
+
+import org.apache.http.Header;
+import org.apache.http.HttpVersion;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.cache.HttpCacheEntrySerializer;
+import org.apache.http.client.cache.impl.CacheEntry;
+import org.apache.http.client.cache.impl.DefaultCacheEntrySerializer;
+import org.apache.http.message.BasicHeader;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestDefaultCacheEntrySerializer {
+
+    @Test
+    public void testSerialization() throws Exception {
+
+        HttpCacheEntrySerializer<CacheEntry> serializer = new DefaultCacheEntrySerializer();
+
+        // write the entry
+        CacheEntry writeEntry = newCacheEntry();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        serializer.writeTo(writeEntry, out);
+
+        // read the entry
+        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        CacheEntry readEntry = serializer.readFrom(in);
+
+        // compare
+        Assert.assertTrue(areEqual(readEntry, writeEntry));
+
+    }
+
+    private CacheEntry newCacheEntry() {
+
+        CacheEntry cacheEntry = new CacheEntry();
+
+        Header[] headers = new Header[5];
+        for (int i = 0; i < headers.length; i++) {
+            headers[i] = new BasicHeader("header" + i, "value" + i);
+        }
+        ProtocolVersion version = new HttpVersion(1, 1);
+        String body = "Lorem ipsum dolor sit amet";
+
+        cacheEntry.setResponseHeaders(headers);
+        cacheEntry.setProtocolVersion(version);
+        cacheEntry.setRequestDate(new Date());
+        cacheEntry.setResponseDate(new Date());
+        cacheEntry.setBody(body.getBytes());
+
+        return cacheEntry;
+
+    }
+
+    private boolean areEqual(CacheEntry one, CacheEntry two) {
+
+        if (!one.getRequestDate().equals(two.getRequestDate()))
+            return false;
+        if (!one.getResponseDate().equals(two.getResponseDate()))
+            return false;
+        if (!one.getProtocolVersion().equals(two.getProtocolVersion()))
+            return false;
+        if (!Arrays.equals(one.getBody(), two.getBody()))
+            return false;
+
+        Header[] oneHeaders = one.getAllHeaders();
+        Header[] twoHeaders = one.getAllHeaders();
+        if (!(oneHeaders.length == twoHeaders.length))
+            return false;
+        for (int i = 0; i < oneHeaders.length; i++) {
+            if (!oneHeaders[i].getName().equals(twoHeaders[i].getName()))
+                return false;
+            if (!oneHeaders[i].getValue().equals(twoHeaders[i].getValue()))
+                return false;
+        }
+
+        return true;
+
+    }
+
+}

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolDeviations.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolDeviations.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolDeviations.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolDeviations.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,378 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.util.Date;
+import java.util.Random;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.cache.HttpCache;
+import org.apache.http.client.cache.impl.BasicHttpCache;
+import org.apache.http.client.cache.impl.CachingHttpClient;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.protocol.HttpContext;
+import org.easymock.Capture;
+import org.easymock.classextension.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
+ * of the rules for proxies apply to us, as far as proper operation of the
+ * requests that pass through us. Generally speaking, we want to make sure that
+ * any response returned from our HttpClient.execute() methods is conditionally
+ * compliant with the rules for an HTTP/1.1 server, and that any requests we
+ * pass downstream to the backend HttpClient are are conditionally compliant
+ * with the rules for an HTTP/1.1 client.
+ *
+ * There are some cases where strictly behaving as a compliant caching proxy
+ * would result in strange behavior, since we're attached as part of a client
+ * and are expected to be a drop-in replacement. The test cases captured here
+ * document the places where we differ from the HTTP RFC.
+ */
+public class TestProtocolDeviations {
+
+    private static ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP", 1, 1);
+
+    private static int MAX_BYTES = 1024;
+    private static int MAX_ENTRIES = 100;
+    private int entityLength = 128;
+
+    private HttpHost host;
+    private HttpEntity body;
+    private HttpEntity mockEntity;
+    private HttpClient mockBackend;
+    private HttpCache<CacheEntry> mockCache;
+    private HttpRequest request;
+    private HttpResponse originResponse;
+
+    private CachingHttpClient impl;
+
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setUp() {
+        host = new HttpHost("foo.example.com");
+
+        body = makeBody(entityLength);
+
+        request = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+        originResponse = make200Response();
+
+        HttpCache<CacheEntry> cache = new BasicHttpCache(MAX_ENTRIES);
+        mockBackend = EasyMock.createMock(HttpClient.class);
+        mockEntity = EasyMock.createMock(HttpEntity.class);
+        mockCache = EasyMock.createMock(HttpCache.class);
+        impl = new CachingHttpClient(mockBackend, cache, MAX_BYTES);
+    }
+
+    private HttpResponse make200Response() {
+        HttpResponse out = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
+        out.setHeader("Date", DateUtils.formatDate(new Date()));
+        out.setHeader("Server", "MockOrigin/1.0");
+        out.setEntity(makeBody(128));
+        return out;
+    }
+
+    private void replayMocks() {
+        EasyMock.replay(mockBackend);
+        EasyMock.replay(mockCache);
+        EasyMock.replay(mockEntity);
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mockBackend);
+        EasyMock.verify(mockCache);
+        EasyMock.verify(mockEntity);
+    }
+
+    private HttpEntity makeBody(int nbytes) {
+        byte[] bytes = new byte[nbytes];
+        (new Random()).nextBytes(bytes);
+        return new ByteArrayEntity(bytes);
+    }
+
+    public static HttpRequest eqRequest(HttpRequest in) {
+        org.easymock.EasyMock.reportMatcher(new RequestEquivalent(in));
+        return null;
+    }
+
+    /*
+     * "For compatibility with HTTP/1.0 applications, HTTP/1.1 requests
+     * containing a message-body MUST include a valid Content-Length header
+     * field unless the server is known to be HTTP/1.1 compliant. If a request
+     * contains a message-body and a Content-Length is not given, the server
+     * SHOULD respond with 400 (bad request) if it cannot determine the length
+     * of the message, or with 411 (length required) if it wishes to insist on
+     * receiving a valid Content-Length."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
+     */
+    @Test
+    public void testHTTP1_1RequestsWithBodiesOfKnownLengthMustHaveContentLength() throws Exception {
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+        post.setEntity(mockEntity);
+
+        replayMocks();
+
+        HttpResponse response = impl.execute(host, post);
+
+        verifyMocks();
+
+        Assert
+                .assertEquals(HttpStatus.SC_LENGTH_REQUIRED, response.getStatusLine()
+                        .getStatusCode());
+    }
+
+    /*
+     * Discussion: if an incoming request has a body, but the HttpEntity
+     * attached has an unknown length (meaning entity.getContentLength() is
+     * negative), we have two choices if we want to be conditionally compliant.
+     * (1) we can slurp the whole body into a bytearray and compute its length
+     * before sending; or (2) we can push responsibility for (1) back onto the
+     * client by just generating a 411 response
+     *
+     * There is a third option, which is that we delegate responsibility for (1)
+     * onto the backend HttpClient, but because that is an injected dependency,
+     * we can't rely on it necessarily being conditionally compliant with
+     * HTTP/1.1. Currently, option (2) seems like the safest bet, as this
+     * exposes to the client application that the slurping required for (1)
+     * needs to happen in order to compute the content length.
+     *
+     * In any event, this test just captures the behavior required.
+     */
+    @Test
+    public void testHTTP1_1RequestsWithUnknownBodyLengthAreRejectedOrHaveContentLengthAdded()
+            throws Exception {
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+
+        byte[] bytes = new byte[128];
+        (new Random()).nextBytes(bytes);
+
+        HttpEntity mockBody = EasyMock.createMockBuilder(ByteArrayEntity.class).withConstructor(
+                new Object[] { bytes }).addMockedMethods("getContentLength").createMock();
+        org.easymock.EasyMock.expect(mockBody.getContentLength()).andReturn(-1L).anyTimes();
+        post.setEntity(mockBody);
+
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.eq(host), org.easymock.EasyMock
+                        .capture(reqCap), (HttpContext) org.easymock.EasyMock.isNull())).andReturn(
+                originResponse).times(0, 1);
+
+        replayMocks();
+        EasyMock.replay(mockBody);
+
+        HttpResponse result = impl.execute(host, post);
+
+        verifyMocks();
+        EasyMock.verify(mockBody);
+
+        if (reqCap.hasCaptured()) {
+            // backend request was made
+            HttpRequest forwarded = reqCap.getValue();
+            Assert.assertNotNull(forwarded.getFirstHeader("Content-Length"));
+        } else {
+            int status = result.getStatusLine().getStatusCode();
+            Assert.assertTrue(HttpStatus.SC_LENGTH_REQUIRED == status
+                    || HttpStatus.SC_BAD_REQUEST == status);
+        }
+    }
+
+    /*
+     * "If the OPTIONS request includes an entity-body (as indicated by the
+     * presence of Content-Length or Transfer-Encoding), then the media type
+     * MUST be indicated by a Content-Type field."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+     */
+    @Test
+    public void testOPTIONSRequestsWithBodiesAndNoContentTypeHaveOneSupplied() throws Exception {
+        BasicHttpEntityEnclosingRequest options = new BasicHttpEntityEnclosingRequest("OPTIONS",
+                "/", HTTP_1_1);
+        options.setEntity(body);
+        options.setHeader("Content-Length", "1");
+
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.eq(host), org.easymock.EasyMock
+                        .capture(reqCap), (HttpContext) org.easymock.EasyMock.isNull())).andReturn(
+                originResponse);
+        replayMocks();
+
+        impl.execute(host, options);
+
+        verifyMocks();
+
+        HttpRequest forwarded = reqCap.getValue();
+        Assert.assertTrue(forwarded instanceof HttpEntityEnclosingRequest);
+        HttpEntityEnclosingRequest reqWithBody = (HttpEntityEnclosingRequest) forwarded;
+        HttpEntity reqBody = reqWithBody.getEntity();
+        Assert.assertNotNull(reqBody);
+        Assert.assertNotNull(reqBody.getContentType());
+    }
+
+    /*
+     * "10.2.7 206 Partial Content ... The request MUST have included a Range
+     * header field (section 14.35) indicating the desired range, and MAY have
+     * included an If-Range header field (section 14.27) to make the request
+     * conditional."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+     */
+    @Test
+    public void testPartialContentIsNotReturnedToAClientThatDidNotAskForIt() throws Exception {
+
+        // tester's note: I don't know what the cache will *do* in
+        // this situation, but it better not just pass the response
+        // on.
+        request.removeHeaders("Range");
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+                "Partial Content");
+        originResponse.setHeader("Content-Range", "bytes 0-499/1234");
+        originResponse.setEntity(makeBody(500));
+
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.isA(HttpHost.class),
+                        org.easymock.EasyMock.isA(HttpRequest.class),
+                        (HttpContext) org.easymock.EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        try {
+            HttpResponse result = impl.execute(host, request);
+            Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT != result.getStatusLine()
+                    .getStatusCode());
+        } catch (ClientProtocolException acceptableBehavior) {
+            // this is probably ok
+        }
+    }
+
+    /*
+     * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
+     * header field (section 14.47) containing a challenge applicable to the
+     * requested resource."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
+     */
+    @Test(expected = ClientProtocolException.class)
+    public void testCantReturnOrigin401ResponseWithoutWWWAuthenticateHeader() throws Exception {
+
+        originResponse = new BasicHttpResponse(HTTP_1_1, 401, "Unauthorized");
+
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.isA(HttpHost.class),
+                        org.easymock.EasyMock.isA(HttpRequest.class),
+                        (HttpContext) org.easymock.EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        // this is another case where we are caught in a sticky
+        // situation, where the origin was not 1.1-compliant.
+        try {
+            impl.execute(host, request);
+        } catch (ClientProtocolException possiblyAcceptableBehavior) {
+            verifyMocks();
+            throw possiblyAcceptableBehavior;
+        }
+    }
+
+    /*
+     * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
+     * header containing a list of valid methods for the requested resource.
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
+     */
+    @Test(expected = ClientProtocolException.class)
+    public void testCantReturnAnOrigin405WithoutAllowHeader() throws Exception {
+        originResponse = new BasicHttpResponse(HTTP_1_1, 405, "Method Not Allowed");
+
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.isA(HttpHost.class),
+                        org.easymock.EasyMock.isA(HttpRequest.class),
+                        (HttpContext) org.easymock.EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        // this is another case where we are caught in a sticky
+        // situation, where the origin was not 1.1-compliant.
+        try {
+            impl.execute(host, request);
+        } catch (ClientProtocolException possiblyAcceptableBehavior) {
+            verifyMocks();
+            throw possiblyAcceptableBehavior;
+        }
+    }
+
+    /*
+     * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
+     * Proxy-Authenticate header field (section 14.33) containing a challenge
+     * applicable to the proxy for the requested resource."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
+     */
+    @Test
+    public void testCantReturnA407WithoutAProxyAuthenticateHeader() throws Exception {
+        originResponse = new BasicHttpResponse(HTTP_1_1, 407, "Proxy Authentication Required");
+
+        org.easymock.EasyMock.expect(
+                mockBackend.execute(org.easymock.EasyMock.isA(HttpHost.class),
+                        org.easymock.EasyMock.isA(HttpRequest.class),
+                        (HttpContext) org.easymock.EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        boolean gotException = false;
+        // this is another case where we are caught in a sticky
+        // situation, where the origin was not 1.1-compliant.
+        try {
+            HttpResponse result = impl.execute(host, request);
+            Assert.fail("should have gotten ClientProtocolException");
+
+            if (result.getStatusLine().getStatusCode() == 407) {
+                Assert.assertNotNull(result.getFirstHeader("Proxy-Authentication"));
+            }
+        } catch (ClientProtocolException possiblyAcceptableBehavior) {
+            gotException = true;
+        }
+
+        verifyMocks();
+        Assert.assertTrue(gotException);
+    }
+
+}
\ No newline at end of file



Mime
View raw message