You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2010/04/30 23:00:10 UTC
svn commit: r939814 [5/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...
Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,3147 @@
+/*
+ * ====================================================================
+ * 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.IOException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.Random;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+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.CacheEntry;
+import org.apache.http.client.cache.impl.CachingHttpClient;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.RequestWrapper;
+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.IExpectationSetters;
+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.
+ */
+public class TestProtocolRequirements {
+
+ 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 void replayMocks() {
+ EasyMock.replay(mockBackend);
+ EasyMock.replay(mockCache);
+ EasyMock.replay(mockEntity);
+ }
+
+ private void verifyMocks() {
+ EasyMock.verify(mockBackend);
+ EasyMock.verify(mockCache);
+ EasyMock.verify(mockEntity);
+ }
+
+ 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.setHeader("Content-Length", "128");
+ out.setEntity(makeBody(128));
+ return out;
+ }
+
+ private HttpEntity makeBody(int nbytes) {
+ byte[] bytes = new byte[nbytes];
+ (new Random()).nextBytes(bytes);
+ return new ByteArrayEntity(bytes);
+ }
+
+ private IExpectationSetters<HttpResponse> backendExpectsAnyRequest() throws Exception {
+ HttpResponse resp = mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
+ .isA(HttpRequest.class), (HttpContext) EasyMock.isNull());
+ return EasyMock.expect(resp);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void emptyMockCacheExpectsNoPuts() throws Exception {
+ mockBackend = EasyMock.createMock(HttpClient.class);
+ mockCache = EasyMock.createMock(HttpCache.class);
+ mockEntity = EasyMock.createMock(HttpEntity.class);
+
+ impl = new CachingHttpClient(mockBackend, mockCache, MAX_BYTES);
+
+ EasyMock.expect(mockCache.getEntry((String) EasyMock.anyObject())).andReturn(null)
+ .anyTimes();
+
+ mockCache.removeEntry(EasyMock.isA(String.class));
+ EasyMock.expectLastCall().anyTimes();
+ }
+
+ public static HttpRequest eqRequest(HttpRequest in) {
+ EasyMock.reportMatcher(new RequestEquivalent(in));
+ return null;
+ }
+
+ @Test
+ public void testCacheMissOnGETUsesOriginResponse() throws Exception {
+ EasyMock.expect(mockBackend.execute(host, request, (HttpContext) null)).andReturn(
+ originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
+ }
+
+ /*
+ * "Proxy and gateway applications need to be careful when forwarding
+ * messages in protocol versions different from that of the application.
+ * Since the protocol version indicates the protocol capability of the
+ * sender, a proxy/gateway MUST NOT send a message with a version indicator
+ * which is greater than its actual version. If a higher version request is
+ * received, the proxy/gateway MUST either downgrade the request version, or
+ * respond with an error, or switch to tunnel behavior."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
+ */
+ @Test
+ public void testHigherMajorProtocolVersionsOnRequestSwitchToTunnelBehavior() throws Exception {
+
+ // tunnel behavior: I don't muck with request or response in
+ // any way
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 2, 13));
+
+ EasyMock.expect(mockBackend.execute(host, request, (HttpContext) null)).andReturn(
+ originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertSame(originResponse, result);
+ }
+
+ @Test
+ public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
+
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2));
+
+ HttpRequest downgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(downgraded),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
+ }
+
+ /*
+ * "Due to interoperability problems with HTTP/1.0 proxies discovered since
+ * the publication of RFC 2068[33], caching proxies MUST, gateways MAY, and
+ * tunnels MUST NOT upgrade the request to the highest version they support.
+ * The proxy/gateway's response to that request MUST be in the same major
+ * version as the request."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
+ */
+ @Test
+ public void testRequestsWithLowerProtocolVersionsGetUpgradedTo1_1() throws Exception {
+
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+ HttpRequest upgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(upgraded), (HttpContext) EasyMock
+ .isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
+ }
+
+ /*
+ * "An HTTP server SHOULD send a response version equal to the highest
+ * version for which the server is at least conditionally compliant, and
+ * whose major version is less than or equal to the one received in the
+ * request."
+ *
+ * http://www.ietf.org/rfc/rfc2145.txt
+ */
+ @Test
+ public void testLowerOriginResponsesUpgradedToOurVersion1_1() throws Exception {
+ originResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 2), HttpStatus.SC_OK,
+ "OK");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockOrigin/1.0");
+ originResponse.setEntity(body);
+
+ // not testing this internal behavior in this test, just want
+ // to check the protocol version that comes out the other end
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertEquals(HTTP_1_1, result.getProtocolVersion());
+ }
+
+ @Test
+ public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertEquals(HTTP_1_1, result.getProtocolVersion());
+ }
+
+ /*
+ * "A proxy MUST forward an unknown header, unless it is protected by a
+ * Connection header." http://www.ietf.org/rfc/rfc2145.txt
+ */
+ @Test
+ public void testForwardsUnknownHeadersOnRequestsFromHigherProtocolVersions() throws Exception {
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2));
+ request.removeHeaders("Connection");
+ request.addHeader("X-Unknown-Header", "some-value");
+
+ HttpRequest downgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+ downgraded.removeHeaders("Connection");
+ downgraded.addHeader("X-Unknown-Header", "some-value");
+
+ RequestWrapper downgradedWrapper = new RequestWrapper(downgraded);
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), eqRequest(downgradedWrapper),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+ }
+
+ /*
+ * "A server MUST NOT send transfer-codings to an HTTP/1.0 client."
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
+ */
+ @Test
+ public void testTransferCodingsAreNotSentToAnHTTP_1_0Client() throws Exception {
+
+ originResponse.setHeader("Transfer-Encoding", "identity");
+
+ request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertNull(result.getFirstHeader("TE"));
+ Assert.assertNull(result.getFirstHeader("Transfer-Encoding"));
+ }
+
+ /*
+ * "Multiple message-header fields with the same field-name MAY be present
+ * in a message if and only if the entire field-value for that header field
+ * is defined as a comma-separated list [i.e., #(values)]. It MUST be
+ * possible to combine the multiple header fields into one
+ * "field-name: field-value" pair, without changing the semantics of the
+ * message, by appending each subsequent field-value to the first, each
+ * separated by a comma. The order in which header fields with the same
+ * field-name are received is therefore significant to the interpretation of
+ * the combined field value, and thus a proxy MUST NOT change the order of
+ * these field values when a message is forwarded."
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+ */
+ private void testOrderOfMultipleHeadersIsPreservedOnRequests(String h, HttpRequest request)
+ throws Exception {
+ Capture<HttpRequest> reqCapture = new Capture<HttpRequest>();
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.capture(reqCapture),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+
+ HttpRequest forwarded = reqCapture.getValue();
+ Assert.assertNotNull(forwarded);
+ Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(request, h), HttpTestUtils
+ .getCanonicalHeaderValue(forwarded, h));
+
+ }
+
+ @Test
+ public void testOrderOfMultipleAcceptHeaderValuesIsPreservedOnRequests() throws Exception {
+ request.addHeader("Accept", "audio/*; q=0.2, audio/basic");
+ request.addHeader("Accept", "text/*, text/html, text/html;level=1, */*");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Accept", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleAcceptCharsetHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Accept-Charset", "iso-8859-5");
+ request.addHeader("Accept-Charset", "unicode-1-1;q=0.8");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Charset", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleAcceptEncodingHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Accept-Encoding", "identity");
+ request.addHeader("Accept-Encoding", "compress, gzip");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleAcceptLanguageHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7");
+ request.addHeader("Accept-Language", "i-cherokee");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleAllowHeadersIsPreservedOnRequests() throws Exception {
+ BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
+ HTTP_1_1);
+ put.setEntity(body);
+ put.addHeader("Allow", "GET, HEAD");
+ put.addHeader("Allow", "DELETE");
+ put.addHeader("Content-Length", "128");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Allow", put);
+ }
+
+ @Test
+ public void testOrderOfMultipleCacheControlHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Cache-Control", "max-age=5");
+ request.addHeader("Cache-Control", "min-fresh=10");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Cache-Control", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnRequests() throws Exception {
+ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+ HTTP_1_1);
+ post.setEntity(body);
+ post.addHeader("Content-Encoding", "gzip");
+ post.addHeader("Content-Encoding", "compress");
+ post.addHeader("Content-Length", "128");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Encoding", post);
+ }
+
+ @Test
+ public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnRequests() throws Exception {
+ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+ HTTP_1_1);
+ post.setEntity(body);
+ post.addHeader("Content-Language", "mi");
+ post.addHeader("Content-Language", "en");
+ post.addHeader("Content-Length", "128");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Language", post);
+ }
+
+ @Test
+ public void testOrderOfMultipleExpectHeadersIsPreservedOnRequests() throws Exception {
+ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+ HTTP_1_1);
+ post.setEntity(body);
+ post.addHeader("Expect", "100-continue");
+ post.addHeader("Expect", "x-expect=true");
+ post.addHeader("Content-Length", "128");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Expect", post);
+ }
+
+ @Test
+ public void testOrderOfMultiplePragmaHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Pragma", "no-cache");
+ request.addHeader("Pragma", "x-pragma-1, x-pragma-2");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Pragma", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
+ request.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Via", request);
+ }
+
+ @Test
+ public void testOrderOfMultipleWarningHeadersIsPreservedOnRequests() throws Exception {
+ request.addHeader("Warning", "199 fred \"bargle\"");
+ request.addHeader("Warning", "199 barney \"bungle\"");
+ testOrderOfMultipleHeadersIsPreservedOnRequests("Warning", request);
+ }
+
+ private void testOrderOfMultipleHeadersIsPreservedOnResponses(String h) throws Exception {
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
+ .getCanonicalHeaderValue(result, h));
+
+ }
+
+ @Test
+ public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
+ originResponse = new BasicHttpResponse(HTTP_1_1, 405, "Method Not Allowed");
+ originResponse.addHeader("Allow", "HEAD");
+ originResponse.addHeader("Allow", "DELETE");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
+ }
+
+ @Test
+ public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("Cache-Control", "max-age=0");
+ originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
+ }
+
+ @Test
+ public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("Content-Encoding", "gzip");
+ originResponse.addHeader("Content-Encoding", "compress");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
+ }
+
+ @Test
+ public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("Content-Language", "mi");
+ originResponse.addHeader("Content-Language", "en");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
+ }
+
+ @Test
+ public void testOrderOfMultiplePragmaHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("Pragma", "no-cache, x-pragma-2");
+ originResponse.addHeader("Pragma", "x-pragma-1");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
+ }
+
+ @Test
+ public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
+ originResponse.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
+ }
+
+ @Test
+ public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
+ originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
+ originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
+ testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
+ }
+
+ /*
+ * "However, applications MUST understand the class of any status code, as
+ * indicated by the first digit, and treat any unrecognized response as
+ * being equivalent to the x00 status code of that class, with the exception
+ * that an unrecognized response MUST NOT be cached."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
+ */
+ private void testUnknownResponseStatusCodeIsNotCached(int code) throws Exception {
+
+ emptyMockCacheExpectsNoPuts();
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, code, "Moo");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockOrigin/1.0");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setEntity(body);
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, request);
+
+ // in particular, there were no storage calls on the cache
+ verifyMocks();
+ }
+
+ @Test
+ public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
+ for (int i = 102; i <= 199; i++) {
+ testUnknownResponseStatusCodeIsNotCached(i);
+ }
+ for (int i = 207; i <= 299; i++) {
+ testUnknownResponseStatusCodeIsNotCached(i);
+ }
+ for (int i = 308; i <= 399; i++) {
+ testUnknownResponseStatusCodeIsNotCached(i);
+ }
+ for (int i = 418; i <= 499; i++) {
+ testUnknownResponseStatusCodeIsNotCached(i);
+ }
+ for (int i = 506; i <= 999; i++) {
+ testUnknownResponseStatusCodeIsNotCached(i);
+ }
+ }
+
+ /*
+ * "Unrecognized header fields SHOULD be ignored by the recipient and MUST
+ * be forwarded by transparent proxies."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
+ */
+ @Test
+ public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
+ request.addHeader("X-Unknown-Header", "blahblah");
+ Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.anyObject())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+ HttpRequest forwarded = reqCap.getValue();
+ Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
+ Assert.assertEquals(1, hdrs.length);
+ Assert.assertEquals("blahblah", hdrs[0].getValue());
+ }
+
+ @Test
+ public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
+ originResponse.addHeader("X-Unknown-Header", "blahblah");
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Header[] hdrs = result.getHeaders("X-Unknown-Header");
+ Assert.assertEquals(1, hdrs.length);
+ Assert.assertEquals("blahblah", hdrs[0].getValue());
+ }
+
+ /*
+ * "If a client will wait for a 100 (Continue) response before sending the
+ * request body, it MUST send an Expect request-header field (section 14.20)
+ * with the '100-continue' expectation."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+ */
+ @Test
+ public void testRequestsExpecting100ContinueBehaviorShouldSetExpectHeader() throws Exception {
+ BasicHttpEntityEnclosingRequest post = EasyMock.createMockBuilder(
+ BasicHttpEntityEnclosingRequest.class).withConstructor("POST", "/", HTTP_1_1)
+ .addMockedMethods("expectContinue").createMock();
+ post.setEntity(mockEntity);
+ post.setHeader("Content-Length", "128");
+
+ Capture<HttpEntityEnclosingRequest> reqCap = new Capture<HttpEntityEnclosingRequest>();
+
+ EasyMock.expect(post.expectContinue()).andReturn(true).anyTimes();
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ EasyMock.replay(post);
+
+ impl.execute(host, post);
+
+ verifyMocks();
+ EasyMock.verify(post);
+
+ HttpEntityEnclosingRequest forwarded = reqCap.getValue();
+ Assert.assertTrue(forwarded.expectContinue());
+ boolean foundExpect = false;
+ for (Header h : forwarded.getHeaders("Expect")) {
+ for (HeaderElement elt : h.getElements()) {
+ if ("100-continue".equalsIgnoreCase(elt.getName())) {
+ foundExpect = true;
+ break;
+ }
+ }
+ }
+ Assert.assertTrue(foundExpect);
+ }
+
+ /*
+ * "If a client will wait for a 100 (Continue) response before sending the
+ * request body, it MUST send an Expect request-header field (section 14.20)
+ * with the '100-continue' expectation."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+ */
+ @Test
+ public void testRequestsNotExpecting100ContinueBehaviorShouldNotSetExpectContinueHeader()
+ throws Exception {
+ BasicHttpEntityEnclosingRequest post = EasyMock.createMockBuilder(
+ BasicHttpEntityEnclosingRequest.class).withConstructor("POST", "/", HTTP_1_1)
+ .addMockedMethods("expectContinue").createMock();
+ post.setEntity(mockEntity);
+ post.setHeader("Content-Length", "128");
+
+ Capture<HttpEntityEnclosingRequest> reqCap = new Capture<HttpEntityEnclosingRequest>();
+
+ EasyMock.expect(post.expectContinue()).andReturn(false).anyTimes();
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ EasyMock.replay(post);
+
+ impl.execute(host, post);
+
+ verifyMocks();
+ EasyMock.verify(post);
+
+ HttpEntityEnclosingRequest forwarded = reqCap.getValue();
+ Assert.assertFalse(forwarded.expectContinue());
+ boolean foundExpect = false;
+ for (Header h : forwarded.getHeaders("Expect")) {
+ for (HeaderElement elt : h.getElements()) {
+ if ("100-continue".equalsIgnoreCase(elt.getName())) {
+ foundExpect = true;
+ break;
+ }
+ }
+ }
+ Assert.assertFalse(foundExpect);
+ }
+
+ /*
+ * "A client MUST NOT send an Expect request-header field (section 14.20)
+ * with the '100-continue' expectation if it does not intend to send a
+ * request body."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+ */
+ @Test
+ public void testExpect100ContinueIsNotSentIfThereIsNoRequestBody() throws Exception {
+ request.addHeader("Expect", "100-continue");
+ Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+ HttpRequest forwarded = reqCap.getValue();
+ boolean foundExpectContinue = false;
+
+ for (Header h : forwarded.getHeaders("Expect")) {
+ for (HeaderElement elt : h.getElements()) {
+ if ("100-continue".equalsIgnoreCase(elt.getName())) {
+ foundExpectContinue = true;
+ break;
+ }
+ }
+ }
+ Assert.assertFalse(foundExpectContinue);
+ }
+
+ /*
+ * "If a proxy receives a request that includes an Expect request- header
+ * field with the '100-continue' expectation, and the proxy either knows
+ * that the next-hop server complies with HTTP/1.1 or higher, or does not
+ * know the HTTP version of the next-hop server, it MUST forward the
+ * request, including the Expect header field.
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+ */
+ @Test
+ public void testExpectHeadersAreForwardedOnRequests() throws Exception {
+ // This would mostly apply to us if we were part of an
+ // application that was a proxy, and would be the
+ // responsibility of the greater application. Our
+ // responsibility is to make sure that if we get an
+ // entity-enclosing request that we properly set (or unset)
+ // the Expect header per the request.expectContinue() flag,
+ // which is tested by the previous few tests.
+ }
+
+ /*
+ * "A proxy MUST NOT forward a 100 (Continue) response if the request
+ * message was received from an HTTP/1.0 (or earlier) client and did not
+ * include an Expect request-header field with the '100-continue'
+ * expectation. This requirement overrides the general rule for forwarding
+ * of 1xx responses (see section 10.1)."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+ */
+ @Test
+ public void test100ContinueResponsesAreNotForwardedTo1_0ClientsWhoDidNotAskForThem()
+ throws Exception {
+
+ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+ new ProtocolVersion("HTTP", 1, 0));
+ post.setEntity(body);
+ post.setHeader("Content-Length", "128");
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, 100, "Continue");
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ try {
+ // if a 100 response gets up to us from the HttpClient
+ // backend, we can't really handle it at that point
+ impl.execute(host, post);
+ Assert.fail("should have thrown an exception");
+ } catch (ClientProtocolException expected) {
+ }
+
+ verifyMocks();
+ }
+
+ /*
+ * "9.2 OPTIONS. ...Responses to this method are not cacheable.
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+ */
+ @Test
+ public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+ request = new BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+ }
+
+ /*
+ * "A 200 response SHOULD .... If no response body is included, the response
+ * MUST include a Content-Length field with a field-value of '0'."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+ */
+ @Test
+ public void test200ResponseToOPTIONSWithNoBodyShouldIncludeContentLengthZero() throws Exception {
+
+ request = new BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+ originResponse.setEntity(null);
+ originResponse.setHeader("Content-Length", "0");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Header contentLength = result.getFirstHeader("Content-Length");
+ Assert.assertNotNull(contentLength);
+ Assert.assertEquals("0", contentLength.getValue());
+ }
+
+ /*
+ * "When a proxy receives an OPTIONS request on an absoluteURI for which
+ * request forwarding is permitted, the proxy MUST check for a Max-Forwards
+ * field. If the Max-Forwards field-value is zero ("0"), the proxy MUST NOT
+ * forward the message; instead, the proxy SHOULD respond with its own
+ * communication options."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+ */
+ @Test
+ public void testDoesNotForwardOPTIONSWhenMaxForwardsIsZeroOnAbsoluteURIRequest()
+ throws Exception {
+ request = new BasicHttpRequest("OPTIONS", "*", HTTP_1_1);
+ request.setHeader("Max-Forwards", "0");
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+ }
+
+ /*
+ * "If the Max-Forwards field-value is an integer greater than zero, the
+ * proxy MUST decrement the field-value when it forwards the request."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+ */
+ @Test
+ public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
+
+ request = new BasicHttpRequest("OPTIONS", "*", HTTP_1_1);
+ request.setHeader("Max-Forwards", "7");
+
+ Capture<HttpRequest> cap = new Capture<HttpRequest>();
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(cap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+
+ HttpRequest captured = cap.getValue();
+ Assert.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
+ }
+
+ /*
+ * "If no Max-Forwards field is present in the request, then the forwarded
+ * request MUST NOT include a Max-Forwards field."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+ */
+ @Test
+ public void testDoesNotAddAMaxForwardsHeaderToForwardedOPTIONSRequests() throws Exception {
+ request = new BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+ Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+
+ HttpRequest forwarded = reqCap.getValue();
+ Assert.assertNull(forwarded.getFirstHeader("Max-Forwards"));
+ }
+
+ /*
+ * "The HEAD method is identical to GET except that the server MUST NOT
+ * return a message-body in the response."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
+ */
+ @Test
+ public void testResponseToAHEADRequestMustNotHaveABody() throws Exception {
+ request = new BasicHttpRequest("HEAD", "/", HTTP_1_1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+ }
+
+ /*
+ * "If the new field values indicate that the cached entity differs from the
+ * current entity (as would be indicated by a change in Content-Length,
+ * Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache
+ * entry as stale."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
+ */
+ private void testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale(String eHeader,
+ String oldVal, String newVal) throws Exception {
+
+ // put something cacheable in the cache
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.addHeader("Cache-Control", "max-age=3600");
+ resp1.setHeader(eHeader, oldVal);
+
+ // get a head that penetrates the cache
+ HttpRequest req2 = new BasicHttpRequest("HEAD", "/", HTTP_1_1);
+ req2.addHeader("Cache-Control", "no-cache");
+ HttpResponse resp2 = make200Response();
+ resp2.setEntity(null);
+ resp2.setHeader(eHeader, newVal);
+
+ // next request doesn't tolerate stale entry
+ HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req3.addHeader("Cache-Control", "max-stale=0");
+ HttpResponse resp3 = make200Response();
+ resp3.setHeader(eHeader, newVal);
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(req1), (HttpContext) EasyMock
+ .isNull())).andReturn(resp1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(req2), (HttpContext) EasyMock
+ .isNull())).andReturn(resp2);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp3);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+ impl.execute(host, req3);
+
+ verifyMocks();
+ }
+
+ @Test
+ public void testHEADResponseWithUpdatedContentLengthFieldMakeACacheEntryStale()
+ throws Exception {
+ testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-Length", "128", "127");
+ }
+
+ @Test
+ public void testHEADResponseWithUpdatedContentMD5FieldMakeACacheEntryStale() throws Exception {
+ testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-MD5",
+ "Q2hlY2sgSW50ZWdyaXR5IQ==", "Q2hlY2sgSW50ZWdyaXR5IR==");
+
+ }
+
+ @Test
+ public void testHEADResponseWithUpdatedETagFieldMakeACacheEntryStale() throws Exception {
+ testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("ETag", "\"etag1\"",
+ "\"etag2\"");
+ }
+
+ @Test
+ public void testHEADResponseWithUpdatedLastModifiedFieldMakeACacheEntryStale() throws Exception {
+ Date now = new Date();
+ Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+ Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L);
+ testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils
+ .formatDate(tenSecondsAgo), DateUtils.formatDate(sixSecondsAgo));
+ }
+
+ /*
+ * "9.5 POST. Responses to this method are not cacheable, unless the
+ * response includes appropriate Cache-Control or Expires header fields."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
+ */
+ @Test
+ public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+
+ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+ HTTP_1_1);
+ post.setHeader("Content-Length", "128");
+ post.setEntity(makeBody(128));
+
+ originResponse.removeHeaders("Cache-Control");
+ originResponse.removeHeaders("Expires");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, post);
+
+ verifyMocks();
+ }
+
+ /*
+ * "9.5 PUT. ...Responses to this method are not cacheable."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
+ */
+ @Test
+ public void testResponsesToPUTsAreNotCached() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+
+ BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
+ HTTP_1_1);
+ put.setEntity(makeBody(128));
+ put.addHeader("Content-Length", "128");
+
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, put);
+
+ verifyMocks();
+ }
+
+ /*
+ * "9.6 DELETE. ... Responses to this method are not cacheable."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
+ */
+ @Test
+ public void testResponsesToDELETEsAreNotCached() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+
+ request = new BasicHttpRequest("DELETE", "/", HTTP_1_1);
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+ }
+
+ /*
+ * "A TRACE request MUST NOT include an entity."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
+ */
+ @Test
+ public void testForwardedTRACERequestsDoNotIncludeAnEntity() throws Exception {
+ BasicHttpEntityEnclosingRequest trace = new BasicHttpEntityEnclosingRequest("TRACE", "/",
+ HTTP_1_1);
+ trace.setEntity(makeBody(entityLength));
+ trace.setHeader("Content-Length", Integer.toString(entityLength));
+
+ Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ impl.execute(host, trace);
+ verifyMocks();
+
+ HttpRequest forwarded = reqCap.getValue();
+ if (forwarded instanceof HttpEntityEnclosingRequest) {
+ HttpEntityEnclosingRequest bodyReq = (HttpEntityEnclosingRequest) forwarded;
+ Assert.assertTrue(bodyReq.getEntity() == null
+ || bodyReq.getEntity().getContentLength() == 0);
+ } else {
+ // request didn't enclose an entity
+ }
+ }
+
+ /*
+ * "9.8 TRACE ... Responses to this method MUST NOT be cached."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
+ */
+ @Test
+ public void testResponsesToTRACEsAreNotCached() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+
+ request = new BasicHttpRequest("TRACE", "/", HTTP_1_1);
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ impl.execute(host, request);
+
+ verifyMocks();
+ }
+
+ /*
+ * "The 204 response MUST NOT include a message-body, and thus is always
+ * terminated by the first empty line after the header fields."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
+ */
+ @Test
+ public void test204ResponsesDoNotContainMessageBodies() throws Exception {
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
+ originResponse.setEntity(makeBody(entityLength));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+ }
+
+ /*
+ * "10.2.6 205 Reset Content ... The response MUST NOT include an entity."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.6
+ */
+ @Test
+ public void test205ResponsesDoNotContainMessageBodies() throws Exception {
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_RESET_CONTENT,
+ "Reset Content");
+ originResponse.setEntity(makeBody(entityLength));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+ }
+
+ /*
+ * "The [206] response MUST include the following header fields:
+ *
+ * - Either a Content-Range header field (section 14.16) indicating the
+ * range included with this response, or a multipart/byteranges Content-Type
+ * including Content-Range fields for each part. If a Content-Length header
+ * field is present in the response, its value MUST match the actual number
+ * of OCTETs transmitted in the message-body.
+ *
+ * - Date
+ *
+ * - ETag and/or Content-Location, if the header would have been sent in a
+ * 200 response to the same request
+ *
+ * - Expires, Cache-Control, and/or Vary, if the field-value might differ
+ * from that sent in any previous response for the same variant"
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+ */
+ @Test
+ public void test206ResponseGeneratedFromCacheMustHaveContentRangeOrMultipartByteRangesContentType()
+ throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "\"etag\"");
+ resp1.setHeader("Cache-Control", "max-age=3600");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+ replayMocks();
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+ verifyMocks();
+
+ if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+ if (result.getFirstHeader("Content-Range") == null) {
+ HeaderElement elt = result.getFirstHeader("Content-Type").getElements()[0];
+ Assert.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
+ Assert.assertNotNull(elt.getParameterByName("boundary"));
+ Assert.assertNotNull(elt.getParameterByName("boundary").getValue());
+ Assert.assertFalse("".equals(elt.getParameterByName("boundary").getValue().trim()));
+ }
+ }
+ }
+
+ @Test
+ public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent()
+ throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "\"etag\"");
+ resp1.setHeader("Cache-Control", "max-age=3600");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+ replayMocks();
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+ verifyMocks();
+
+ if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+ Header h = result.getFirstHeader("Content-Length");
+ if (h != null) {
+ int contentLength = Integer.parseInt(h.getValue());
+ int bytesRead = 0;
+ InputStream i = result.getEntity().getContent();
+ while ((i.read()) != -1) {
+ bytesRead++;
+ }
+ i.close();
+ Assert.assertEquals(contentLength, bytesRead);
+ }
+ }
+ }
+
+ @Test
+ public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "\"etag\"");
+ resp1.setHeader("Cache-Control", "max-age=3600");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+ replayMocks();
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+ verifyMocks();
+
+ if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+ Assert.assertNotNull(result.getFirstHeader("Date"));
+ }
+ }
+
+ @Test
+ public void test206ResponseReturnedToClientMustHaveDateHeader() throws Exception {
+ request.addHeader("Range", "bytes=0-50");
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+ "Partial Content");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockOrigin/1.0");
+ originResponse.setEntity(makeBody(500));
+ originResponse.setHeader("Content-Range", "bytes 0-499/1234");
+ originResponse.removeHeaders("Date");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+ Assert.assertTrue(result.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT
+ || result.getFirstHeader("Date") != null);
+
+ verifyMocks();
+ }
+
+ @Test
+ public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+ originResponse.addHeader("ETag", "\"etag1\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.addHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+ Assert.assertNotNull(result.getFirstHeader("ETag"));
+ }
+ }
+
+ @Test
+ public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+ originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.addHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+ Assert.assertNotNull(result.getFirstHeader("Content-Location"));
+ }
+ }
+
+ @Test
+ public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req1.addHeader("Accept-Encoding", "gzip");
+
+ Date now = new Date();
+ Date inOneHour = new Date(now.getTime() + 3600 * 1000L);
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+ originResponse.addHeader("Expires", DateUtils.formatDate(inOneHour));
+ originResponse.addHeader("Vary", "Accept-Encoding");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.addHeader("Cache-Control", "no-cache");
+ req2.addHeader("Accept-Encoding", "gzip");
+ Date nextSecond = new Date(now.getTime() + 1000L);
+ Date inTwoHoursPlusASec = new Date(now.getTime() + 2 * 3600 * 1000L + 1000L);
+
+ HttpResponse originResponse2 = make200Response();
+ originResponse2.setHeader("Date", DateUtils.formatDate(nextSecond));
+ originResponse2.setHeader("Cache-Control", "max-age=7200");
+ originResponse2.setHeader("Expires", DateUtils.formatDate(inTwoHoursPlusASec));
+ originResponse2.setHeader("Vary", "Accept-Encoding");
+
+ HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req3.addHeader("Range", "bytes=0-50");
+ req3.addHeader("Accept-Encoding", "gzip");
+
+ backendExpectsAnyRequest().andReturn(originResponse);
+ backendExpectsAnyRequest().andReturn(originResponse2).times(1, 2);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+ HttpResponse result = impl.execute(host, req3);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+ Assert.assertNotNull(result.getFirstHeader("Expires"));
+ Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
+ Assert.assertNotNull(result.getFirstHeader("Vary"));
+ }
+ }
+
+ /*
+ * "If the [206] response is the result of an If-Range request that used a
+ * weak validator, the response MUST NOT include other entity-headers; this
+ * prevents inconsistencies between cached entity-bodies and updated
+ * headers."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+ */
+ @Test
+ public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders()
+ throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ Date now = new Date();
+ Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+ originResponse = make200Response();
+ originResponse.addHeader("Allow", "GET,HEAD");
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+ originResponse.addHeader("Content-Language", "en");
+ originResponse.addHeader("Content-Encoding", "identity");
+ originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+ originResponse.addHeader("Content-Length", "128");
+ originResponse.addHeader("Content-Type", "application/octet-stream");
+ originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+ originResponse.addHeader("ETag", "W/\"weak-tag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.addHeader("If-Range", "W/\"weak-tag\"");
+ req2.addHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+ Assert.assertNull(result.getFirstHeader("Allow"));
+ Assert.assertNull(result.getFirstHeader("Content-Encoding"));
+ Assert.assertNull(result.getFirstHeader("Content-Language"));
+ Assert.assertNull(result.getFirstHeader("Content-MD5"));
+ Assert.assertNull(result.getFirstHeader("Last-Modified"));
+ }
+ }
+
+ /*
+ * "Otherwise, the [206] response MUST include all of the entity-headers
+ * that would have been returned with a 200 (OK) response to the same
+ * [If-Range] request."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+ */
+ @Test
+ public void test206ResponseToIfRangeWithStrongValidatorReturnsAllEntityHeaders()
+ throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ Date now = new Date();
+ Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+ originResponse.addHeader("Allow", "GET,HEAD");
+ originResponse.addHeader("Cache-Control", "max-age=3600");
+ originResponse.addHeader("Content-Language", "en");
+ originResponse.addHeader("Content-Encoding", "identity");
+ originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+ originResponse.addHeader("Content-Length", "128");
+ originResponse.addHeader("Content-Type", "application/octet-stream");
+ originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+ originResponse.addHeader("ETag", "\"strong-tag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.addHeader("If-Range", "\"strong-tag\"");
+ req2.addHeader("Range", "bytes=0-50");
+
+ backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+ Assert.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
+ Assert.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
+ Assert.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
+ Assert.assertEquals("identity", result.getFirstHeader("Content-Encoding").getValue());
+ Assert.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
+ .getValue());
+ Assert.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
+ .getFirstHeader("Last-Modified").getValue());
+ }
+ }
+
+ /*
+ * "A cache MUST NOT combine a 206 response with other previously cached
+ * content if the ETag or Last-Modified headers do not match exactly, see
+ * 13.5.4."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+ */
+ @Test
+ public void test206ResponseIsNotCombinedWithPreviousContentIfETagDoesNotMatch()
+ throws Exception {
+
+ Date now = new Date();
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("Cache-Control", "max-age=3600");
+ resp1.setHeader("ETag", "\"etag1\"");
+ byte[] bytes1 = new byte[128];
+ for (int i = 0; i < bytes1.length; i++) {
+ bytes1[i] = (byte) 1;
+ }
+ resp1.setEntity(new ByteArrayEntity(bytes1));
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Cache-Control", "no-cache");
+ req2.setHeader("Range", "bytes=0-50");
+
+ Date inOneSecond = new Date(now.getTime() + 1000L);
+ HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+ "Partial Content");
+ resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+ resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
+ resp2.setHeader("ETag", "\"etag2\"");
+ resp2.setHeader("Content-Range", "bytes 0-50/128");
+ byte[] bytes2 = new byte[51];
+ for (int i = 0; i < bytes2.length; i++) {
+ bytes2[i] = (byte) 2;
+ }
+ resp2.setEntity(new ByteArrayEntity(bytes2));
+
+ Date inTwoSeconds = new Date(now.getTime() + 2000L);
+ HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp3 = make200Response();
+ resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
+ resp3.setHeader("Cache-Control", "max-age=3600");
+ resp3.setHeader("ETag", "\"etag2\"");
+ byte[] bytes3 = new byte[128];
+ for (int i = 0; i < bytes3.length; i++) {
+ bytes3[i] = (byte) 2;
+ }
+ resp3.setEntity(new ByteArrayEntity(bytes3));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp2);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+ HttpResponse result = impl.execute(host, req3);
+
+ verifyMocks();
+
+ InputStream i = result.getEntity().getContent();
+ int b;
+ boolean found1 = false;
+ boolean found2 = false;
+ while ((b = i.read()) != -1) {
+ if (b == 1)
+ found1 = true;
+ if (b == 2)
+ found2 = true;
+ }
+ i.close();
+ Assert.assertFalse(found1 && found2); // mixture of content
+ }
+
+ @Test
+ public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch()
+ throws Exception {
+
+ Date now = new Date();
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ Date oneHourAgo = new Date(now.getTime() - 3600L);
+ resp1.setHeader("Cache-Control", "max-age=3600");
+ resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+ byte[] bytes1 = new byte[128];
+ for (int i = 0; i < bytes1.length; i++) {
+ bytes1[i] = (byte) 1;
+ }
+ resp1.setEntity(new ByteArrayEntity(bytes1));
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Cache-Control", "no-cache");
+ req2.setHeader("Range", "bytes=0-50");
+
+ Date inOneSecond = new Date(now.getTime() + 1000L);
+ HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+ "Partial Content");
+ resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+ resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
+ resp2.setHeader("Last-Modified", DateUtils.formatDate(now));
+ resp2.setHeader("Content-Range", "bytes 0-50/128");
+ byte[] bytes2 = new byte[51];
+ for (int i = 0; i < bytes2.length; i++) {
+ bytes2[i] = (byte) 2;
+ }
+ resp2.setEntity(new ByteArrayEntity(bytes2));
+
+ Date inTwoSeconds = new Date(now.getTime() + 2000L);
+ HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp3 = make200Response();
+ resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
+ resp3.setHeader("Cache-Control", "max-age=3600");
+ resp3.setHeader("ETag", "\"etag2\"");
+ byte[] bytes3 = new byte[128];
+ for (int i = 0; i < bytes3.length; i++) {
+ bytes3[i] = (byte) 2;
+ }
+ resp3.setEntity(new ByteArrayEntity(bytes3));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp2);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+ HttpResponse result = impl.execute(host, req3);
+
+ verifyMocks();
+
+ InputStream i = result.getEntity().getContent();
+ int b;
+ boolean found1 = false;
+ boolean found2 = false;
+ while ((b = i.read()) != -1) {
+ if (b == 1)
+ found1 = true;
+ if (b == 2)
+ found2 = true;
+ }
+ i.close();
+ Assert.assertFalse(found1 && found2); // mixture of content
+ }
+
+ /*
+ * "A cache that does not support the Range and Content-Range headers MUST
+ * NOT cache 206 (Partial) responses."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
+ */
+ @Test
+ public void test206ResponsesAreNotCachedIfTheCacheDoesNotSupportRangeAndContentRangeHeaders()
+ throws Exception {
+
+ if (!impl.supportsRangeAndContentRangeHeaders()) {
+ emptyMockCacheExpectsNoPuts();
+
+ request = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ request.addHeader("Range", "bytes=0-50");
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+ "Partial Content");
+ originResponse.setHeader("Content-Range", "bytes 0-50/128");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ byte[] bytes = new byte[51];
+ (new Random()).nextBytes(bytes);
+ originResponse.setEntity(new ByteArrayEntity(bytes));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
+ .isA(HttpRequest.class), (HttpContext) EasyMock.isNull())).andReturn(
+ originResponse);
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+ }
+ }
+
+ /*
+ * "10.3.4 303 See Other ... The 303 response MUST NOT be cached, but the
+ * response to the second (redirected) request might be cacheable."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
+ */
+ @Test
+ public void test303ResponsesAreNotCached() throws Exception {
+ emptyMockCacheExpectsNoPuts();
+
+ request = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_SEE_OTHER, "See Other");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockServer/1.0");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Content-Type", "application/x-cachingclient-test");
+ originResponse.setHeader("Location", "http://foo.example.com/other");
+ originResponse.setEntity(makeBody(entityLength));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+ impl.execute(host, request);
+ verifyMocks();
+ }
+
+ /*
+ * "The 304 response MUST NOT contain a message-body, and thus is always
+ * terminated by the first empty line after the header fields."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void test304ResponseDoesNotContainABody() throws Exception {
+ request.setHeader("If-None-Match", "\"etag\"");
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockServer/1.0");
+ originResponse.setHeader("Content-Length", "128");
+ originResponse.setEntity(makeBody(entityLength));
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+
+ Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+ }
+
+ /*
+ * "The [304] response MUST include the following header fields: - Date,
+ * unless its omission is required by section 14.18.1 [clockless origin
+ * servers]."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void test304ResponseWithDateHeaderForwardedFromOriginIncludesDateHeader()
+ throws Exception {
+
+ request.setHeader("If-None-Match", "\"etag\"");
+
+ originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+ originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+ originResponse.setHeader("Server", "MockServer/1.0");
+ originResponse.setHeader("ETag", "\"etag\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+
+ verifyMocks();
+ Assert.assertNotNull(result.getFirstHeader("Date"));
+ }
+
+ @Test
+ public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("ETag", "\"etag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("If-None-Match", "\"etag\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ Assert.assertNotNull(result.getFirstHeader("Date"));
+ }
+ }
+
+ /*
+ * "The [304] response MUST include the following header fields: - ETag
+ * and/or Content-Location, if the header would have been sent in a 200
+ * response to the same request."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("ETag", "\"etag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("If-None-Match", "\"etag\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ Assert.assertNotNull(result.getFirstHeader("ETag"));
+ }
+ }
+
+ @Test
+ public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid()
+ throws Exception {
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Content-Location", "http://foo.example.com/other");
+ originResponse.setHeader("ETag", "\"etag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("If-None-Match", "\"etag\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ Assert.assertNotNull(result.getFirstHeader("Content-Location"));
+ }
+ }
+
+ /*
+ * "The [304] response MUST include the following header fields: ... -
+ * Expires, Cache-Control, and/or Vary, if the field-value might differ from
+ * that sent in any previous response for the same variant
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer()
+ throws Exception {
+
+ Date now = new Date();
+ Date inTwoHours = new Date(now.getTime() + 2 * 3600 * 1000L);
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req1.setHeader("Accept-Encoding", "gzip");
+
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "\"v1\"");
+ resp1.setHeader("Cache-Control", "max-age=7200");
+ resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours));
+ resp1.setHeader("Vary", "Accept-Encoding");
+ resp1.setEntity(makeBody(entityLength));
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req1.setHeader("Accept-Encoding", "gzip");
+ req1.setHeader("Cache-Control", "no-cache");
+
+ HttpResponse resp2 = make200Response();
+ resp2.setHeader("ETag", "\"v2\"");
+ resp2.setHeader("Cache-Control", "max-age=3600");
+ resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours));
+ resp2.setHeader("Vary", "Accept-Encoding");
+ resp2.setEntity(makeBody(entityLength));
+
+ HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req3.setHeader("Accept-Encoding", "gzip");
+ req3.setHeader("If-None-Match", "\"v2\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp2).times(1, 2);
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+ HttpResponse result = impl.execute(host, req3);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ Assert.assertNotNull(result.getFirstHeader("Expires"));
+ Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
+ Assert.assertNotNull(result.getFirstHeader("Vary"));
+ }
+ }
+
+ /*
+ * "Otherwise (i.e., the conditional GET used a weak validator), the
+ * response MUST NOT include other entity-headers; this prevents
+ * inconsistencies between cached entity-bodies and updated headers."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders()
+ throws Exception {
+
+ Date now = new Date();
+ Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "W/\"v1\"");
+ resp1.setHeader("Allow", "GET,HEAD");
+ resp1.setHeader("Content-Encoding", "identity");
+ resp1.setHeader("Content-Language", "en");
+ resp1.setHeader("Content-Length", "128");
+ resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+ resp1.setHeader("Content-Type", "application/octet-stream");
+ resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+ resp1.setHeader("Cache-Control", "max-age=7200");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("If-None-Match", "W/\"v1\"");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1).times(1, 2);
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+ Assert.assertNull(result.getFirstHeader("Allow"));
+ Assert.assertNull(result.getFirstHeader("Content-Encoding"));
+ Assert.assertNull(result.getFirstHeader("Content-Length"));
+ Assert.assertNull(result.getFirstHeader("Content-MD5"));
+ Assert.assertNull(result.getFirstHeader("Content-Type"));
+ Assert.assertNull(result.getFirstHeader("Last-Modified"));
+ }
+ }
+
+ /*
+ * "If a 304 response indicates an entity not currently cached, then the
+ * cache MUST disregard the response and repeat the request without the
+ * conditional."
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET()
+ throws Exception {
+
+ Date now = new Date();
+
+ // load cache with cacheable entry
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("ETag", "\"etag1\"");
+ resp1.setHeader("Cache-Control", "max-age=3600");
+
+ // force a revalidation
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
+
+ // updated ETag provided to a conditional revalidation
+ HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
+ "Not Modified");
+ resp2.setHeader("Date", DateUtils.formatDate(now));
+ resp2.setHeader("Server", "MockServer/1.0");
+ resp2.setHeader("ETag", "\"etag2\"");
+
+ // conditional validation uses If-None-Match
+ HttpRequest conditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ conditionalValidation.setHeader("If-None-Match", "\"etag1\"");
+
+ // unconditional validation doesn't use If-None-Match
+ HttpRequest unconditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ // new response to unconditional validation provides new body
+ HttpResponse resp3 = make200Response();
+ resp1.setHeader("ETag", "\"etag2\"");
+ resp1.setHeader("Cache-Control", "max-age=3600");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1);
+ // this next one will happen once if the cache tries to
+ // conditionally validate, zero if it goes full revalidation
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(conditionalValidation),
+ (HttpContext) EasyMock.isNull())).andReturn(resp2).times(0, 1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), eqRequest(unconditionalValidation),
+ (HttpContext) EasyMock.isNull())).andReturn(resp3);
+ replayMocks();
+
+ impl.execute(host, req1);
+ impl.execute(host, req2);
+
+ verifyMocks();
+ }
+
+ /*
+ * "If a cache uses a received 304 response to update a cache entry, the
+ * cache MUST update the entry to reflect any new field values given in the
+ * response.
+ *
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ */
+ @Test
+ public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
+
+ Date now = new Date();
+ Date inOneSecond = new Date(now.getTime() + 1000L);
+ HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ HttpResponse resp1 = make200Response();
+ resp1.setHeader("Cache-Control", "max-age=3600");
+ resp1.setHeader("ETag", "\"etag\"");
+
+ HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
+
+ HttpRequest conditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+ conditionalValidation.setHeader("If-None-Match", "\"etag\"");
+
+ HttpRequest unconditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+ // to be used if the cache generates a conditional validation
+ HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
+ "Not Modified");
+ resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+ resp2.setHeader("Server", "MockUtils/1.0");
+ resp2.setHeader("ETag", "\"etag\"");
+ resp2.setHeader("X-Extra", "junk");
+
+ // to be used if the cache generates an unconditional validation
+ HttpResponse resp3 = make200Response();
+ resp3.setHeader("Date", DateUtils.formatDate(inOneSecond));
+ resp3.setHeader("ETag", "\"etag\"");
+
+ Capture<HttpRequest> cap1 = new Capture<HttpRequest>();
+ Capture<HttpRequest> cap2 = new Capture<HttpRequest>();
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(resp1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.and(
+ eqRequest(conditionalValidation), EasyMock.capture(cap1)),
+ (HttpContext) EasyMock.isNull())).andReturn(resp2).times(0, 1);
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.eq(host), EasyMock.and(
+ eqRequest(unconditionalValidation), EasyMock.capture(cap2)),
+ (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+
+ replayMocks();
+
+ impl.execute(host, req1);
+ HttpResponse result = impl.execute(host, req2);
+
+ verifyMocks();
+
+ Assert.assertTrue((cap1.hasCaptured() && !cap2.hasCaptured())
+ || (!cap1.hasCaptured() && cap2.hasCaptured()));
+
+ if (cap1.hasCaptured()) {
+ Assert.assertEquals(DateUtils.formatDate(inOneSecond), result.getFirstHeader("Date")
+ .getValue());
+ Assert.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
+ }
+ }
+
+ /*
+ * "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
+ public void testMustIncludeWWWAuthenticateHeaderOnAnOrigin401Response() throws Exception {
+ originResponse = new BasicHttpResponse(HTTP_1_1, 401, "Unauthorized");
+ originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
+
+ EasyMock.expect(
+ mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+ (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+ replayMocks();
+
+ HttpResponse result = impl.execute(host, request);
+ if (result.getStatusLine().getStatusCode() == 401) {
+ Assert.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
+ }
+
+ verifyMocks();
+ }
+
+ /*
+ * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
+ * header containing a list of valid methods for the requested resource.
+ *
[... 996 lines stripped ...]