View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.http.impl.client.cache;
28  
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.net.SocketTimeoutException;
32  import java.util.Arrays;
33  import java.util.Date;
34  import java.util.Random;
35  import java.util.regex.Matcher;
36  import java.util.regex.Pattern;
37  
38  import org.apache.http.Header;
39  import org.apache.http.HeaderElement;
40  import org.apache.http.HttpEntityEnclosingRequest;
41  import org.apache.http.HttpHost;
42  import org.apache.http.HttpRequest;
43  import org.apache.http.HttpResponse;
44  import org.apache.http.HttpStatus;
45  import org.apache.http.HttpVersion;
46  import org.apache.http.ProtocolVersion;
47  import org.apache.http.client.ClientProtocolException;
48  import org.apache.http.client.cache.HttpCacheEntry;
49  import org.apache.http.client.methods.CloseableHttpResponse;
50  import org.apache.http.client.methods.HttpExecutionAware;
51  import org.apache.http.client.methods.HttpRequestWrapper;
52  import org.apache.http.client.protocol.HttpClientContext;
53  import org.apache.http.client.utils.DateUtils;
54  import org.apache.http.conn.routing.HttpRoute;
55  import org.apache.http.entity.BasicHttpEntity;
56  import org.apache.http.entity.ByteArrayEntity;
57  import org.apache.http.message.BasicHeader;
58  import org.apache.http.message.BasicHttpEntityEnclosingRequest;
59  import org.apache.http.message.BasicHttpRequest;
60  import org.apache.http.message.BasicHttpResponse;
61  import org.apache.http.protocol.HTTP;
62  import org.easymock.Capture;
63  import org.easymock.EasyMock;
64  import org.junit.Assert;
65  import org.junit.Ignore;
66  import org.junit.Test;
67  
68  /**
69   * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
70   * of the rules for proxies apply to us, as far as proper operation of the
71   * requests that pass through us. Generally speaking, we want to make sure that
72   * any response returned from our HttpClient.execute() methods is conditionally
73   * compliant with the rules for an HTTP/1.1 server, and that any requests we
74   * pass downstream to the backend HttpClient are are conditionally compliant
75   * with the rules for an HTTP/1.1 client.
76   */
77  public class TestProtocolRequirements extends AbstractProtocolTest {
78  
79      @Test
80      public void testCacheMissOnGETUsesOriginResponse() throws Exception {
81          EasyMock.expect(
82                  mockBackend.execute(
83                          EasyMock.eq(route),
84                          eqRequest(request),
85                          EasyMock.isA(HttpClientContext.class),
86                          EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
87          replayMocks();
88  
89          final HttpResponse result = impl.execute(route, request, context, null);
90  
91          verifyMocks();
92          Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
93      }
94  
95      /*
96       * "Proxy and gateway applications need to be careful when forwarding
97       * messages in protocol versions different from that of the application.
98       * Since the protocol version indicates the protocol capability of the
99       * sender, a proxy/gateway MUST NOT send a message with a version indicator
100      * which is greater than its actual version. If a higher version request is
101      * received, the proxy/gateway MUST either downgrade the request version, or
102      * respond with an error, or switch to tunnel behavior."
103      *
104      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
105      */
106     @Test
107     public void testHigherMajorProtocolVersionsOnRequestSwitchToTunnelBehavior() throws Exception {
108 
109         // tunnel behavior: I don't muck with request or response in
110         // any way
111         request = HttpRequestWrapper.wrap(
112                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 2, 13)));
113 
114         EasyMock.expect(
115                 mockBackend.execute(
116                         EasyMock.eq(route),
117                         eqRequest(request),
118                         EasyMock.isA(HttpClientContext.class),
119                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
120         replayMocks();
121 
122         final HttpResponse result = impl.execute(route, request, context, null);
123 
124         verifyMocks();
125         Assert.assertSame(originResponse, result);
126     }
127 
128     @Test
129     public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
130 
131         request = HttpRequestWrapper.wrap(
132                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2)));
133 
134         final HttpRequestWrapper downgraded = HttpRequestWrapper.wrap(
135                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
136 
137         EasyMock.expect(
138                 mockBackend.execute(
139                         EasyMock.eq(route),
140                         eqRequest(downgraded),
141                         EasyMock.isA(HttpClientContext.class),
142                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
143 
144         replayMocks();
145         final HttpResponse result = impl.execute(route, request, context, null);
146 
147         verifyMocks();
148         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
149     }
150 
151     /*
152      * "Due to interoperability problems with HTTP/1.0 proxies discovered since
153      * the publication of RFC 2068[33], caching proxies MUST, gateways MAY, and
154      * tunnels MUST NOT upgrade the request to the highest version they support.
155      * The proxy/gateway's response to that request MUST be in the same major
156      * version as the request."
157      *
158      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
159      */
160     @Test
161     public void testRequestsWithLowerProtocolVersionsGetUpgradedTo1_1() throws Exception {
162 
163         request = HttpRequestWrapper.wrap(
164                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
165         final HttpRequestWrapper upgraded = HttpRequestWrapper.wrap(
166                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
167 
168         EasyMock.expect(
169                 mockBackend.execute(
170                         EasyMock.eq(route),
171                         eqRequest(upgraded),
172                         EasyMock.isA(HttpClientContext.class),
173                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
174         replayMocks();
175 
176         final HttpResponse result = impl.execute(route, request, context, null);
177 
178         verifyMocks();
179         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
180     }
181 
182     /*
183      * "An HTTP server SHOULD send a response version equal to the highest
184      * version for which the server is at least conditionally compliant, and
185      * whose major version is less than or equal to the one received in the
186      * request."
187      *
188      * http://www.ietf.org/rfc/rfc2145.txt
189      */
190     @Test
191     public void testLowerOriginResponsesUpgradedToOurVersion1_1() throws Exception {
192         originResponse = Proxies.enhanceResponse(
193                 new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 2), HttpStatus.SC_OK, "OK"));
194         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
195         originResponse.setHeader("Server", "MockOrigin/1.0");
196         originResponse.setEntity(body);
197 
198         // not testing this internal behavior in this test, just want
199         // to check the protocol version that comes out the other end
200         EasyMock.expect(
201                 mockBackend.execute(
202                         EasyMock.isA(HttpRoute.class),
203                         EasyMock.isA(HttpRequestWrapper.class),
204                         EasyMock.isA(HttpClientContext.class),
205                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
206         replayMocks();
207 
208         final HttpResponse result = impl.execute(route, request, context, null);
209 
210         verifyMocks();
211         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getProtocolVersion());
212     }
213 
214     @Test
215     public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
216         request = HttpRequestWrapper.wrap(
217                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
218 
219         EasyMock.expect(
220                 mockBackend.execute(
221                         EasyMock.isA(HttpRoute.class),
222                         EasyMock.isA(HttpRequestWrapper.class),
223                         EasyMock.isA(HttpClientContext.class),
224                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
225         replayMocks();
226 
227         final HttpResponse result = impl.execute(route, request, context, null);
228 
229         verifyMocks();
230         Assert.assertEquals(HttpVersion.HTTP_1_1, result.getProtocolVersion());
231     }
232 
233     /*
234      * "A proxy MUST forward an unknown header, unless it is protected by a
235      * Connection header." http://www.ietf.org/rfc/rfc2145.txt
236      */
237     @Test
238     public void testForwardsUnknownHeadersOnRequestsFromHigherProtocolVersions() throws Exception {
239         request = HttpRequestWrapper.wrap(
240                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2)));
241         request.removeHeaders("Connection");
242         request.addHeader("X-Unknown-Header", "some-value");
243 
244         final HttpRequestWrapper downgraded = HttpRequestWrapper.wrap(
245                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
246         downgraded.removeHeaders("Connection");
247         downgraded.addHeader("X-Unknown-Header", "some-value");
248 
249         EasyMock.expect(
250                 mockBackend.execute(
251                         EasyMock.isA(HttpRoute.class),
252                         eqRequest(downgraded),
253                         EasyMock.isA(HttpClientContext.class),
254                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
255         replayMocks();
256 
257         impl.execute(route, request, context, null);
258 
259         verifyMocks();
260     }
261 
262     /*
263      * "A server MUST NOT send transfer-codings to an HTTP/1.0 client."
264      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
265      */
266     @Test
267     public void testTransferCodingsAreNotSentToAnHTTP_1_0Client() throws Exception {
268 
269         originResponse.setHeader("Transfer-Encoding", "identity");
270 
271         request = HttpRequestWrapper.wrap(
272                 new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0)));
273 
274         EasyMock.expect(
275                 mockBackend.execute(
276                         EasyMock.isA(HttpRoute.class),
277                         EasyMock.isA(HttpRequestWrapper.class),
278                         EasyMock.isA(HttpClientContext.class),
279                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
280         replayMocks();
281 
282         final HttpResponse result = impl.execute(route, request, context, null);
283 
284         verifyMocks();
285 
286         Assert.assertNull(result.getFirstHeader("TE"));
287         Assert.assertNull(result.getFirstHeader("Transfer-Encoding"));
288     }
289 
290     /*
291      * "Multiple message-header fields with the same field-name MAY be present
292      * in a message if and only if the entire field-value for that header field
293      * is defined as a comma-separated list [i.e., #(values)]. It MUST be
294      * possible to combine the multiple header fields into one
295      * "field-name: field-value" pair, without changing the semantics of the
296      * message, by appending each subsequent field-value to the first, each
297      * separated by a comma. The order in which header fields with the same
298      * field-name are received is therefore significant to the interpretation of
299      * the combined field value, and thus a proxy MUST NOT change the order of
300      * these field values when a message is forwarded."
301      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
302      */
303     private void testOrderOfMultipleHeadersIsPreservedOnRequests(final String h, final HttpRequestWrapper request)
304             throws Exception {
305         final Capture<HttpRequestWrapper> reqCapture = new Capture<HttpRequestWrapper>();
306 
307         EasyMock.expect(
308                 mockBackend.execute(
309                         EasyMock.isA(HttpRoute.class),
310                         EasyMock.capture(reqCapture),
311                         EasyMock.isA(HttpClientContext.class),
312                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
313         replayMocks();
314 
315         impl.execute(route, request, context, null);
316 
317         verifyMocks();
318 
319         final HttpRequest forwarded = reqCapture.getValue();
320         Assert.assertNotNull(forwarded);
321         final String expected = HttpTestUtils.getCanonicalHeaderValue(request, h);
322         final String actual = HttpTestUtils.getCanonicalHeaderValue(forwarded, h);
323         if (!actual.contains(expected)) {
324             Assert.assertEquals(expected, actual);
325         }
326 
327     }
328 
329     @Test
330     public void testOrderOfMultipleAcceptHeaderValuesIsPreservedOnRequests() throws Exception {
331         request.addHeader("Accept", "audio/*; q=0.2, audio/basic");
332         request.addHeader("Accept", "text/*, text/html, text/html;level=1, */*");
333         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept", request);
334     }
335 
336     @Test
337     public void testOrderOfMultipleAcceptCharsetHeadersIsPreservedOnRequests() throws Exception {
338         request.addHeader("Accept-Charset", "iso-8859-5");
339         request.addHeader("Accept-Charset", "unicode-1-1;q=0.8");
340         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Charset", request);
341     }
342 
343     @Test
344     public void testOrderOfMultipleAcceptEncodingHeadersIsPreservedOnRequests() throws Exception {
345         request.addHeader("Accept-Encoding", "identity");
346         request.addHeader("Accept-Encoding", "compress, gzip");
347         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
348     }
349 
350     @Test
351     public void testOrderOfMultipleAcceptLanguageHeadersIsPreservedOnRequests() throws Exception {
352         request.addHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7");
353         request.addHeader("Accept-Language", "i-cherokee");
354         testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
355     }
356 
357     @Test
358     public void testOrderOfMultipleAllowHeadersIsPreservedOnRequests() throws Exception {
359         final BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
360                 HttpVersion.HTTP_1_1);
361         put.setEntity(body);
362         put.addHeader("Allow", "GET, HEAD");
363         put.addHeader("Allow", "DELETE");
364         put.addHeader("Content-Length", "128");
365         testOrderOfMultipleHeadersIsPreservedOnRequests("Allow", HttpRequestWrapper.wrap(put));
366     }
367 
368     @Test
369     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnRequests() throws Exception {
370         request.addHeader("Cache-Control", "max-age=5");
371         request.addHeader("Cache-Control", "min-fresh=10");
372         testOrderOfMultipleHeadersIsPreservedOnRequests("Cache-Control", request);
373     }
374 
375     @Test
376     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnRequests() throws Exception {
377         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
378                 HttpVersion.HTTP_1_1);
379         post.setEntity(body);
380         post.addHeader("Content-Encoding", "gzip");
381         post.addHeader("Content-Encoding", "compress");
382         post.addHeader("Content-Length", "128");
383         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Encoding", HttpRequestWrapper.wrap(post));
384     }
385 
386     @Test
387     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnRequests() throws Exception {
388         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
389                 HttpVersion.HTTP_1_1);
390         post.setEntity(body);
391         post.addHeader("Content-Language", "mi");
392         post.addHeader("Content-Language", "en");
393         post.addHeader("Content-Length", "128");
394         testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Language", HttpRequestWrapper.wrap(post));
395     }
396 
397     @Test
398     public void testOrderOfMultipleExpectHeadersIsPreservedOnRequests() throws Exception {
399         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
400                 HttpVersion.HTTP_1_1);
401         post.setEntity(body);
402         post.addHeader("Expect", "100-continue");
403         post.addHeader("Expect", "x-expect=true");
404         post.addHeader("Content-Length", "128");
405         testOrderOfMultipleHeadersIsPreservedOnRequests("Expect", HttpRequestWrapper.wrap(post));
406     }
407 
408     @Test
409     public void testOrderOfMultiplePragmaHeadersIsPreservedOnRequests() throws Exception {
410         request.addHeader("Pragma", "no-cache");
411         request.addHeader("Pragma", "x-pragma-1, x-pragma-2");
412         testOrderOfMultipleHeadersIsPreservedOnRequests("Pragma", request);
413     }
414 
415     @Test
416     public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
417         request.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
418         request.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
419         testOrderOfMultipleHeadersIsPreservedOnRequests("Via", request);
420     }
421 
422     @Test
423     public void testOrderOfMultipleWarningHeadersIsPreservedOnRequests() throws Exception {
424         request.addHeader("Warning", "199 fred \"bargle\"");
425         request.addHeader("Warning", "199 barney \"bungle\"");
426         testOrderOfMultipleHeadersIsPreservedOnRequests("Warning", request);
427     }
428 
429     private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
430         EasyMock.expect(
431                 mockBackend.execute(
432                         EasyMock.isA(HttpRoute.class),
433                         EasyMock.isA(HttpRequestWrapper.class),
434                         EasyMock.isA(HttpClientContext.class),
435                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
436         replayMocks();
437 
438         final HttpResponse result = impl.execute(route, request, context, null);
439 
440         verifyMocks();
441 
442         Assert.assertNotNull(result);
443         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
444                 .getCanonicalHeaderValue(result, h));
445 
446     }
447 
448     @Test
449     public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
450         originResponse = Proxies.enhanceResponse(
451                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 405, "Method Not Allowed"));
452         originResponse.addHeader("Allow", "HEAD");
453         originResponse.addHeader("Allow", "DELETE");
454         testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
455     }
456 
457     @Test
458     public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
459         originResponse.addHeader("Cache-Control", "max-age=0");
460         originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
461         testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
462     }
463 
464     @Test
465     public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
466         originResponse.addHeader("Content-Encoding", "gzip");
467         originResponse.addHeader("Content-Encoding", "compress");
468         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
469     }
470 
471     @Test
472     public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
473         originResponse.addHeader("Content-Language", "mi");
474         originResponse.addHeader("Content-Language", "en");
475         testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
476     }
477 
478     @Test
479     public void testOrderOfMultiplePragmaHeadersIsPreservedOnResponses() throws Exception {
480         originResponse.addHeader("Pragma", "no-cache, x-pragma-2");
481         originResponse.addHeader("Pragma", "x-pragma-1");
482         testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
483     }
484 
485     @Test
486     public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
487         originResponse.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
488         originResponse.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
489         testOrderOfMultipleHeadersIsPreservedOnResponses("Via");
490     }
491 
492     @Test
493     public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
494         originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
495         originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
496         testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
497     }
498 
499     /*
500      * "However, applications MUST understand the class of any status code, as
501      * indicated by the first digit, and treat any unrecognized response as
502      * being equivalent to the x00 status code of that class, with the exception
503      * that an unrecognized response MUST NOT be cached."
504      *
505      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
506      */
507     private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
508 
509         emptyMockCacheExpectsNoPuts();
510 
511         originResponse = Proxies.enhanceResponse(
512                 new BasicHttpResponse(HttpVersion.HTTP_1_1, code, "Moo"));
513         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
514         originResponse.setHeader("Server", "MockOrigin/1.0");
515         originResponse.setHeader("Cache-Control", "max-age=3600");
516         originResponse.setEntity(body);
517 
518         EasyMock.expect(
519                 mockBackend.execute(
520                         EasyMock.isA(HttpRoute.class),
521                         EasyMock.isA(HttpRequestWrapper.class),
522                         EasyMock.isA(HttpClientContext.class),
523                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
524 
525         replayMocks();
526 
527         impl.execute(route, request, context, null);
528 
529         // in particular, there were no storage calls on the cache
530         verifyMocks();
531     }
532 
533     @Test
534     public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
535         for (int i = 102; i <= 199; i++) {
536             testUnknownResponseStatusCodeIsNotCached(i);
537         }
538         for (int i = 207; i <= 299; i++) {
539             testUnknownResponseStatusCodeIsNotCached(i);
540         }
541         for (int i = 308; i <= 399; i++) {
542             testUnknownResponseStatusCodeIsNotCached(i);
543         }
544         for (int i = 418; i <= 499; i++) {
545             testUnknownResponseStatusCodeIsNotCached(i);
546         }
547         for (int i = 506; i <= 999; i++) {
548             testUnknownResponseStatusCodeIsNotCached(i);
549         }
550     }
551 
552     /*
553      * "Unrecognized header fields SHOULD be ignored by the recipient and MUST
554      * be forwarded by transparent proxies."
555      *
556      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
557      */
558     @Test
559     public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
560         request.addHeader("X-Unknown-Header", "blahblah");
561         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
562         EasyMock.expect(
563                 mockBackend.execute(
564                         EasyMock.isA(HttpRoute.class),
565                         EasyMock.capture(reqCap),
566                         EasyMock.isA(HttpClientContext.class),
567                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
568 
569         replayMocks();
570 
571         impl.execute(route, request, context, null);
572 
573         verifyMocks();
574         final HttpRequest forwarded = reqCap.getValue();
575         final Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
576         Assert.assertEquals(1, hdrs.length);
577         Assert.assertEquals("blahblah", hdrs[0].getValue());
578     }
579 
580     @Test
581     public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
582         originResponse.addHeader("X-Unknown-Header", "blahblah");
583         EasyMock.expect(
584                 mockBackend.execute(
585                         EasyMock.isA(HttpRoute.class),
586                         EasyMock.isA(HttpRequestWrapper.class),
587                         EasyMock.isA(HttpClientContext.class),
588                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
589 
590         replayMocks();
591 
592         final HttpResponse result = impl.execute(route, request, context, null);
593 
594         verifyMocks();
595         final Header[] hdrs = result.getHeaders("X-Unknown-Header");
596         Assert.assertEquals(1, hdrs.length);
597         Assert.assertEquals("blahblah", hdrs[0].getValue());
598     }
599 
600     /*
601      * "If a client will wait for a 100 (Continue) response before sending the
602      * request body, it MUST send an Expect request-header field (section 14.20)
603      * with the '100-continue' expectation."
604      *
605      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
606      */
607     @Test
608     public void testRequestsExpecting100ContinueBehaviorShouldSetExpectHeader() throws Exception {
609         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest(
610                 "POST", "/", HttpVersion.HTTP_1_1);
611         post.setHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
612         post.setHeader("Content-Length", "128");
613         post.setEntity(new BasicHttpEntity());
614 
615         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
616 
617         EasyMock.expect(
618                 mockBackend.execute(
619                         EasyMock.eq(route),
620                         EasyMock.capture(reqCap),
621                         EasyMock.isA(HttpClientContext.class),
622                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
623 
624         replayMocks();
625 
626         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
627 
628         verifyMocks();
629 
630         final HttpRequestWrapper forwarded = reqCap.getValue();
631         boolean foundExpect = false;
632         for (final Header h : forwarded.getHeaders("Expect")) {
633             for (final HeaderElement elt : h.getElements()) {
634                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
635                     foundExpect = true;
636                     break;
637                 }
638             }
639         }
640         Assert.assertTrue(foundExpect);
641     }
642 
643     /*
644      * "If a client will wait for a 100 (Continue) response before sending the
645      * request body, it MUST send an Expect request-header field (section 14.20)
646      * with the '100-continue' expectation."
647      *
648      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
649      */
650     @Test
651     public void testRequestsNotExpecting100ContinueBehaviorShouldNotSetExpectContinueHeader()
652             throws Exception {
653         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest(
654                 "POST", "/", HttpVersion.HTTP_1_1);
655         post.setHeader("Content-Length", "128");
656         post.setEntity(new BasicHttpEntity());
657 
658         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
659 
660         EasyMock.expect(
661                 mockBackend.execute(
662                         EasyMock.eq(route),
663                         EasyMock.capture(reqCap),
664                         EasyMock.isA(HttpClientContext.class),
665                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
666 
667         replayMocks();
668 
669         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
670 
671         verifyMocks();
672 
673         final HttpRequestWrapper forwarded = reqCap.getValue();
674         boolean foundExpect = false;
675         for (final Header h : forwarded.getHeaders("Expect")) {
676             for (final HeaderElement elt : h.getElements()) {
677                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
678                     foundExpect = true;
679                     break;
680                 }
681             }
682         }
683         Assert.assertFalse(foundExpect);
684     }
685 
686     /*
687      * "A client MUST NOT send an Expect request-header field (section 14.20)
688      * with the '100-continue' expectation if it does not intend to send a
689      * request body."
690      *
691      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
692      */
693     @Test
694     public void testExpect100ContinueIsNotSentIfThereIsNoRequestBody() throws Exception {
695         request.addHeader("Expect", "100-continue");
696         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
697         EasyMock.expect(
698                 mockBackend.execute(
699                         EasyMock.eq(route),
700                         EasyMock.capture(reqCap),
701                         EasyMock.isA(HttpClientContext.class),
702                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
703 
704         replayMocks();
705         impl.execute(route, request, context, null);
706         verifyMocks();
707         final HttpRequest forwarded = reqCap.getValue();
708         boolean foundExpectContinue = false;
709 
710         for (final Header h : forwarded.getHeaders("Expect")) {
711             for (final HeaderElement elt : h.getElements()) {
712                 if ("100-continue".equalsIgnoreCase(elt.getName())) {
713                     foundExpectContinue = true;
714                     break;
715                 }
716             }
717         }
718         Assert.assertFalse(foundExpectContinue);
719     }
720 
721     /*
722      * "If a proxy receives a request that includes an Expect request- header
723      * field with the '100-continue' expectation, and the proxy either knows
724      * that the next-hop server complies with HTTP/1.1 or higher, or does not
725      * know the HTTP version of the next-hop server, it MUST forward the
726      * request, including the Expect header field.
727      *
728      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
729      */
730     @Test
731     public void testExpectHeadersAreForwardedOnRequests() throws Exception {
732         // This would mostly apply to us if we were part of an
733         // application that was a proxy, and would be the
734         // responsibility of the greater application. Our
735         // responsibility is to make sure that if we get an
736         // entity-enclosing request that we properly set (or unset)
737         // the Expect header per the request.expectContinue() flag,
738         // which is tested by the previous few tests.
739     }
740 
741     /*
742      * "A proxy MUST NOT forward a 100 (Continue) response if the request
743      * message was received from an HTTP/1.0 (or earlier) client and did not
744      * include an Expect request-header field with the '100-continue'
745      * expectation. This requirement overrides the general rule for forwarding
746      * of 1xx responses (see section 10.1)."
747      *
748      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
749      */
750     @Test
751     public void test100ContinueResponsesAreNotForwardedTo1_0ClientsWhoDidNotAskForThem()
752             throws Exception {
753 
754         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
755                 new ProtocolVersion("HTTP", 1, 0));
756         post.setEntity(body);
757         post.setHeader("Content-Length", "128");
758 
759         originResponse = Proxies.enhanceResponse(
760                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 100, "Continue"));
761         EasyMock.expect(
762                 mockBackend.execute(
763                         EasyMock.eq(route),
764                         EasyMock.isA(HttpRequestWrapper.class),
765                         EasyMock.isA(HttpClientContext.class),
766                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
767         replayMocks();
768 
769         try {
770             // if a 100 response gets up to us from the HttpClient
771             // backend, we can't really handle it at that point
772             impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
773             Assert.fail("should have thrown an exception");
774         } catch (final ClientProtocolException expected) {
775         }
776 
777         verifyMocks();
778     }
779 
780     /*
781      * "9.2 OPTIONS. ...Responses to this method are not cacheable.
782      *
783      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
784      */
785     @Test
786     public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
787         emptyMockCacheExpectsNoPuts();
788         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
789         originResponse.addHeader("Cache-Control", "max-age=3600");
790 
791         EasyMock.expect(
792                 mockBackend.execute(
793                         EasyMock.eq(route),
794                         EasyMock.isA(HttpRequestWrapper.class),
795                         EasyMock.isA(HttpClientContext.class),
796                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
797 
798         replayMocks();
799 
800         impl.execute(route, request, context, null);
801 
802         verifyMocks();
803     }
804 
805     /*
806      * "A 200 response SHOULD .... If no response body is included, the response
807      * MUST include a Content-Length field with a field-value of '0'."
808      *
809      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
810      */
811     @Test
812     public void test200ResponseToOPTIONSWithNoBodyShouldIncludeContentLengthZero() throws Exception {
813 
814         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
815         originResponse.setEntity(null);
816         originResponse.setHeader("Content-Length", "0");
817 
818         EasyMock.expect(
819                 mockBackend.execute(
820                         EasyMock.eq(route),
821                         EasyMock.isA(HttpRequestWrapper.class),
822                         EasyMock.isA(HttpClientContext.class),
823                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
824         replayMocks();
825 
826         final HttpResponse result = impl.execute(route, request, context, null);
827 
828         verifyMocks();
829         final Header contentLength = result.getFirstHeader("Content-Length");
830         Assert.assertNotNull(contentLength);
831         Assert.assertEquals("0", contentLength.getValue());
832     }
833 
834     /*
835      * "When a proxy receives an OPTIONS request on an absoluteURI for which
836      * request forwarding is permitted, the proxy MUST check for a Max-Forwards
837      * field. If the Max-Forwards field-value is zero ("0"), the proxy MUST NOT
838      * forward the message; instead, the proxy SHOULD respond with its own
839      * communication options."
840      *
841      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
842      */
843     @Test
844     public void testDoesNotForwardOPTIONSWhenMaxForwardsIsZeroOnAbsoluteURIRequest()
845             throws Exception {
846         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "*", HttpVersion.HTTP_1_1));
847         request.setHeader("Max-Forwards", "0");
848 
849         replayMocks();
850         impl.execute(route, request, context, null);
851         verifyMocks();
852     }
853 
854     /*
855      * "If the Max-Forwards field-value is an integer greater than zero, the
856      * proxy MUST decrement the field-value when it forwards the request."
857      *
858      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
859      */
860     @Test
861     public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
862 
863         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "*", HttpVersion.HTTP_1_1));
864         request.setHeader("Max-Forwards", "7");
865 
866         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
867 
868         EasyMock.expect(
869                 mockBackend.execute(
870                         EasyMock.eq(route),
871                         EasyMock.capture(cap),
872                         EasyMock.isA(HttpClientContext.class),
873                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
874 
875         replayMocks();
876         impl.execute(route, request, context, null);
877         verifyMocks();
878 
879         final HttpRequest captured = cap.getValue();
880         Assert.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
881     }
882 
883     /*
884      * "If no Max-Forwards field is present in the request, then the forwarded
885      * request MUST NOT include a Max-Forwards field."
886      *
887      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
888      */
889     @Test
890     public void testDoesNotAddAMaxForwardsHeaderToForwardedOPTIONSRequests() throws Exception {
891         request = HttpRequestWrapper.wrap(new BasicHttpRequest("OPTIONS", "/", HttpVersion.HTTP_1_1));
892         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
893         EasyMock.expect(
894                 mockBackend.execute(
895                         EasyMock.eq(route),
896                         EasyMock.capture(reqCap),
897                         EasyMock.isA(HttpClientContext.class),
898                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
899 
900         replayMocks();
901         impl.execute(route, request, context, null);
902         verifyMocks();
903 
904         final HttpRequest forwarded = reqCap.getValue();
905         Assert.assertNull(forwarded.getFirstHeader("Max-Forwards"));
906     }
907 
908     /*
909      * "The HEAD method is identical to GET except that the server MUST NOT
910      * return a message-body in the response."
911      *
912      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
913      */
914     @Test
915     public void testResponseToAHEADRequestMustNotHaveABody() throws Exception {
916         request = HttpRequestWrapper.wrap(new BasicHttpRequest("HEAD", "/", HttpVersion.HTTP_1_1));
917         EasyMock.expect(
918                 mockBackend.execute(
919                         EasyMock.eq(route),
920                         EasyMock.isA(HttpRequestWrapper.class),
921                         EasyMock.isA(HttpClientContext.class),
922                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
923 
924         replayMocks();
925 
926         final HttpResponse result = impl.execute(route, request, context, null);
927 
928         verifyMocks();
929 
930         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
931     }
932 
933     /*
934      * "If the new field values indicate that the cached entity differs from the
935      * current entity (as would be indicated by a change in Content-Length,
936      * Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache
937      * entry as stale."
938      *
939      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
940      */
941     private void testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale(final String eHeader,
942             final String oldVal, final String newVal) throws Exception {
943 
944         // put something cacheable in the cache
945         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
946                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
947         final HttpResponse resp1 = HttpTestUtils.make200Response();
948         resp1.addHeader("Cache-Control", "max-age=3600");
949         resp1.setHeader(eHeader, oldVal);
950 
951         // get a head that penetrates the cache
952         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
953                 new BasicHttpRequest("HEAD", "/", HttpVersion.HTTP_1_1));
954         req2.addHeader("Cache-Control", "no-cache");
955         final HttpResponse resp2 = HttpTestUtils.make200Response();
956         resp2.setEntity(null);
957         resp2.setHeader(eHeader, newVal);
958 
959         // next request doesn't tolerate stale entry
960         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
961                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
962         req3.addHeader("Cache-Control", "max-stale=0");
963         final HttpResponse resp3 = HttpTestUtils.make200Response();
964         resp3.setHeader(eHeader, newVal);
965 
966         EasyMock.expect(
967                 mockBackend.execute(
968                         EasyMock.eq(route),
969                         eqRequest(req1),
970                         EasyMock.isA(HttpClientContext.class),
971                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
972         EasyMock.expect(
973                 mockBackend.execute(
974                         EasyMock.eq(route),
975                         eqRequest(req2),
976                         EasyMock.isA(HttpClientContext.class),
977                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
978         EasyMock.expect(
979                 mockBackend.execute(
980                         EasyMock.eq(route),
981                         EasyMock.isA(HttpRequestWrapper.class),
982                         EasyMock.isA(HttpClientContext.class),
983                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
984                                 Proxies.enhanceResponse(resp3));
985 
986         replayMocks();
987 
988         impl.execute(route, req1, context, null);
989         impl.execute(route, req2, context, null);
990         impl.execute(route, req3, context, null);
991 
992         verifyMocks();
993     }
994 
995     @Test
996     public void testHEADResponseWithUpdatedContentLengthFieldMakeACacheEntryStale()
997             throws Exception {
998         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-Length", "128", "127");
999     }
1000 
1001     @Test
1002     public void testHEADResponseWithUpdatedContentMD5FieldMakeACacheEntryStale() throws Exception {
1003         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-MD5",
1004                 "Q2hlY2sgSW50ZWdyaXR5IQ==", "Q2hlY2sgSW50ZWdyaXR5IR==");
1005 
1006     }
1007 
1008     @Test
1009     public void testHEADResponseWithUpdatedETagFieldMakeACacheEntryStale() throws Exception {
1010         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("ETag", "\"etag1\"",
1011                 "\"etag2\"");
1012     }
1013 
1014     @Test
1015     public void testHEADResponseWithUpdatedLastModifiedFieldMakeACacheEntryStale() throws Exception {
1016         final Date now = new Date();
1017         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
1018         final Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L);
1019         testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils
1020                 .formatDate(tenSecondsAgo), DateUtils.formatDate(sixSecondsAgo));
1021     }
1022 
1023     /*
1024      * "9.5 POST. Responses to this method are not cacheable, unless the
1025      * response includes appropriate Cache-Control or Expires header fields."
1026      *
1027      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
1028      */
1029     @Test
1030     public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
1031         emptyMockCacheExpectsNoPuts();
1032 
1033         final BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
1034                 HttpVersion.HTTP_1_1);
1035         post.setHeader("Content-Length", "128");
1036         post.setEntity(HttpTestUtils.makeBody(128));
1037 
1038         originResponse.removeHeaders("Cache-Control");
1039         originResponse.removeHeaders("Expires");
1040 
1041         EasyMock.expect(
1042                 mockBackend.execute(
1043                         EasyMock.isA(HttpRoute.class),
1044                         EasyMock.isA(HttpRequestWrapper.class),
1045                         EasyMock.isA(HttpClientContext.class),
1046                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1047 
1048         replayMocks();
1049 
1050         impl.execute(route, HttpRequestWrapper.wrap(post), context, null);
1051 
1052         verifyMocks();
1053     }
1054 
1055     /*
1056      * "9.5 PUT. ...Responses to this method are not cacheable."
1057      *
1058      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
1059      */
1060     @Test
1061     public void testResponsesToPUTsAreNotCached() throws Exception {
1062         emptyMockCacheExpectsNoPuts();
1063 
1064         final BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
1065                 HttpVersion.HTTP_1_1);
1066         put.setEntity(HttpTestUtils.makeBody(128));
1067         put.addHeader("Content-Length", "128");
1068 
1069         originResponse.setHeader("Cache-Control", "max-age=3600");
1070 
1071         EasyMock.expect(
1072                 mockBackend.execute(
1073                         EasyMock.isA(HttpRoute.class),
1074                         EasyMock.isA(HttpRequestWrapper.class),
1075                         EasyMock.isA(HttpClientContext.class),
1076                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1077 
1078         replayMocks();
1079 
1080         impl.execute(route, HttpRequestWrapper.wrap(put), context, null);
1081 
1082         verifyMocks();
1083     }
1084 
1085     /*
1086      * "9.6 DELETE. ... Responses to this method are not cacheable."
1087      *
1088      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
1089      */
1090     @Test
1091     public void testResponsesToDELETEsAreNotCached() throws Exception {
1092         emptyMockCacheExpectsNoPuts();
1093 
1094         request = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
1095         originResponse.setHeader("Cache-Control", "max-age=3600");
1096 
1097         EasyMock.expect(
1098                 mockBackend.execute(
1099                         EasyMock.isA(HttpRoute.class),
1100                         EasyMock.isA(HttpRequestWrapper.class),
1101                         EasyMock.isA(HttpClientContext.class),
1102                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1103 
1104         replayMocks();
1105 
1106         impl.execute(route, request, context, null);
1107 
1108         verifyMocks();
1109     }
1110 
1111     /*
1112      * "A TRACE request MUST NOT include an entity."
1113      *
1114      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
1115      */
1116     @Test
1117     public void testForwardedTRACERequestsDoNotIncludeAnEntity() throws Exception {
1118         final BasicHttpEntityEnclosingRequest trace = new BasicHttpEntityEnclosingRequest("TRACE", "/",
1119                 HttpVersion.HTTP_1_1);
1120         trace.setEntity(HttpTestUtils.makeBody(entityLength));
1121         trace.setHeader("Content-Length", Integer.toString(entityLength));
1122 
1123         final Capture<HttpRequestWrapper> reqCap = new Capture<HttpRequestWrapper>();
1124 
1125         EasyMock.expect(
1126                 mockBackend.execute(
1127                         EasyMock.eq(route),
1128                         EasyMock.capture(reqCap),
1129                         EasyMock.isA(HttpClientContext.class),
1130                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1131 
1132         replayMocks();
1133         impl.execute(route, HttpRequestWrapper.wrap(trace), context, null);
1134         verifyMocks();
1135 
1136         final HttpRequest forwarded = reqCap.getValue();
1137         if (forwarded instanceof HttpEntityEnclosingRequest) {
1138             final HttpEntityEnclosingRequest bodyReq = (HttpEntityEnclosingRequest) forwarded;
1139             Assert.assertTrue(bodyReq.getEntity() == null
1140                     || bodyReq.getEntity().getContentLength() == 0);
1141         } else {
1142             // request didn't enclose an entity
1143         }
1144     }
1145 
1146     /*
1147      * "9.8 TRACE ... Responses to this method MUST NOT be cached."
1148      *
1149      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
1150      */
1151     @Test
1152     public void testResponsesToTRACEsAreNotCached() throws Exception {
1153         emptyMockCacheExpectsNoPuts();
1154 
1155         request = HttpRequestWrapper.wrap(new BasicHttpRequest("TRACE", "/", HttpVersion.HTTP_1_1));
1156         originResponse.setHeader("Cache-Control", "max-age=3600");
1157 
1158         EasyMock.expect(
1159                 mockBackend.execute(
1160                         EasyMock.isA(HttpRoute.class),
1161                         EasyMock.isA(HttpRequestWrapper.class),
1162                         EasyMock.isA(HttpClientContext.class),
1163                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1164 
1165         replayMocks();
1166 
1167         impl.execute(route, request, context, null);
1168 
1169         verifyMocks();
1170     }
1171 
1172     /*
1173      * "The 204 response MUST NOT include a message-body, and thus is always
1174      * terminated by the first empty line after the header fields."
1175      *
1176      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
1177      */
1178     @Test
1179     public void test204ResponsesDoNotContainMessageBodies() throws Exception {
1180         originResponse = Proxies.enhanceResponse(
1181                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"));
1182         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1183 
1184         EasyMock.expect(
1185                 mockBackend.execute(
1186                         EasyMock.isA(HttpRoute.class),
1187                         EasyMock.isA(HttpRequestWrapper.class),
1188                         EasyMock.isA(HttpClientContext.class),
1189                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1190 
1191         replayMocks();
1192 
1193         final HttpResponse result = impl.execute(route, request, context, null);
1194 
1195         verifyMocks();
1196 
1197         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1198     }
1199 
1200     /*
1201      * "10.2.6 205 Reset Content ... The response MUST NOT include an entity."
1202      *
1203      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.6
1204      */
1205     @Test
1206     public void test205ResponsesDoNotContainMessageBodies() throws Exception {
1207         originResponse = Proxies.enhanceResponse(
1208                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_RESET_CONTENT, "Reset Content"));
1209         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1210 
1211         EasyMock.expect(
1212                 mockBackend.execute(
1213                         EasyMock.isA(HttpRoute.class),
1214                         EasyMock.isA(HttpRequestWrapper.class),
1215                         EasyMock.isA(HttpClientContext.class),
1216                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1217 
1218         replayMocks();
1219 
1220         final HttpResponse result = impl.execute(route, request, context, null);
1221 
1222         verifyMocks();
1223 
1224         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1225     }
1226 
1227     /*
1228      * "The [206] response MUST include the following header fields:
1229      *
1230      * - Either a Content-Range header field (section 14.16) indicating the
1231      * range included with this response, or a multipart/byteranges Content-Type
1232      * including Content-Range fields for each part. If a Content-Length header
1233      * field is present in the response, its value MUST match the actual number
1234      * of OCTETs transmitted in the message-body.
1235      *
1236      * - Date
1237      *
1238      * - ETag and/or Content-Location, if the header would have been sent in a
1239      * 200 response to the same request
1240      *
1241      * - Expires, Cache-Control, and/or Vary, if the field-value might differ
1242      * from that sent in any previous response for the same variant"
1243      *
1244      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1245      */
1246     @Test
1247     public void test206ResponseGeneratedFromCacheMustHaveContentRangeOrMultipartByteRangesContentType()
1248             throws Exception {
1249 
1250         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1251                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1252         final HttpResponse resp1 = HttpTestUtils.make200Response();
1253         resp1.setHeader("ETag", "\"etag\"");
1254         resp1.setHeader("Cache-Control", "max-age=3600");
1255 
1256         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1257                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1258         req2.setHeader("Range", "bytes=0-50");
1259 
1260         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1261 
1262         replayMocks();
1263         impl.execute(route, req1, context, null);
1264         final HttpResponse result = impl.execute(route, req2, context, null);
1265         verifyMocks();
1266 
1267         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1268             if (result.getFirstHeader("Content-Range") == null) {
1269                 final HeaderElement elt = result.getFirstHeader("Content-Type").getElements()[0];
1270                 Assert.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
1271                 Assert.assertNotNull(elt.getParameterByName("boundary"));
1272                 Assert.assertNotNull(elt.getParameterByName("boundary").getValue());
1273                 Assert.assertFalse("".equals(elt.getParameterByName("boundary").getValue().trim()));
1274             }
1275         }
1276     }
1277 
1278     @Test
1279     public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent()
1280             throws Exception {
1281 
1282         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1283                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1284         final HttpResponse resp1 = HttpTestUtils.make200Response();
1285         resp1.setHeader("ETag", "\"etag\"");
1286         resp1.setHeader("Cache-Control", "max-age=3600");
1287 
1288         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1289                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1290         req2.setHeader("Range", "bytes=0-50");
1291 
1292         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1293 
1294         replayMocks();
1295         impl.execute(route, req1, context, null);
1296         final HttpResponse result = impl.execute(route, req2, context, null);
1297         verifyMocks();
1298 
1299         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1300             final Header h = result.getFirstHeader("Content-Length");
1301             if (h != null) {
1302                 final int contentLength = Integer.parseInt(h.getValue());
1303                 int bytesRead = 0;
1304                 final InputStream i = result.getEntity().getContent();
1305                 while ((i.read()) != -1) {
1306                     bytesRead++;
1307                 }
1308                 i.close();
1309                 Assert.assertEquals(contentLength, bytesRead);
1310             }
1311         }
1312     }
1313 
1314     @Test
1315     public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
1316         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1317                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1318         final HttpResponse resp1 = HttpTestUtils.make200Response();
1319         resp1.setHeader("ETag", "\"etag\"");
1320         resp1.setHeader("Cache-Control", "max-age=3600");
1321 
1322         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1323                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1324         req2.setHeader("Range", "bytes=0-50");
1325 
1326         backendExpectsAnyRequestAndReturn(resp1).times(1, 2);
1327 
1328         replayMocks();
1329         impl.execute(route, req1, context, null);
1330         final HttpResponse result = impl.execute(route, req2, context, null);
1331         verifyMocks();
1332 
1333         if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
1334             Assert.assertNotNull(result.getFirstHeader("Date"));
1335         }
1336     }
1337 
1338     @Test
1339     public void test206ResponseReturnedToClientMustHaveDateHeader() throws Exception {
1340         request.addHeader("Range", "bytes=0-50");
1341         originResponse = Proxies.enhanceResponse(
1342                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content"));
1343         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1344         originResponse.setHeader("Server", "MockOrigin/1.0");
1345         originResponse.setEntity(HttpTestUtils.makeBody(500));
1346         originResponse.setHeader("Content-Range", "bytes 0-499/1234");
1347         originResponse.removeHeaders("Date");
1348 
1349         EasyMock.expect(
1350                 mockBackend.execute(
1351                         EasyMock.isA(HttpRoute.class),
1352                         EasyMock.isA(HttpRequestWrapper.class),
1353                         EasyMock.isA(HttpClientContext.class),
1354                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1355 
1356         replayMocks();
1357 
1358         final HttpResponse result = impl.execute(route, request, context, null);
1359         Assert.assertTrue(result.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT
1360                 || result.getFirstHeader("Date") != null);
1361 
1362         verifyMocks();
1363     }
1364 
1365     @Test
1366     public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
1367         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1368                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1369 
1370         originResponse.addHeader("Cache-Control", "max-age=3600");
1371         originResponse.addHeader("ETag", "\"etag1\"");
1372 
1373         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1374                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1375         req2.addHeader("Range", "bytes=0-50");
1376 
1377         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1378 
1379         replayMocks();
1380 
1381         impl.execute(route, req1, context, null);
1382         final HttpResponse result = impl.execute(route, req2, context, null);
1383 
1384         verifyMocks();
1385 
1386         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1387             Assert.assertNotNull(result.getFirstHeader("ETag"));
1388         }
1389     }
1390 
1391     @Test
1392     public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
1393         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1394                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1395 
1396         originResponse.addHeader("Cache-Control", "max-age=3600");
1397         originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
1398 
1399         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1400                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1401         req2.addHeader("Range", "bytes=0-50");
1402 
1403         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1404 
1405         replayMocks();
1406 
1407         impl.execute(route, req1, context, null);
1408         final HttpResponse result = impl.execute(route, req2, context, null);
1409 
1410         verifyMocks();
1411 
1412         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1413             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1414         }
1415     }
1416 
1417     @Test
1418     public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
1419 
1420         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1421                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1422         req1.addHeader("Accept-Encoding", "gzip");
1423 
1424         final Date now = new Date();
1425         final Date inOneHour = new Date(now.getTime() + 3600 * 1000L);
1426         originResponse.addHeader("Cache-Control", "max-age=3600");
1427         originResponse.addHeader("Expires", DateUtils.formatDate(inOneHour));
1428         originResponse.addHeader("Vary", "Accept-Encoding");
1429 
1430         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1431                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1432         req2.addHeader("Cache-Control", "no-cache");
1433         req2.addHeader("Accept-Encoding", "gzip");
1434         final Date nextSecond = new Date(now.getTime() + 1000L);
1435         final Date inTwoHoursPlusASec = new Date(now.getTime() + 2 * 3600 * 1000L + 1000L);
1436 
1437         final HttpResponse originResponse2 = HttpTestUtils.make200Response();
1438         originResponse2.setHeader("Date", DateUtils.formatDate(nextSecond));
1439         originResponse2.setHeader("Cache-Control", "max-age=7200");
1440         originResponse2.setHeader("Expires", DateUtils.formatDate(inTwoHoursPlusASec));
1441         originResponse2.setHeader("Vary", "Accept-Encoding");
1442 
1443         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1444                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1445         req3.addHeader("Range", "bytes=0-50");
1446         req3.addHeader("Accept-Encoding", "gzip");
1447 
1448         backendExpectsAnyRequest().andReturn(originResponse);
1449         backendExpectsAnyRequestAndReturn(originResponse2).times(1, 2);
1450 
1451         replayMocks();
1452 
1453         impl.execute(route, req1, context, null);
1454         impl.execute(route, req2, context, null);
1455         final HttpResponse result = impl.execute(route, req3, context, null);
1456 
1457         verifyMocks();
1458 
1459         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1460             Assert.assertNotNull(result.getFirstHeader("Expires"));
1461             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
1462             Assert.assertNotNull(result.getFirstHeader("Vary"));
1463         }
1464     }
1465 
1466     /*
1467      * "If the [206] response is the result of an If-Range request that used a
1468      * weak validator, the response MUST NOT include other entity-headers; this
1469      * prevents inconsistencies between cached entity-bodies and updated
1470      * headers."
1471      *
1472      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1473      */
1474     @Test
1475     public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders()
1476             throws Exception {
1477 
1478         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1479                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1480 
1481         final Date now = new Date();
1482         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1483         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
1484         originResponse.addHeader("Allow", "GET,HEAD");
1485         originResponse.addHeader("Cache-Control", "max-age=3600");
1486         originResponse.addHeader("Content-Language", "en");
1487         originResponse.addHeader("Content-Encoding", "x-coding");
1488         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1489         originResponse.addHeader("Content-Length", "128");
1490         originResponse.addHeader("Content-Type", "application/octet-stream");
1491         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1492         originResponse.addHeader("ETag", "W/\"weak-tag\"");
1493 
1494         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1495                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1496         req2.addHeader("If-Range", "W/\"weak-tag\"");
1497         req2.addHeader("Range", "bytes=0-50");
1498 
1499         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1500 
1501         replayMocks();
1502 
1503         impl.execute(route, req1, context, null);
1504         final HttpResponse result = impl.execute(route, req2, context, null);
1505 
1506         verifyMocks();
1507 
1508         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1509             Assert.assertNull(result.getFirstHeader("Allow"));
1510             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
1511             Assert.assertNull(result.getFirstHeader("Content-Language"));
1512             Assert.assertNull(result.getFirstHeader("Content-MD5"));
1513             Assert.assertNull(result.getFirstHeader("Last-Modified"));
1514         }
1515     }
1516 
1517     /*
1518      * "Otherwise, the [206] response MUST include all of the entity-headers
1519      * that would have been returned with a 200 (OK) response to the same
1520      * [If-Range] request."
1521      *
1522      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1523      */
1524     @Test
1525     public void test206ResponseToIfRangeWithStrongValidatorReturnsAllEntityHeaders()
1526             throws Exception {
1527 
1528         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1529                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1530 
1531         final Date now = new Date();
1532         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
1533         originResponse.addHeader("Allow", "GET,HEAD");
1534         originResponse.addHeader("Cache-Control", "max-age=3600");
1535         originResponse.addHeader("Content-Language", "en");
1536         originResponse.addHeader("Content-Encoding", "x-coding");
1537         originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
1538         originResponse.addHeader("Content-Length", "128");
1539         originResponse.addHeader("Content-Type", "application/octet-stream");
1540         originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1541         originResponse.addHeader("ETag", "\"strong-tag\"");
1542 
1543         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1544                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1545         req2.addHeader("If-Range", "\"strong-tag\"");
1546         req2.addHeader("Range", "bytes=0-50");
1547 
1548         backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
1549 
1550         replayMocks();
1551 
1552         impl.execute(route, req1, context, null);
1553         final HttpResponse result = impl.execute(route, req2, context, null);
1554 
1555         verifyMocks();
1556 
1557         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
1558             Assert.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
1559             Assert.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
1560             Assert.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
1561             Assert.assertEquals("x-coding", result.getFirstHeader("Content-Encoding").getValue());
1562             Assert.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
1563                     .getValue());
1564             Assert.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
1565                     .getFirstHeader("Last-Modified").getValue());
1566         }
1567     }
1568 
1569     /*
1570      * "A cache MUST NOT combine a 206 response with other previously cached
1571      * content if the ETag or Last-Modified headers do not match exactly, see
1572      * 13.5.4."
1573      *
1574      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1575      */
1576     @Test
1577     public void test206ResponseIsNotCombinedWithPreviousContentIfETagDoesNotMatch()
1578             throws Exception {
1579 
1580         final Date now = new Date();
1581 
1582         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1583                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1584         final HttpResponse resp1 = HttpTestUtils.make200Response();
1585         resp1.setHeader("Cache-Control", "max-age=3600");
1586         resp1.setHeader("ETag", "\"etag1\"");
1587         final byte[] bytes1 = new byte[128];
1588         Arrays.fill(bytes1, (byte) 1);
1589         resp1.setEntity(new ByteArrayEntity(bytes1));
1590 
1591         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1592                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1593         req2.setHeader("Cache-Control", "no-cache");
1594         req2.setHeader("Range", "bytes=0-50");
1595 
1596         final Date inOneSecond = new Date(now.getTime() + 1000L);
1597         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1598                 "Partial Content");
1599         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1600         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1601         resp2.setHeader("ETag", "\"etag2\"");
1602         resp2.setHeader("Content-Range", "bytes 0-50/128");
1603         final byte[] bytes2 = new byte[51];
1604         Arrays.fill(bytes2, (byte) 2);
1605         resp2.setEntity(new ByteArrayEntity(bytes2));
1606 
1607         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1608         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1609                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1610         final HttpResponse resp3 = HttpTestUtils.make200Response();
1611         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1612         resp3.setHeader("Cache-Control", "max-age=3600");
1613         resp3.setHeader("ETag", "\"etag2\"");
1614         final byte[] bytes3 = new byte[128];
1615         Arrays.fill(bytes3, (byte) 2);
1616         resp3.setEntity(new ByteArrayEntity(bytes3));
1617 
1618         EasyMock.expect(
1619                 mockBackend.execute(
1620                         EasyMock.isA(HttpRoute.class),
1621                         EasyMock.isA(HttpRequestWrapper.class),
1622                         EasyMock.isA(HttpClientContext.class),
1623                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1624                                 Proxies.enhanceResponse(resp1));
1625         EasyMock.expect(
1626                 mockBackend.execute(
1627                         EasyMock.isA(HttpRoute.class),
1628                         EasyMock.isA(HttpRequestWrapper.class),
1629                         EasyMock.isA(HttpClientContext.class),
1630                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1631                                 Proxies.enhanceResponse(resp2));
1632         EasyMock.expect(
1633                 mockBackend.execute(
1634                         EasyMock.isA(HttpRoute.class),
1635                         EasyMock.isA(HttpRequestWrapper.class),
1636                         EasyMock.isA(HttpClientContext.class),
1637                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1638                                 Proxies.enhanceResponse(resp3)).times(0, 1);
1639         replayMocks();
1640 
1641         impl.execute(route, req1, context, null);
1642         impl.execute(route, req2, context, null);
1643         final HttpResponse result = impl.execute(route, req3, context, null);
1644 
1645         verifyMocks();
1646 
1647         final InputStream i = result.getEntity().getContent();
1648         int b;
1649         boolean found1 = false;
1650         boolean found2 = false;
1651         while ((b = i.read()) != -1) {
1652             if (b == 1) {
1653                 found1 = true;
1654             }
1655             if (b == 2) {
1656                 found2 = true;
1657             }
1658         }
1659         i.close();
1660         Assert.assertFalse(found1 && found2); // mixture of content
1661     }
1662 
1663     @Test
1664     public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch()
1665             throws Exception {
1666 
1667         final Date now = new Date();
1668 
1669         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1670                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1671         final HttpResponse resp1 = HttpTestUtils.make200Response();
1672         final Date oneHourAgo = new Date(now.getTime() - 3600L);
1673         resp1.setHeader("Cache-Control", "max-age=3600");
1674         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
1675         final byte[] bytes1 = new byte[128];
1676         Arrays.fill(bytes1, (byte) 1);
1677         resp1.setEntity(new ByteArrayEntity(bytes1));
1678 
1679         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1680                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1681         req2.setHeader("Cache-Control", "no-cache");
1682         req2.setHeader("Range", "bytes=0-50");
1683 
1684         final Date inOneSecond = new Date(now.getTime() + 1000L);
1685         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1686                 "Partial Content");
1687         resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
1688         resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
1689         resp2.setHeader("Last-Modified", DateUtils.formatDate(now));
1690         resp2.setHeader("Content-Range", "bytes 0-50/128");
1691         final byte[] bytes2 = new byte[51];
1692         Arrays.fill(bytes2, (byte) 2);
1693         resp2.setEntity(new ByteArrayEntity(bytes2));
1694 
1695         final Date inTwoSeconds = new Date(now.getTime() + 2000L);
1696         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
1697                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1698         final HttpResponse resp3 = HttpTestUtils.make200Response();
1699         resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
1700         resp3.setHeader("Cache-Control", "max-age=3600");
1701         resp3.setHeader("ETag", "\"etag2\"");
1702         final byte[] bytes3 = new byte[128];
1703         Arrays.fill(bytes3, (byte) 2);
1704         resp3.setEntity(new ByteArrayEntity(bytes3));
1705 
1706         EasyMock.expect(
1707                 mockBackend.execute(
1708                         EasyMock.isA(HttpRoute.class),
1709                         EasyMock.isA(HttpRequestWrapper.class),
1710                         EasyMock.isA(HttpClientContext.class),
1711                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1712                                 Proxies.enhanceResponse(resp1));
1713         EasyMock.expect(
1714                 mockBackend.execute(
1715                         EasyMock.isA(HttpRoute.class),
1716                         EasyMock.isA(HttpRequestWrapper.class),
1717                         EasyMock.isA(HttpClientContext.class),
1718                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1719                                 Proxies.enhanceResponse(resp2));
1720         EasyMock.expect(
1721                 mockBackend.execute(
1722                         EasyMock.isA(HttpRoute.class),
1723                         EasyMock.isA(HttpRequestWrapper.class),
1724                         EasyMock.isA(HttpClientContext.class),
1725                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
1726                                 Proxies.enhanceResponse(resp3)).times(0, 1);
1727         replayMocks();
1728 
1729         impl.execute(route, req1, context, null);
1730         impl.execute(route, req2, context, null);
1731         final HttpResponse result = impl.execute(route, req3, context, null);
1732 
1733         verifyMocks();
1734 
1735         final InputStream i = result.getEntity().getContent();
1736         int b;
1737         boolean found1 = false;
1738         boolean found2 = false;
1739         while ((b = i.read()) != -1) {
1740             if (b == 1) {
1741                 found1 = true;
1742             }
1743             if (b == 2) {
1744                 found2 = true;
1745             }
1746         }
1747         i.close();
1748         Assert.assertFalse(found1 && found2); // mixture of content
1749     }
1750 
1751     /*
1752      * "A cache that does not support the Range and Content-Range headers MUST
1753      * NOT cache 206 (Partial) responses."
1754      *
1755      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
1756      */
1757     @Test
1758     public void test206ResponsesAreNotCachedIfTheCacheDoesNotSupportRangeAndContentRangeHeaders()
1759             throws Exception {
1760 
1761         if (!supportsRangeAndContentRangeHeaders(impl)) {
1762             emptyMockCacheExpectsNoPuts();
1763 
1764             request = HttpRequestWrapper.wrap(
1765                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1766             request.addHeader("Range", "bytes=0-50");
1767 
1768             originResponse = Proxies.enhanceResponse(
1769                     new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
1770                     "Partial Content"));
1771             originResponse.setHeader("Content-Range", "bytes 0-50/128");
1772             originResponse.setHeader("Cache-Control", "max-age=3600");
1773             final byte[] bytes = new byte[51];
1774             new Random().nextBytes(bytes);
1775             originResponse.setEntity(new ByteArrayEntity(bytes));
1776 
1777             EasyMock.expect(
1778                     mockBackend.execute(
1779                             EasyMock.isA(HttpRoute.class),
1780                             EasyMock.isA(HttpRequestWrapper.class),
1781                             EasyMock.isA(HttpClientContext.class),
1782                             EasyMock.<HttpExecutionAware>isNull())).andReturn(
1783                                     originResponse);
1784 
1785             replayMocks();
1786             impl.execute(route, request, context, null);
1787             verifyMocks();
1788         }
1789     }
1790 
1791     /*
1792      * "10.3.4 303 See Other ... The 303 response MUST NOT be cached, but the
1793      * response to the second (redirected) request might be cacheable."
1794      *
1795      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
1796      */
1797     @Test
1798     public void test303ResponsesAreNotCached() throws Exception {
1799         emptyMockCacheExpectsNoPuts();
1800 
1801         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1802 
1803         originResponse = Proxies.enhanceResponse(
1804                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_SEE_OTHER, "See Other"));
1805         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1806         originResponse.setHeader("Server", "MockServer/1.0");
1807         originResponse.setHeader("Cache-Control", "max-age=3600");
1808         originResponse.setHeader("Content-Type", "application/x-cachingclient-test");
1809         originResponse.setHeader("Location", "http://foo.example.com/other");
1810         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1811 
1812         EasyMock.expect(
1813                 mockBackend.execute(
1814                         EasyMock.isA(HttpRoute.class),
1815                         EasyMock.isA(HttpRequestWrapper.class),
1816                         EasyMock.isA(HttpClientContext.class),
1817                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1818 
1819         replayMocks();
1820         impl.execute(route, request, context, null);
1821         verifyMocks();
1822     }
1823 
1824     /*
1825      * "The 304 response MUST NOT contain a message-body, and thus is always
1826      * terminated by the first empty line after the header fields."
1827      *
1828      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1829      */
1830     @Test
1831     public void test304ResponseDoesNotContainABody() throws Exception {
1832         request.setHeader("If-None-Match", "\"etag\"");
1833 
1834         originResponse = Proxies.enhanceResponse(
1835                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
1836                         "Not Modified"));
1837         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1838         originResponse.setHeader("Server", "MockServer/1.0");
1839         originResponse.setHeader("Content-Length", "128");
1840         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
1841 
1842         EasyMock.expect(
1843                 mockBackend.execute(
1844                         EasyMock.isA(HttpRoute.class),
1845                         EasyMock.isA(HttpRequestWrapper.class),
1846                         EasyMock.isA(HttpClientContext.class),
1847                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1848 
1849         replayMocks();
1850 
1851         final HttpResponse result = impl.execute(route, request, context, null);
1852 
1853         verifyMocks();
1854 
1855         Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
1856     }
1857 
1858     /*
1859      * "The [304] response MUST include the following header fields: - Date,
1860      * unless its omission is required by section 14.18.1 [clockless origin
1861      * servers]."
1862      *
1863      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1864      */
1865     @Test
1866     public void test304ResponseWithDateHeaderForwardedFromOriginIncludesDateHeader()
1867             throws Exception {
1868 
1869         request.setHeader("If-None-Match", "\"etag\"");
1870 
1871         originResponse = Proxies.enhanceResponse(
1872                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
1873                         "Not Modified"));
1874         originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
1875         originResponse.setHeader("Server", "MockServer/1.0");
1876         originResponse.setHeader("ETag", "\"etag\"");
1877 
1878         EasyMock.expect(
1879                 mockBackend.execute(
1880                         EasyMock.isA(HttpRoute.class),
1881                         EasyMock.isA(HttpRequestWrapper.class),
1882                         EasyMock.isA(HttpClientContext.class),
1883                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
1884         replayMocks();
1885 
1886         final HttpResponse result = impl.execute(route, request, context, null);
1887 
1888         verifyMocks();
1889         Assert.assertNotNull(result.getFirstHeader("Date"));
1890     }
1891 
1892     @Test
1893     public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
1894 
1895         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1896                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1897         originResponse.setHeader("Cache-Control", "max-age=3600");
1898         originResponse.setHeader("ETag", "\"etag\"");
1899 
1900         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1901                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1902         req2.setHeader("If-None-Match", "\"etag\"");
1903 
1904         EasyMock.expect(
1905                 mockBackend.execute(
1906                         EasyMock.isA(HttpRoute.class),
1907                         EasyMock.isA(HttpRequestWrapper.class),
1908                         EasyMock.isA(HttpClientContext.class),
1909                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1910         replayMocks();
1911 
1912         impl.execute(route, req1, context, null);
1913         final HttpResponse result = impl.execute(route, req2, context, null);
1914 
1915         verifyMocks();
1916         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1917             Assert.assertNotNull(result.getFirstHeader("Date"));
1918         }
1919     }
1920 
1921     /*
1922      * "The [304] response MUST include the following header fields: - ETag
1923      * and/or Content-Location, if the header would have been sent in a 200
1924      * response to the same request."
1925      *
1926      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1927      */
1928     @Test
1929     public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
1930         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1931                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1932         originResponse.setHeader("Cache-Control", "max-age=3600");
1933         originResponse.setHeader("ETag", "\"etag\"");
1934 
1935         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1936                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1937         req2.setHeader("If-None-Match", "\"etag\"");
1938 
1939         EasyMock.expect(
1940                 mockBackend.execute(
1941                         EasyMock.isA(HttpRoute.class),
1942                         EasyMock.isA(HttpRequestWrapper.class),
1943                         EasyMock.isA(HttpClientContext.class),
1944                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1945         replayMocks();
1946 
1947         impl.execute(route, req1, context, null);
1948         final HttpResponse result = impl.execute(route, req2, context, null);
1949 
1950         verifyMocks();
1951         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1952             Assert.assertNotNull(result.getFirstHeader("ETag"));
1953         }
1954     }
1955 
1956     @Test
1957     public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid()
1958             throws Exception {
1959         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
1960                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1961         originResponse.setHeader("Cache-Control", "max-age=3600");
1962         originResponse.setHeader("Content-Location", "http://foo.example.com/other");
1963         originResponse.setHeader("ETag", "\"etag\"");
1964 
1965         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
1966                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
1967         req2.setHeader("If-None-Match", "\"etag\"");
1968 
1969         EasyMock.expect(
1970                 mockBackend.execute(
1971                         EasyMock.isA(HttpRoute.class),
1972                         EasyMock.isA(HttpRequestWrapper.class),
1973                         EasyMock.isA(HttpClientContext.class),
1974                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(1, 2);
1975         replayMocks();
1976 
1977         impl.execute(route, req1, context, null);
1978         final HttpResponse result = impl.execute(route, req2, context, null);
1979 
1980         verifyMocks();
1981         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
1982             Assert.assertNotNull(result.getFirstHeader("Content-Location"));
1983         }
1984     }
1985 
1986     /*
1987      * "The [304] response MUST include the following header fields: ... -
1988      * Expires, Cache-Control, and/or Vary, if the field-value might differ from
1989      * that sent in any previous response for the same variant
1990      *
1991      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
1992      */
1993     @Test
1994     public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer()
1995             throws Exception {
1996 
1997         final Date now = new Date();
1998         final Date inTwoHours = new Date(now.getTime() + 2 * 3600 * 1000L);
1999 
2000         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2001                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2002         req1.setHeader("Accept-Encoding", "gzip");
2003 
2004         final HttpResponse resp1 = HttpTestUtils.make200Response();
2005         resp1.setHeader("ETag", "\"v1\"");
2006         resp1.setHeader("Cache-Control", "max-age=7200");
2007         resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours));
2008         resp1.setHeader("Vary", "Accept-Encoding");
2009         resp1.setEntity(HttpTestUtils.makeBody(entityLength));
2010 
2011         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2012                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2013         req1.setHeader("Accept-Encoding", "gzip");
2014         req1.setHeader("Cache-Control", "no-cache");
2015 
2016         final HttpResponse resp2 = HttpTestUtils.make200Response();
2017         resp2.setHeader("ETag", "\"v2\"");
2018         resp2.setHeader("Cache-Control", "max-age=3600");
2019         resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours));
2020         resp2.setHeader("Vary", "Accept-Encoding");
2021         resp2.setEntity(HttpTestUtils.makeBody(entityLength));
2022 
2023         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
2024                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2025         req3.setHeader("Accept-Encoding", "gzip");
2026         req3.setHeader("If-None-Match", "\"v2\"");
2027 
2028         EasyMock.expect(
2029                 mockBackend.execute(
2030                         EasyMock.isA(HttpRoute.class),
2031                         EasyMock.isA(HttpRequestWrapper.class),
2032                         EasyMock.isA(HttpClientContext.class),
2033                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2034                                 Proxies.enhanceResponse(resp1));
2035         EasyMock.expect(
2036                 mockBackend.execute(
2037                         EasyMock.isA(HttpRoute.class),
2038                         EasyMock.isA(HttpRequestWrapper.class),
2039                         EasyMock.isA(HttpClientContext.class),
2040                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2041                                 Proxies.enhanceResponse(resp2)).times(1, 2);
2042         replayMocks();
2043 
2044         impl.execute(route, req1, context, null);
2045         impl.execute(route, req2, context, null);
2046         final HttpResponse result = impl.execute(route, req3, context, null);
2047 
2048         verifyMocks();
2049 
2050         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
2051             Assert.assertNotNull(result.getFirstHeader("Expires"));
2052             Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
2053             Assert.assertNotNull(result.getFirstHeader("Vary"));
2054         }
2055     }
2056 
2057     /*
2058      * "Otherwise (i.e., the conditional GET used a weak validator), the
2059      * response MUST NOT include other entity-headers; this prevents
2060      * inconsistencies between cached entity-bodies and updated headers."
2061      *
2062      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2063      */
2064     @Test
2065     public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders()
2066             throws Exception {
2067 
2068         final Date now = new Date();
2069         final Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
2070 
2071         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2072                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2073 
2074         final HttpResponse resp1 = HttpTestUtils.make200Response();
2075         resp1.setHeader("ETag", "W/\"v1\"");
2076         resp1.setHeader("Allow", "GET,HEAD");
2077         resp1.setHeader("Content-Encoding", "x-coding");
2078         resp1.setHeader("Content-Language", "en");
2079         resp1.setHeader("Content-Length", "128");
2080         resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
2081         resp1.setHeader("Content-Type", "application/octet-stream");
2082         resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
2083         resp1.setHeader("Cache-Control", "max-age=7200");
2084 
2085         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2086                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2087         req2.setHeader("If-None-Match", "W/\"v1\"");
2088 
2089         EasyMock.expect(
2090                 mockBackend.execute(
2091                         EasyMock.isA(HttpRoute.class),
2092                         EasyMock.isA(HttpRequestWrapper.class),
2093                         EasyMock.isA(HttpClientContext.class),
2094                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2095                                 Proxies.enhanceResponse(resp1)).times(1, 2);
2096         replayMocks();
2097 
2098         impl.execute(route, req1, context, null);
2099         final HttpResponse result = impl.execute(route, req2, context, null);
2100 
2101         verifyMocks();
2102 
2103         if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
2104             Assert.assertNull(result.getFirstHeader("Allow"));
2105             Assert.assertNull(result.getFirstHeader("Content-Encoding"));
2106             Assert.assertNull(result.getFirstHeader("Content-Length"));
2107             Assert.assertNull(result.getFirstHeader("Content-MD5"));
2108             Assert.assertNull(result.getFirstHeader("Content-Type"));
2109             Assert.assertNull(result.getFirstHeader("Last-Modified"));
2110         }
2111     }
2112 
2113     /*
2114      * "If a 304 response indicates an entity not currently cached, then the
2115      * cache MUST disregard the response and repeat the request without the
2116      * conditional."
2117      *
2118      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2119      */
2120     @Test
2121     public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET()
2122             throws Exception {
2123 
2124         final Date now = new Date();
2125 
2126         // load cache with cacheable entry
2127         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2128                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2129         final HttpResponse resp1 = HttpTestUtils.make200Response();
2130         resp1.setHeader("ETag", "\"etag1\"");
2131         resp1.setHeader("Cache-Control", "max-age=3600");
2132 
2133         // force a revalidation
2134         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2135                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2136         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
2137 
2138         // updated ETag provided to a conditional revalidation
2139         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2140                 "Not Modified");
2141         resp2.setHeader("Date", DateUtils.formatDate(now));
2142         resp2.setHeader("Server", "MockServer/1.0");
2143         resp2.setHeader("ETag", "\"etag2\"");
2144 
2145         // conditional validation uses If-None-Match
2146         final HttpRequestWrapper conditionalValidation = HttpRequestWrapper.wrap(
2147                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2148         conditionalValidation.setHeader("If-None-Match", "\"etag1\"");
2149 
2150         // unconditional validation doesn't use If-None-Match
2151         final HttpRequestWrapper unconditionalValidation = HttpRequestWrapper.wrap(
2152                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2153         // new response to unconditional validation provides new body
2154         final HttpResponse resp3 = HttpTestUtils.make200Response();
2155         resp1.setHeader("ETag", "\"etag2\"");
2156         resp1.setHeader("Cache-Control", "max-age=3600");
2157 
2158         EasyMock.expect(
2159                 mockBackend.execute(
2160                         EasyMock.isA(HttpRoute.class),
2161                         EasyMock.isA(HttpRequestWrapper.class),
2162                         EasyMock.isA(HttpClientContext.class),
2163                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2164                                 Proxies.enhanceResponse(resp1));
2165         // this next one will happen once if the cache tries to
2166         // conditionally validate, zero if it goes full revalidation
2167         EasyMock.expect(
2168                 mockBackend.execute(
2169                         EasyMock.eq(route),
2170                         eqRequest(conditionalValidation),
2171                         EasyMock.isA(HttpClientContext.class),
2172                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2173                                 Proxies.enhanceResponse(resp2)).times(0, 1);
2174         EasyMock.expect(
2175                 mockBackend.execute(
2176                         EasyMock.eq(route),
2177                         eqRequest(unconditionalValidation),
2178                         EasyMock.isA(HttpClientContext.class),
2179                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2180                                 Proxies.enhanceResponse(resp3));
2181         replayMocks();
2182 
2183         impl.execute(route, req1, context, null);
2184         impl.execute(route, req2, context, null);
2185 
2186         verifyMocks();
2187     }
2188 
2189     /*
2190      * "If a cache uses a received 304 response to update a cache entry, the
2191      * cache MUST update the entry to reflect any new field values given in the
2192      * response.
2193      *
2194      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
2195      */
2196     @Test
2197     public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
2198 
2199         final Date now = new Date();
2200         final Date inFiveSeconds = new Date(now.getTime() + 5000L);
2201 
2202         final HttpRequestWrapper initialRequest = HttpRequestWrapper.wrap(
2203                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2204 
2205         final HttpResponse cachedResponse = HttpTestUtils.make200Response();
2206         cachedResponse.setHeader("Cache-Control", "max-age=3600");
2207         cachedResponse.setHeader("ETag", "\"etag\"");
2208 
2209         final HttpRequestWrapper secondRequest = HttpRequestWrapper.wrap(
2210                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2211         secondRequest.setHeader("Cache-Control", "max-age=0,max-stale=0");
2212 
2213         final HttpRequestWrapper conditionalValidationRequest = HttpRequestWrapper.wrap(
2214                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2215         conditionalValidationRequest.setHeader("If-None-Match", "\"etag\"");
2216 
2217         final HttpRequestWrapper unconditionalValidationRequest = HttpRequestWrapper.wrap(
2218                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2219 
2220         // to be used if the cache generates a conditional validation
2221         final HttpResponse conditionalResponse = new BasicHttpResponse(
2222                 HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
2223         conditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
2224         conditionalResponse.setHeader("Server", "MockUtils/1.0");
2225         conditionalResponse.setHeader("ETag", "\"etag\"");
2226         conditionalResponse.setHeader("X-Extra", "junk");
2227 
2228         // to be used if the cache generates an unconditional validation
2229         final HttpResponse unconditionalResponse = HttpTestUtils.make200Response();
2230         unconditionalResponse.setHeader("Date", DateUtils.formatDate(inFiveSeconds));
2231         unconditionalResponse.setHeader("ETag", "\"etag\"");
2232 
2233         final Capture<HttpRequestWrapper> cap1 = new Capture<HttpRequestWrapper>();
2234         final Capture<HttpRequestWrapper> cap2 = new Capture<HttpRequestWrapper>();
2235 
2236         EasyMock.expect(
2237                 mockBackend.execute(
2238                         EasyMock.isA(HttpRoute.class),
2239                         EasyMock.isA(HttpRequestWrapper.class),
2240                         EasyMock.isA(HttpClientContext.class),
2241                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2242                                 Proxies.enhanceResponse(cachedResponse));
2243         EasyMock.expect(
2244                 mockBackend.execute(
2245                         EasyMock.eq(route),
2246                         EasyMock.and(eqRequest(conditionalValidationRequest), EasyMock.capture(cap1)),
2247                         EasyMock.isA(HttpClientContext.class),
2248                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2249                                 Proxies.enhanceResponse(conditionalResponse)).times(0, 1);
2250         EasyMock.expect(
2251                 mockBackend.execute(
2252                         EasyMock.eq(route),
2253                         EasyMock.and(eqRequest(unconditionalValidationRequest), EasyMock.capture(cap2)),
2254                         EasyMock.isA(HttpClientContext.class),
2255                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2256                                 Proxies.enhanceResponse(unconditionalResponse)).times(0, 1);
2257 
2258         replayMocks();
2259 
2260         impl.execute(route, initialRequest, context, null);
2261         final HttpResponse result = impl.execute(route, secondRequest, context, null);
2262 
2263         verifyMocks();
2264 
2265         Assert.assertTrue((cap1.hasCaptured() && !cap2.hasCaptured())
2266                 || (!cap1.hasCaptured() && cap2.hasCaptured()));
2267 
2268         if (cap1.hasCaptured()) {
2269             Assert.assertEquals(DateUtils.formatDate(inFiveSeconds), result.getFirstHeader("Date")
2270                     .getValue());
2271             Assert.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
2272         }
2273     }
2274 
2275     /*
2276      * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
2277      * header field (section 14.47) containing a challenge applicable to the
2278      * requested resource."
2279      *
2280      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
2281      */
2282     @Test
2283     public void testMustIncludeWWWAuthenticateHeaderOnAnOrigin401Response() throws Exception {
2284         originResponse = Proxies.enhanceResponse(
2285                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 401, "Unauthorized"));
2286         originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
2287 
2288         EasyMock.expect(
2289                 mockBackend.execute(
2290                         EasyMock.isA(HttpRoute.class),
2291                         EasyMock.isA(HttpRequestWrapper.class),
2292                         EasyMock.isA(HttpClientContext.class),
2293                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2294         replayMocks();
2295 
2296         final HttpResponse result = impl.execute(route, request, context, null);
2297         if (result.getStatusLine().getStatusCode() == 401) {
2298             Assert.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
2299         }
2300 
2301         verifyMocks();
2302     }
2303 
2304     /*
2305      * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
2306      * header containing a list of valid methods for the requested resource.
2307      *
2308      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
2309      */
2310     @Test
2311     public void testMustIncludeAllowHeaderFromAnOrigin405Response() throws Exception {
2312         originResponse = Proxies.enhanceResponse(
2313                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 405, "Method Not Allowed"));
2314         originResponse.setHeader("Allow", "GET, HEAD");
2315 
2316         backendExpectsAnyRequest().andReturn(originResponse);
2317 
2318         replayMocks();
2319 
2320         final HttpResponse result = impl.execute(route, request, context, null);
2321         if (result.getStatusLine().getStatusCode() == 405) {
2322             Assert.assertNotNull(result.getFirstHeader("Allow"));
2323         }
2324 
2325         verifyMocks();
2326     }
2327 
2328     /*
2329      * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
2330      * Proxy-Authenticate header field (section 14.33) containing a challenge
2331      * applicable to the proxy for the requested resource."
2332      *
2333      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
2334      */
2335     @Test
2336     public void testMustIncludeProxyAuthenticateHeaderFromAnOrigin407Response() throws Exception {
2337         originResponse = Proxies.enhanceResponse(
2338                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 407, "Proxy Authentication Required"));
2339         originResponse.setHeader("Proxy-Authenticate", "x-scheme x-param");
2340 
2341         EasyMock.expect(
2342                 mockBackend.execute(
2343                         EasyMock.isA(HttpRoute.class),
2344                         EasyMock.isA(HttpRequestWrapper.class),
2345                         EasyMock.isA(HttpClientContext.class),
2346                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2347         replayMocks();
2348 
2349         final HttpResponse result = impl.execute(route, request, context, null);
2350         if (result.getStatusLine().getStatusCode() == 407) {
2351             Assert.assertNotNull(result.getFirstHeader("Proxy-Authenticate"));
2352         }
2353 
2354         verifyMocks();
2355     }
2356 
2357     /*
2358      * "10.4.17 416 Requested Range Not Satisfiable ... This response MUST NOT
2359      * use the multipart/byteranges content-type."
2360      *
2361      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
2362      */
2363     @Test
2364     public void testMustNotAddMultipartByteRangeContentTypeTo416Response() throws Exception {
2365         originResponse = Proxies.enhanceResponse(
2366                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 416, "Requested Range Not Satisfiable"));
2367 
2368         EasyMock.expect(
2369                 mockBackend.execute(
2370                         EasyMock.isA(HttpRoute.class),
2371                         EasyMock.isA(HttpRequestWrapper.class),
2372                         EasyMock.isA(HttpClientContext.class),
2373                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2374 
2375         replayMocks();
2376         final HttpResponse result = impl.execute(route, request, context, null);
2377         verifyMocks();
2378 
2379         if (result.getStatusLine().getStatusCode() == 416) {
2380             for (final Header h : result.getHeaders("Content-Type")) {
2381                 for (final HeaderElement elt : h.getElements()) {
2382                     Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2383                 }
2384             }
2385         }
2386     }
2387 
2388     @Test
2389     public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses()
2390             throws Exception {
2391 
2392         originResponse.setEntity(HttpTestUtils.makeBody(entityLength));
2393         originResponse.setHeader("Content-Length", "128");
2394         originResponse.setHeader("Cache-Control", "max-age=3600");
2395 
2396         final HttpRequestWrapper rangeReq = HttpRequestWrapper.wrap(
2397                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2398         rangeReq.setHeader("Range", "bytes=1000-1200");
2399 
2400         final HttpResponse orig416 = new BasicHttpResponse(HttpVersion.HTTP_1_1, 416,
2401                 "Requested Range Not Satisfiable");
2402 
2403         EasyMock.expect(
2404                 mockBackend.execute(
2405                         EasyMock.isA(HttpRoute.class),
2406                         EasyMock.isA(HttpRequestWrapper.class),
2407                         EasyMock.isA(HttpClientContext.class),
2408                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
2409         // cache may 416 me right away if it understands byte ranges,
2410         // ok to delegate to origin though
2411         EasyMock.expect(
2412                 mockBackend.execute(
2413                         EasyMock.isA(HttpRoute.class),
2414                         EasyMock.isA(HttpRequestWrapper.class),
2415                         EasyMock.isA(HttpClientContext.class),
2416                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2417                                 Proxies.enhanceResponse(orig416)).times(0, 1);
2418 
2419         replayMocks();
2420         impl.execute(route, request, context, null);
2421         final HttpResponse result = impl.execute(route, rangeReq, context, null);
2422         verifyMocks();
2423 
2424         // might have gotten a 416 from the origin or the cache
2425         if (result.getStatusLine().getStatusCode() == 416) {
2426             for (final Header h : result.getHeaders("Content-Type")) {
2427                 for (final HeaderElement elt : h.getElements()) {
2428                     Assert.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
2429                 }
2430             }
2431         }
2432     }
2433 
2434     /*
2435      * "A correct cache MUST respond to a request with the most up-to-date
2436      * response held by the cache that is appropriate to the request (see
2437      * sections 13.2.5, 13.2.6, and 13.12) which meets one of the following
2438      * conditions:
2439      *
2440      * 1. It has been checked for equivalence with what the origin server would
2441      * have returned by revalidating the response with the origin server
2442      * (section 13.3);
2443      *
2444      * 2. It is "fresh enough" (see section 13.2). In the default case, this
2445      * means it meets the least restrictive freshness requirement of the client,
2446      * origin server, and cache (see section 14.9); if the origin server so
2447      * specifies, it is the freshness requirement of the origin server alone.
2448      *
2449      * If a stored response is not "fresh enough" by the most restrictive
2450      * freshness requirement of both the client and the origin server, in
2451      * carefully considered circumstances the cache MAY still return the
2452      * response with the appropriate Warning header (see section 13.1.5 and
2453      * 14.46), unless such a response is prohibited (e.g., by a "no-store"
2454      * cache-directive, or by a "no-cache" cache-request-directive; see section
2455      * 14.9).
2456      *
2457      * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect), or
2458      * error (4xx or 5xx) response message."
2459      *
2460      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2461      */
2462     @Test
2463     public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
2464 
2465         final Date now = new Date();
2466         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2467         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2468         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2469 
2470         final Header[] hdrs = new Header[] {
2471                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2472                 new BasicHeader("Cache-Control", "max-age=0"),
2473                 new BasicHeader("ETag", "\"etag\""),
2474                 new BasicHeader("Content-Length", "128")
2475         };
2476 
2477         final byte[] bytes = new byte[128];
2478         new Random().nextBytes(bytes);
2479 
2480         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2481 
2482         impl = new CachingExec(mockBackend, mockCache, config);
2483 
2484         request = HttpRequestWrapper.wrap(
2485                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2486 
2487         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2488                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2489         validate.setHeader("If-None-Match", "\"etag\"");
2490 
2491         final CloseableHttpResponse notModified = Proxies.enhanceResponse(
2492                 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified"));
2493         notModified.setHeader("Date", DateUtils.formatDate(now));
2494         notModified.setHeader("ETag", "\"etag\"");
2495 
2496         EasyMock.expect(
2497                 mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request)))
2498                 .andReturn(entry);
2499         EasyMock.expect(
2500                 mockBackend.execute(
2501                         EasyMock.eq(route),
2502                         eqRequest(validate),
2503                         EasyMock.isA(HttpClientContext.class),
2504                         EasyMock.<HttpExecutionAware>isNull())).andReturn(notModified);
2505         EasyMock.expect(mockCache.updateCacheEntry(
2506                 EasyMock.eq(host),
2507                 eqRequest(request),
2508                 EasyMock.eq(entry),
2509                 eqResponse(notModified),
2510                 EasyMock.isA(Date.class),
2511                 EasyMock.isA(Date.class)))
2512             .andReturn(HttpTestUtils.makeCacheEntry());
2513 
2514         replayMocks();
2515         impl.execute(route, request, context, null);
2516         verifyMocks();
2517     }
2518 
2519     @Test
2520     public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
2521 
2522         final Date now = new Date();
2523         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2524         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2525         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2526 
2527         final Header[] hdrs = new Header[] {
2528                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2529                 new BasicHeader("Cache-Control", "max-age=3600"),
2530                 new BasicHeader("Content-Length", "128")
2531         };
2532 
2533         final byte[] bytes = new byte[128];
2534         new Random().nextBytes(bytes);
2535 
2536         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2537 
2538         impl = new CachingExec(mockBackend, mockCache, config);
2539         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2540 
2541         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2542 
2543         replayMocks();
2544         final HttpResponse result = impl.execute(route, request, context, null);
2545         verifyMocks();
2546 
2547         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2548     }
2549 
2550     /*
2551      * "If the cache can not communicate with the origin server, then a correct
2552      * cache SHOULD respond as above if the response can be correctly served
2553      * from the cache; if not it MUST return an error or warning indicating that
2554      * there was a communication failure."
2555      *
2556      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
2557      *
2558      * "111 Revalidation failed MUST be included if a cache returns a stale
2559      * response because an attempt to revalidate the response failed, due to an
2560      * inability to reach the server."
2561      *
2562      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2563      */
2564     @Test
2565     public void testMustServeAppropriateErrorOrWarningIfNoOriginCommunicationPossible()
2566             throws Exception {
2567 
2568         final Date now = new Date();
2569         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2570         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2571         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2572 
2573         final Header[] hdrs = new Header[] {
2574                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2575                 new BasicHeader("Cache-Control", "max-age=0"),
2576                 new BasicHeader("Content-Length", "128"),
2577                 new BasicHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo))
2578         };
2579 
2580         final byte[] bytes = new byte[128];
2581         new Random().nextBytes(bytes);
2582 
2583         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2584 
2585         impl = new CachingExec(mockBackend, mockCache, config);
2586         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2587 
2588         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2589         EasyMock.expect(
2590                 mockBackend.execute(
2591                         EasyMock.isA(HttpRoute.class),
2592                         EasyMock.isA(HttpRequestWrapper.class),
2593                         EasyMock.isA(HttpClientContext.class),
2594                         EasyMock.<HttpExecutionAware>isNull())).andThrow(
2595                 new IOException("can't talk to origin!")).anyTimes();
2596 
2597         replayMocks();
2598 
2599         final HttpResponse result = impl.execute(route, request, context, null);
2600 
2601         verifyMocks();
2602 
2603         final int status = result.getStatusLine().getStatusCode();
2604         if (status == 200) {
2605             boolean foundWarning = false;
2606             for (final Header h : result.getHeaders("Warning")) {
2607                 if (h.getValue().split(" ")[0].equals("111")) {
2608                     foundWarning = true;
2609                 }
2610             }
2611             Assert.assertTrue(foundWarning);
2612         } else {
2613             Assert.assertTrue(status >= 500 && status <= 599);
2614         }
2615     }
2616 
2617     /*
2618      * "Whenever a cache returns a response that is neither first-hand nor
2619      * "fresh enough" (in the sense of condition 2 in section 13.1.1), it MUST
2620      * attach a warning to that effect, using a Warning general-header."
2621      *
2622      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2623      */
2624     @Test
2625     public void testAttachesWarningHeaderWhenGeneratingStaleResponse() throws Exception {
2626         // covered by previous test
2627     }
2628 
2629     /*
2630      * "1xx Warnings that describe the freshness or revalidation status of the
2631      * response, and so MUST be deleted after a successful revalidation."
2632      *
2633      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2634      */
2635     @Test
2636     public void test1xxWarningsAreDeletedAfterSuccessfulRevalidation() throws Exception {
2637 
2638         final Date now = new Date();
2639         final Date tenSecondsAgo = new Date(now.getTime() - 25 * 1000L);
2640         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2641         final HttpResponse resp1 = HttpTestUtils.make200Response();
2642         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2643         resp1.setHeader("ETag", "\"etag\"");
2644         resp1.setHeader("Cache-Control", "max-age=5");
2645         resp1.setHeader("Warning", "110 squid \"stale stuff\"");
2646         resp1.setHeader("Via", "1.1 fred");
2647 
2648         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2649 
2650         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2651                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2652         validate.setHeader("If-None-Match", "\"etag\"");
2653 
2654         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2655                 "Not Modified");
2656         resp2.setHeader("Date", DateUtils.formatDate(now));
2657         resp2.setHeader("Server", "MockServer/1.0");
2658         resp2.setHeader("ETag", "\"etag\"");
2659         resp2.setHeader("Via", "1.1 fred");
2660 
2661         backendExpectsAnyRequestAndReturn(resp1);
2662         EasyMock.expect(
2663                 mockBackend.execute(
2664                         EasyMock.eq(route),
2665                         eqRequest(validate),
2666                         EasyMock.isA(HttpClientContext.class),
2667                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2668                                 Proxies.enhanceResponse(resp2));
2669 
2670         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2671 
2672         replayMocks();
2673 
2674         final HttpResponse stale = impl.execute(route, req1, context, null);
2675         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2676 
2677         final HttpResponse result1 = impl.execute(route, req2, context, null);
2678         final HttpResponse result2 = impl.execute(route, req3, context, null);
2679 
2680         verifyMocks();
2681 
2682         boolean found1xxWarning = false;
2683         for (final Header h : result1.getHeaders("Warning")) {
2684             for (final HeaderElement elt : h.getElements()) {
2685                 if (elt.getName().startsWith("1")) {
2686                     found1xxWarning = true;
2687                 }
2688             }
2689         }
2690         for (final Header h : result2.getHeaders("Warning")) {
2691             for (final HeaderElement elt : h.getElements()) {
2692                 if (elt.getName().startsWith("1")) {
2693                     found1xxWarning = true;
2694                 }
2695             }
2696         }
2697         Assert.assertFalse(found1xxWarning);
2698     }
2699 
2700     /*
2701      * "2xx Warnings that describe some aspect of the entity body or entity
2702      * headers that is not rectified by a revalidation (for example, a lossy
2703      * compression of the entity bodies) and which MUST NOT be deleted after a
2704      * successful revalidation."
2705      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.2
2706      */
2707     @Test
2708     public void test2xxWarningsAreNotDeletedAfterSuccessfulRevalidation() throws Exception {
2709         final Date now = new Date();
2710         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2711         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2712         final HttpResponse resp1 = HttpTestUtils.make200Response();
2713         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
2714         resp1.setHeader("ETag", "\"etag\"");
2715         resp1.setHeader("Cache-Control", "max-age=5");
2716         resp1.setHeader("Via", "1.1 xproxy");
2717         resp1.setHeader("Warning", "214 xproxy \"transformed stuff\"");
2718 
2719         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2720 
2721         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
2722                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2723         validate.setHeader("If-None-Match", "\"etag\"");
2724 
2725         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
2726                 "Not Modified");
2727         resp2.setHeader("Date", DateUtils.formatDate(now));
2728         resp2.setHeader("Server", "MockServer/1.0");
2729         resp2.setHeader("ETag", "\"etag\"");
2730         resp1.setHeader("Via", "1.1 xproxy");
2731 
2732         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2733 
2734         backendExpectsAnyRequestAndReturn(resp1);
2735 
2736         EasyMock.expect(
2737                 mockBackend.execute(
2738                         EasyMock.eq(route),
2739                         eqRequest(validate),
2740                         EasyMock.isA(HttpClientContext.class),
2741                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2742                                 Proxies.enhanceResponse(resp2));
2743 
2744         replayMocks();
2745 
2746         final HttpResponse stale = impl.execute(route, req1, context, null);
2747         Assert.assertNotNull(stale.getFirstHeader("Warning"));
2748 
2749         final HttpResponse result1 = impl.execute(route, req2, context, null);
2750         final HttpResponse result2 = impl.execute(route, req3, context, null);
2751 
2752         verifyMocks();
2753 
2754         boolean found214Warning = false;
2755         for (final Header h : result1.getHeaders("Warning")) {
2756             for (final HeaderElement elt : h.getElements()) {
2757                 final String[] parts = elt.getName().split(" ");
2758                 if ("214".equals(parts[0])) {
2759                     found214Warning = true;
2760                 }
2761             }
2762         }
2763         Assert.assertTrue(found214Warning);
2764 
2765         found214Warning = false;
2766         for (final Header h : result2.getHeaders("Warning")) {
2767             for (final HeaderElement elt : h.getElements()) {
2768                 final String[] parts = elt.getName().split(" ");
2769                 if ("214".equals(parts[0])) {
2770                     found214Warning = true;
2771                 }
2772             }
2773         }
2774         Assert.assertTrue(found214Warning);
2775     }
2776 
2777     /*
2778      * "When a response is generated from a cache entry, the cache MUST include
2779      * a single Age header field in the response with a value equal to the cache
2780      * entry's current_age."
2781      *
2782      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
2783      */
2784     @Test
2785     public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
2786 
2787         final Date now = new Date();
2788         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
2789         final Date nineSecondsAgo = new Date(now.getTime() - 9 * 1000L);
2790         final Date eightSecondsAgo = new Date(now.getTime() - 8 * 1000L);
2791 
2792         final Header[] hdrs = new Header[] {
2793                 new BasicHeader("Date", DateUtils.formatDate(nineSecondsAgo)),
2794                 new BasicHeader("Cache-Control", "max-age=3600"),
2795                 new BasicHeader("Content-Length", "128")
2796         };
2797 
2798         final byte[] bytes = new byte[128];
2799         new Random().nextBytes(bytes);
2800 
2801         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
2802 
2803         impl = new CachingExec(mockBackend, mockCache, config);
2804         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2805 
2806         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2807 
2808         replayMocks();
2809         final HttpResponse result = impl.execute(route, request, context, null);
2810         verifyMocks();
2811 
2812         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2813         Assert.assertEquals("11", result.getFirstHeader("Age").getValue());
2814     }
2815 
2816     /*
2817      * "If none of Expires, Cache-Control: max-age, or Cache-Control: s-maxage
2818      * (see section 14.9.3) appears in the response, and the response does not
2819      * include other restrictions on caching, the cache MAY compute a freshness
2820      * lifetime using a heuristic. The cache MUST attach Warning 113 to any
2821      * response whose age is more than 24 hours if such warning has not already
2822      * been added."
2823      *
2824      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4
2825      *
2826      * "113 Heuristic expiration MUST be included if the cache heuristically
2827      * chose a freshness lifetime greater than 24 hours and the response's age
2828      * is greater than 24 hours."
2829      *
2830      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
2831      */
2832     @Test
2833     public void testHeuristicCacheOlderThan24HoursHasWarningAttached() throws Exception {
2834 
2835         final Date now = new Date();
2836         final Date thirtySixHoursAgo = new Date(now.getTime() - 36 * 3600 * 1000L);
2837         final Date oneYearAgo = new Date(now.getTime() - 365 * 24 * 3600 * 1000L);
2838         final Date requestTime = new Date(thirtySixHoursAgo.getTime() - 1000L);
2839         final Date responseTime = new Date(thirtySixHoursAgo.getTime() + 1000L);
2840 
2841         final Header[] hdrs = new Header[] {
2842                 new BasicHeader("Date", DateUtils.formatDate(thirtySixHoursAgo)),
2843                 new BasicHeader("Cache-Control", "public"),
2844                 new BasicHeader("Last-Modified", DateUtils.formatDate(oneYearAgo)),
2845                 new BasicHeader("Content-Length", "128")
2846         };
2847 
2848         final byte[] bytes = new byte[128];
2849         new Random().nextBytes(bytes);
2850 
2851         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(requestTime, responseTime, hdrs, bytes);
2852 
2853         impl = new CachingExec(mockBackend, mockCache, config);
2854 
2855         request = HttpRequestWrapper.wrap(
2856                 new BasicHttpRequest("GET", "/thing", HttpVersion.HTTP_1_1));
2857 
2858         final CloseableHttpResponse validated = Proxies.enhanceResponse(HttpTestUtils.make200Response());
2859         validated.setHeader("Cache-Control", "public");
2860         validated.setHeader("Last-Modified", DateUtils.formatDate(oneYearAgo));
2861         validated.setHeader("Content-Length", "128");
2862         validated.setEntity(new ByteArrayEntity(bytes));
2863 
2864         final CloseableHttpResponse reconstructed = Proxies.enhanceResponse(HttpTestUtils.make200Response());
2865 
2866         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
2867 
2868         mockCache.flushInvalidatedCacheEntriesFor(
2869                 EasyMock.isA(HttpHost.class),
2870                 EasyMock.isA(HttpRequestWrapper.class),
2871                 EasyMock.isA(HttpResponse.class));
2872         EasyMock.expect(mockCache.getCacheEntry(EasyMock.eq(host), eqRequest(request))).andReturn(entry);
2873         EasyMock.expect(
2874                 mockBackend.execute(
2875                         EasyMock.isA(HttpRoute.class),
2876                         EasyMock.capture(cap),
2877                         EasyMock.isA(HttpClientContext.class),
2878                         EasyMock.<HttpExecutionAware>isNull())).andReturn(validated).times(0, 1);
2879         EasyMock.expect(mockCache.getCacheEntry(
2880                 EasyMock.isA(HttpHost.class),
2881                 EasyMock.isA(HttpRequestWrapper.class))).andReturn(entry).times(0, 1);
2882         EasyMock.expect(mockCache.cacheAndReturnResponse(
2883                 EasyMock.isA(HttpHost.class),
2884                 EasyMock.isA(HttpRequestWrapper.class),
2885                 eqCloseableResponse(validated),
2886                 EasyMock.isA(Date.class),
2887                 EasyMock.isA(Date.class))).andReturn(reconstructed).times(0, 1);
2888 
2889         replayMocks();
2890         final HttpResponse result = impl.execute(route, request, context, null);
2891         verifyMocks();
2892 
2893         Assert.assertEquals(200, result.getStatusLine().getStatusCode());
2894         if (!cap.hasCaptured()) {
2895             // heuristic cache hit
2896             boolean found113Warning = false;
2897             for (final Header h : result.getHeaders("Warning")) {
2898                 for (final HeaderElement elt : h.getElements()) {
2899                     final String[] parts = elt.getName().split(" ");
2900                     if ("113".equals(parts[0])) {
2901                         found113Warning = true;
2902                         break;
2903                     }
2904                 }
2905             }
2906             Assert.assertTrue(found113Warning);
2907         }
2908     }
2909 
2910     /*
2911      * "If a cache has two fresh responses for the same representation with
2912      * different validators, it MUST use the one with the more recent Date
2913      * header."
2914      *
2915      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.5
2916      */
2917     @Test
2918     public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
2919 
2920         final Date now = new Date();
2921         final Date inFiveSecond = new Date(now.getTime() + 5 * 1000L);
2922 
2923         // put an entry in the cache
2924         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
2925                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2926 
2927         final HttpResponse resp1 = HttpTestUtils.make200Response();
2928         resp1.setHeader("Date", DateUtils.formatDate(inFiveSecond));
2929         resp1.setHeader("ETag", "\"etag1\"");
2930         resp1.setHeader("Cache-Control", "max-age=3600");
2931         resp1.setHeader("Content-Length", "128");
2932 
2933         // force another origin hit
2934         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
2935                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2936         req2.setHeader("Cache-Control", "no-cache");
2937 
2938         final HttpResponse resp2 = HttpTestUtils.make200Response();
2939         resp2.setHeader("Date", DateUtils.formatDate(now)); // older
2940         resp2.setHeader("ETag", "\"etag2\"");
2941         resp2.setHeader("Cache-Control", "max-age=3600");
2942         resp2.setHeader("Content-Length", "128");
2943 
2944         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
2945                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
2946 
2947         EasyMock.expect(
2948                 mockBackend.execute(
2949                         EasyMock.isA(HttpRoute.class),
2950                         eqRequest(req1),
2951                         EasyMock.isA(HttpClientContext.class),
2952                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
2953                                 Proxies.enhanceResponse(resp1));
2954 
2955         backendExpectsAnyRequestAndReturn(resp2);
2956 
2957         replayMocks();
2958         impl.execute(route, req1, context, null);
2959         impl.execute(route, req2, context, null);
2960         final HttpResponse result = impl.execute(route, req3, context, null);
2961         verifyMocks();
2962         Assert.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
2963     }
2964 
2965     /*
2966      * "Clients MAY issue simple (non-subrange) GET requests with either weak
2967      * validators or strong validators. Clients MUST NOT use weak validators in
2968      * other forms of request."
2969      *
2970      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
2971      *
2972      * Note that we can't determine a priori whether a given HTTP-date is a weak
2973      * or strong validator, because that might depend on an upstream client
2974      * having a cache with a Last-Modified and Date entry that allows the date
2975      * to be a strong validator. We can tell when *we* are generating a request
2976      * for validation, but we can't tell if we receive a conditional request
2977      * from upstream.
2978      */
2979     private HttpResponse testRequestWithWeakETagValidatorIsNotAllowed(final String header)
2980             throws Exception {
2981         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
2982         EasyMock.expect(
2983                 mockBackend.execute(
2984                         EasyMock.eq(route),
2985                         EasyMock.capture(cap),
2986                         EasyMock.isA(HttpClientContext.class),
2987                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(0, 1);
2988 
2989         replayMocks();
2990         final HttpResponse response = impl.execute(route, request, context, null);
2991         verifyMocks();
2992 
2993         // it's probably ok to return a 400 (Bad Request) to this client
2994         if (cap.hasCaptured()) {
2995             final HttpRequest forwarded = cap.getValue();
2996             final Header h = forwarded.getFirstHeader(header);
2997             if (h != null) {
2998                 Assert.assertFalse(h.getValue().startsWith("W/"));
2999             }
3000         }
3001         return response;
3002 
3003     }
3004 
3005     @Test
3006     public void testSubrangeGETWithWeakETagIsNotAllowed() throws Exception {
3007         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3008         request.setHeader("Range", "bytes=0-500");
3009         request.setHeader("If-Range", "W/\"etag\"");
3010 
3011         final HttpResponse response = testRequestWithWeakETagValidatorIsNotAllowed("If-Range");
3012         Assert.assertTrue(response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST);
3013     }
3014 
3015     @Test
3016     public void testPUTWithIfMatchWeakETagIsNotAllowed() throws Exception {
3017         final HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HttpVersion.HTTP_1_1);
3018         put.setEntity(HttpTestUtils.makeBody(128));
3019         put.setHeader("Content-Length", "128");
3020         put.setHeader("If-Match", "W/\"etag\"");
3021         request = HttpRequestWrapper.wrap(put);
3022 
3023         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
3024     }
3025 
3026     @Test
3027     public void testPUTWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
3028         final HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HttpVersion.HTTP_1_1);
3029         put.setEntity(HttpTestUtils.makeBody(128));
3030         put.setHeader("Content-Length", "128");
3031         put.setHeader("If-None-Match", "W/\"etag\"");
3032         request = HttpRequestWrapper.wrap(put);
3033 
3034         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
3035     }
3036 
3037     @Test
3038     public void testDELETEWithIfMatchWeakETagIsNotAllowed() throws Exception {
3039         request = HttpRequestWrapper.wrap(
3040                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
3041         request.setHeader("If-Match", "W/\"etag\"");
3042 
3043         testRequestWithWeakETagValidatorIsNotAllowed("If-Match");
3044     }
3045 
3046     @Test
3047     public void testDELETEWithIfNoneMatchWeakETagIsNotAllowed() throws Exception {
3048         request = HttpRequestWrapper.wrap(
3049                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
3050         request.setHeader("If-None-Match", "W/\"etag\"");
3051 
3052         testRequestWithWeakETagValidatorIsNotAllowed("If-None-Match");
3053     }
3054 
3055     /*
3056      * "A cache or origin server receiving a conditional request, other than a
3057      * full-body GET request, MUST use the strong comparison function to
3058      * evaluate the condition."
3059      *
3060      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
3061      */
3062     @Test
3063     public void testSubrangeGETMustUseStrongComparisonForCachedResponse() throws Exception {
3064         final Date now = new Date();
3065         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3066                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3067         final HttpResponse resp1 = HttpTestUtils.make200Response();
3068         resp1.setHeader("Date", DateUtils.formatDate(now));
3069         resp1.setHeader("Cache-Control", "max-age=3600");
3070         resp1.setHeader("ETag", "\"etag\"");
3071 
3072         // according to weak comparison, this would match. Strong
3073         // comparison doesn't, because the cache entry's ETag is not
3074         // marked weak. Therefore, the If-Range must fail and we must
3075         // either get an error back or the full entity, but we better
3076         // not get the conditionally-requested Partial Content (206).
3077         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3078                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3079         req2.setHeader("Range", "bytes=0-50");
3080         req2.setHeader("If-Range", "W/\"etag\"");
3081 
3082         EasyMock.expect(
3083                 mockBackend.execute(
3084                         EasyMock.isA(HttpRoute.class),
3085                         EasyMock.isA(HttpRequestWrapper.class),
3086                         EasyMock.isA(HttpClientContext.class),
3087                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3088                                 Proxies.enhanceResponse(resp1)).times(1, 2);
3089 
3090         replayMocks();
3091         impl.execute(route, req1, context, null);
3092         final HttpResponse result = impl.execute(route, req2, context, null);
3093         verifyMocks();
3094 
3095         Assert.assertFalse(HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode());
3096     }
3097 
3098     /*
3099      * "HTTP/1.1 clients: - If an entity tag has been provided by the origin
3100      * server, MUST use that entity tag in any cache-conditional request (using
3101      * If- Match or If-None-Match)."
3102      *
3103      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
3104      */
3105     @Test
3106     public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
3107 
3108         final Date now = new Date();
3109         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3110 
3111         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3112                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3113         final HttpResponse resp1 = HttpTestUtils.make200Response();
3114         resp1.setHeader("Date", DateUtils.formatDate(now));
3115         resp1.setHeader("Cache-Control", "max-age=3600");
3116         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3117         resp1.setHeader("ETag", "W/\"etag\"");
3118 
3119         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3120                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3121         req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
3122 
3123         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3124         EasyMock.expect(
3125                 mockBackend.execute(
3126                         EasyMock.isA(HttpRoute.class),
3127                         EasyMock.isA(HttpRequestWrapper.class),
3128                         EasyMock.isA(HttpClientContext.class),
3129                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3130                                 Proxies.enhanceResponse(resp1));
3131 
3132         EasyMock.expect(
3133                 mockBackend.execute(
3134                         EasyMock.eq(route),
3135                         EasyMock.capture(cap),
3136                         EasyMock.isA(HttpClientContext.class),
3137                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3138                                 Proxies.enhanceResponse(resp1));
3139 
3140         replayMocks();
3141         impl.execute(route, req1, context, null);
3142         impl.execute(route, req2, context, null);
3143         verifyMocks();
3144 
3145         final HttpRequest validation = cap.getValue();
3146         boolean isConditional = false;
3147         final String[] conditionalHeaders = { "If-Range", "If-Modified-Since", "If-Unmodified-Since",
3148                 "If-Match", "If-None-Match" };
3149 
3150         for (final String ch : conditionalHeaders) {
3151             if (validation.getFirstHeader(ch) != null) {
3152                 isConditional = true;
3153                 break;
3154             }
3155         }
3156 
3157         if (isConditional) {
3158             boolean foundETag = false;
3159             for (final Header h : validation.getHeaders("If-Match")) {
3160                 for (final HeaderElement elt : h.getElements()) {
3161                     if ("W/\"etag\"".equals(elt.getName())) {
3162                         foundETag = true;
3163                     }
3164                 }
3165             }
3166             for (final Header h : validation.getHeaders("If-None-Match")) {
3167                 for (final HeaderElement elt : h.getElements()) {
3168                     if ("W/\"etag\"".equals(elt.getName())) {
3169                         foundETag = true;
3170                     }
3171                 }
3172             }
3173             Assert.assertTrue(foundETag);
3174         }
3175     }
3176 
3177     /*
3178      * "An HTTP/1.1 caching proxy, upon receiving a conditional request that
3179      * includes both a Last-Modified date and one or more entity tags as cache
3180      * validators, MUST NOT return a locally cached response to the client
3181      * unless that cached response is consistent with all of the conditional
3182      * header fields in the request."
3183      *
3184      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
3185      */
3186     @Test
3187     public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache()
3188             throws Exception {
3189         final Date now = new Date();
3190         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3191         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
3192 
3193         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3194                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3195         final HttpResponse resp1 = HttpTestUtils.make200Response();
3196         resp1.setHeader("Date", DateUtils.formatDate(now));
3197         resp1.setHeader("Cache-Control", "max-age=3600");
3198         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3199         resp1.setHeader("ETag", "W/\"etag\"");
3200 
3201         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3202                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3203         req2.setHeader("If-None-Match", "W/\"etag\"");
3204         req2.setHeader("If-Modified-Since", DateUtils.formatDate(twentySecondsAgo));
3205 
3206         // must hit the origin again for the second request
3207         EasyMock.expect(
3208                 mockBackend.execute(
3209                         EasyMock.isA(HttpRoute.class),
3210                         EasyMock.isA(HttpRequestWrapper.class),
3211                         EasyMock.isA(HttpClientContext.class),
3212                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3213                                 Proxies.enhanceResponse(resp1)).times(2);
3214 
3215         replayMocks();
3216         impl.execute(route, req1, context, null);
3217         final HttpResponse result = impl.execute(route, req2, context, null);
3218         verifyMocks();
3219 
3220         Assert.assertFalse(HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode());
3221     }
3222 
3223     @Test
3224     public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache()
3225             throws Exception {
3226         final Date now = new Date();
3227         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
3228 
3229         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3230                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3231         final HttpResponse resp1 = HttpTestUtils.make200Response();
3232         resp1.setHeader("Date", DateUtils.formatDate(now));
3233         resp1.setHeader("Cache-Control", "max-age=3600");
3234         resp1.setHeader("Last-Modified", DateUtils.formatDate(tenSecondsAgo));
3235         resp1.setHeader("ETag", "W/\"etag\"");
3236 
3237         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3238                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3239         req2.setHeader("If-None-Match", "W/\"etag\"");
3240         req2.setHeader("If-Modified-Since", DateUtils.formatDate(tenSecondsAgo));
3241 
3242         // may hit the origin again for the second request
3243         EasyMock.expect(
3244                 mockBackend.execute(
3245                         EasyMock.isA(HttpRoute.class),
3246                         EasyMock.isA(HttpRequestWrapper.class),
3247                         EasyMock.isA(HttpClientContext.class),
3248                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
3249                                 Proxies.enhanceResponse(resp1)).times(1,2);
3250 
3251         replayMocks();
3252         impl.execute(route, req1, context, null);
3253         impl.execute(route, req2, context, null);
3254         verifyMocks();
3255     }
3256 
3257 
3258     /*
3259      * "However, a cache that does not support the Range and Content-Range
3260      * headers MUST NOT cache 206 (Partial Content) responses."
3261      *
3262      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
3263      */
3264     @Test
3265     public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response()
3266             throws Exception {
3267 
3268         if (!supportsRangeAndContentRangeHeaders(impl)) {
3269             emptyMockCacheExpectsNoPuts();
3270 
3271             final HttpRequestWrapper req = HttpRequestWrapper.wrap(
3272                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3273             req.setHeader("Range", "bytes=0-50");
3274 
3275             final HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 206, "Partial Content");
3276             resp.setHeader("Content-Range", "bytes 0-50/128");
3277             resp.setHeader("ETag", "\"etag\"");
3278             resp.setHeader("Cache-Control", "max-age=3600");
3279 
3280             EasyMock.expect(mockBackend.execute(
3281                     EasyMock.isA(HttpRoute.class),
3282                     EasyMock.isA(HttpRequestWrapper.class),
3283                     EasyMock.isA(HttpClientContext.class),
3284                     EasyMock.<HttpExecutionAware>isNull())).andReturn(Proxies.enhanceResponse(resp));
3285 
3286             replayMocks();
3287             impl.execute(route, req, context, null);
3288             verifyMocks();
3289         }
3290     }
3291 
3292     /*
3293      * "A response received with any other status code (e.g. status codes 302
3294      * and 307) MUST NOT be returned in a reply to a subsequent request unless
3295      * there are cache-control directives or another header(s) that explicitly
3296      * allow it. For example, these include the following: an Expires header
3297      * (section 14.21); a 'max-age', 's-maxage', 'must-revalidate',
3298      * 'proxy-revalidate', 'public' or 'private' cache-control directive
3299      * (section 14.9)."
3300      *
3301      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
3302      */
3303     @Test
3304     public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
3305         originResponse = Proxies.enhanceResponse(
3306                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 302, "Temporary Redirect"));
3307         originResponse.setHeader("Location", "http://foo.example.com/other");
3308         originResponse.removeHeaders("Expires");
3309         originResponse.removeHeaders("Cache-Control");
3310 
3311         backendExpectsAnyRequest().andReturn(originResponse).times(2);
3312 
3313         replayMocks();
3314         impl.execute(route, request, context, null);
3315         impl.execute(route, request, context, null);
3316         verifyMocks();
3317     }
3318 
3319     /*
3320      * "A transparent proxy MUST NOT modify any of the following fields in a
3321      * request or response, and it MUST NOT add any of these fields if not
3322      * already present: - Content-Location - Content-MD5 - ETag - Last-Modified
3323      */
3324     private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
3325         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
3326         originResponse.setHeader(header, value);
3327 
3328         backendExpectsAnyRequest().andReturn(originResponse);
3329 
3330         replayMocks();
3331         final HttpResponse result = impl.execute(route, request, context, null);
3332         verifyMocks();
3333 
3334         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3335     }
3336 
3337     @Test
3338     public void testDoesNotModifyContentLocationHeaderFromOrigin() throws Exception {
3339 
3340         final String url = "http://foo.example.com/other";
3341         testDoesNotModifyHeaderFromOrigin("Content-Location", url);
3342     }
3343 
3344     @Test
3345     public void testDoesNotModifyContentMD5HeaderFromOrigin() throws Exception {
3346         testDoesNotModifyHeaderFromOrigin("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3347     }
3348 
3349     @Test
3350     public void testDoesNotModifyEtagHeaderFromOrigin() throws Exception {
3351         testDoesNotModifyHeaderFromOrigin("Etag", "\"the-etag\"");
3352     }
3353 
3354     @Test
3355     public void testDoesNotModifyLastModifiedHeaderFromOrigin() throws Exception {
3356         final String lm = DateUtils.formatDate(new Date());
3357         testDoesNotModifyHeaderFromOrigin("Last-Modified", lm);
3358     }
3359 
3360     private void testDoesNotAddHeaderToOriginResponse(final String header) throws Exception {
3361         originResponse.removeHeaders(header);
3362 
3363         backendExpectsAnyRequest().andReturn(originResponse);
3364 
3365         replayMocks();
3366         final HttpResponse result = impl.execute(route, request, context, null);
3367         verifyMocks();
3368 
3369         Assert.assertNull(result.getFirstHeader(header));
3370     }
3371 
3372     @Test
3373     public void testDoesNotAddContentLocationToOriginResponse() throws Exception {
3374         testDoesNotAddHeaderToOriginResponse("Content-Location");
3375     }
3376 
3377     @Test
3378     public void testDoesNotAddContentMD5ToOriginResponse() throws Exception {
3379         testDoesNotAddHeaderToOriginResponse("Content-MD5");
3380     }
3381 
3382     @Test
3383     public void testDoesNotAddEtagToOriginResponse() throws Exception {
3384         testDoesNotAddHeaderToOriginResponse("ETag");
3385     }
3386 
3387     @Test
3388     public void testDoesNotAddLastModifiedToOriginResponse() throws Exception {
3389         testDoesNotAddHeaderToOriginResponse("Last-Modified");
3390     }
3391 
3392     private void testDoesNotModifyHeaderFromOriginOnCacheHit(final String header, final String value)
3393             throws Exception {
3394 
3395         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3396                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3397         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3398                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3399 
3400         originResponse = Proxies.enhanceResponse(HttpTestUtils.make200Response());
3401         originResponse.setHeader("Cache-Control", "max-age=3600");
3402         originResponse.setHeader(header, value);
3403 
3404         backendExpectsAnyRequest().andReturn(originResponse);
3405 
3406         replayMocks();
3407         impl.execute(route, req1, context, null);
3408         final HttpResponse result = impl.execute(route, req2, context, null);
3409         verifyMocks();
3410 
3411         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3412     }
3413 
3414     @Test
3415     public void testDoesNotModifyContentLocationFromOriginOnCacheHit() throws Exception {
3416         final String url = "http://foo.example.com/other";
3417         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-Location", url);
3418     }
3419 
3420     @Test
3421     public void testDoesNotModifyContentMD5FromOriginOnCacheHit() throws Exception {
3422         testDoesNotModifyHeaderFromOriginOnCacheHit("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3423     }
3424 
3425     @Test
3426     public void testDoesNotModifyEtagFromOriginOnCacheHit() throws Exception {
3427         testDoesNotModifyHeaderFromOriginOnCacheHit("Etag", "\"the-etag\"");
3428     }
3429 
3430     @Test
3431     public void testDoesNotModifyLastModifiedFromOriginOnCacheHit() throws Exception {
3432         final String lm = DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L));
3433         testDoesNotModifyHeaderFromOriginOnCacheHit("Last-Modified", lm);
3434     }
3435 
3436     private void testDoesNotAddHeaderOnCacheHit(final String header) throws Exception {
3437 
3438         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3439                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3440         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3441                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3442 
3443         originResponse.addHeader("Cache-Control", "max-age=3600");
3444         originResponse.removeHeaders(header);
3445 
3446         backendExpectsAnyRequest().andReturn(originResponse);
3447 
3448         replayMocks();
3449         impl.execute(route, req1, context, null);
3450         final HttpResponse result = impl.execute(route, req2, context, null);
3451         verifyMocks();
3452 
3453         Assert.assertNull(result.getFirstHeader(header));
3454     }
3455 
3456     @Test
3457     public void testDoesNotAddContentLocationHeaderOnCacheHit() throws Exception {
3458         testDoesNotAddHeaderOnCacheHit("Content-Location");
3459     }
3460 
3461     @Test
3462     public void testDoesNotAddContentMD5HeaderOnCacheHit() throws Exception {
3463         testDoesNotAddHeaderOnCacheHit("Content-MD5");
3464     }
3465 
3466     @Test
3467     public void testDoesNotAddETagHeaderOnCacheHit() throws Exception {
3468         testDoesNotAddHeaderOnCacheHit("ETag");
3469     }
3470 
3471     @Test
3472     public void testDoesNotAddLastModifiedHeaderOnCacheHit() throws Exception {
3473         testDoesNotAddHeaderOnCacheHit("Last-Modified");
3474     }
3475 
3476     private void testDoesNotModifyHeaderOnRequest(final String header, final String value) throws Exception {
3477         final BasicHttpEntityEnclosingRequest req =
3478             new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
3479         req.setEntity(HttpTestUtils.makeBody(128));
3480         req.setHeader("Content-Length","128");
3481         req.setHeader(header,value);
3482 
3483         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3484 
3485         EasyMock.expect(
3486                 mockBackend.execute(
3487                         EasyMock.eq(route),
3488                         EasyMock.capture(cap),
3489                         EasyMock.isA(HttpClientContext.class),
3490                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
3491 
3492         replayMocks();
3493         impl.execute(route, HttpRequestWrapper.wrap(req), context, null);
3494         verifyMocks();
3495 
3496         final HttpRequest captured = cap.getValue();
3497         Assert.assertEquals(value, captured.getFirstHeader(header).getValue());
3498     }
3499 
3500     @Test
3501     public void testDoesNotModifyContentLocationHeaderOnRequest() throws Exception {
3502         final String url = "http://foo.example.com/other";
3503         testDoesNotModifyHeaderOnRequest("Content-Location",url);
3504     }
3505 
3506     @Test
3507     public void testDoesNotModifyContentMD5HeaderOnRequest() throws Exception {
3508         testDoesNotModifyHeaderOnRequest("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3509     }
3510 
3511     @Test
3512     public void testDoesNotModifyETagHeaderOnRequest() throws Exception {
3513         testDoesNotModifyHeaderOnRequest("ETag","\"etag\"");
3514     }
3515 
3516     @Test
3517     public void testDoesNotModifyLastModifiedHeaderOnRequest() throws Exception {
3518         final long tenSecondsAgo = System.currentTimeMillis() - 10 * 1000L;
3519         final String lm = DateUtils.formatDate(new Date(tenSecondsAgo));
3520         testDoesNotModifyHeaderOnRequest("Last-Modified", lm);
3521     }
3522 
3523     private void testDoesNotAddHeaderToRequestIfNotPresent(final String header) throws Exception {
3524         final BasicHttpEntityEnclosingRequest req =
3525             new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
3526         req.setEntity(HttpTestUtils.makeBody(128));
3527         req.setHeader("Content-Length","128");
3528         req.removeHeaders(header);
3529 
3530         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
3531 
3532         EasyMock.expect(
3533                 mockBackend.execute(
3534                         EasyMock.eq(route),
3535                         EasyMock.capture(cap),
3536                         EasyMock.isA(HttpClientContext.class),
3537                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
3538 
3539         replayMocks();
3540         impl.execute(route, HttpRequestWrapper.wrap(req), context, null);
3541         verifyMocks();
3542 
3543         final HttpRequest captured = cap.getValue();
3544         Assert.assertNull(captured.getFirstHeader(header));
3545     }
3546 
3547     @Test
3548     public void testDoesNotAddContentLocationToRequestIfNotPresent() throws Exception {
3549         testDoesNotAddHeaderToRequestIfNotPresent("Content-Location");
3550     }
3551 
3552     @Test
3553     public void testDoesNotAddContentMD5ToRequestIfNotPresent() throws Exception {
3554         testDoesNotAddHeaderToRequestIfNotPresent("Content-MD5");
3555     }
3556 
3557     @Test
3558     public void testDoesNotAddETagToRequestIfNotPresent() throws Exception {
3559         testDoesNotAddHeaderToRequestIfNotPresent("ETag");
3560     }
3561 
3562     @Test
3563     public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
3564         testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
3565     }
3566 
3567     /* " A transparent proxy MUST NOT modify any of the following
3568      * fields in a response: - Expires
3569      * but it MAY add any of these fields if not already present. If
3570      * an Expires header is added, it MUST be given a field-value
3571      * identical to that of the Date header in that response.
3572      */
3573     @Test
3574     public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
3575         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3576         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3577         testDoesNotModifyHeaderFromOrigin("Expires", expires);
3578     }
3579 
3580     @Test
3581     public void testDoesNotModifyExpiresHeaderFromOriginOnCacheHit() throws Exception {
3582         final long inTenSeconds = System.currentTimeMillis() + 10 * 1000L;
3583         final String expires = DateUtils.formatDate(new Date(inTenSeconds));
3584         testDoesNotModifyHeaderFromOriginOnCacheHit("Expires", expires);
3585     }
3586 
3587     @Test
3588     public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Exception {
3589         originResponse.removeHeaders("Expires");
3590 
3591         backendExpectsAnyRequest().andReturn(originResponse);
3592 
3593         replayMocks();
3594         final HttpResponse result = impl.execute(route, request, context, null);
3595         verifyMocks();
3596 
3597         final Header expHdr = result.getFirstHeader("Expires");
3598         if (expHdr != null) {
3599             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3600                                 expHdr.getValue());
3601         }
3602     }
3603 
3604     @Test
3605     public void testExpiresHeaderMatchesDateIfAddedToCacheHit() throws Exception {
3606         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3607                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3608         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3609                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3610 
3611         originResponse.setHeader("Cache-Control","max-age=3600");
3612         originResponse.removeHeaders("Expires");
3613 
3614         backendExpectsAnyRequest().andReturn(originResponse);
3615 
3616         replayMocks();
3617         impl.execute(route, req1, context, null);
3618         final HttpResponse result = impl.execute(route, req2, context, null);
3619         verifyMocks();
3620 
3621         final Header expHdr = result.getFirstHeader("Expires");
3622         if (expHdr != null) {
3623             Assert.assertEquals(result.getFirstHeader("Date").getValue(),
3624                                 expHdr.getValue());
3625         }
3626     }
3627 
3628     /* "A proxy MUST NOT modify or add any of the following fields in
3629      * a message that contains the no-transform cache-control
3630      * directive, or in any request: - Content-Encoding - Content-Range
3631      * - Content-Type"
3632      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
3633      */
3634     private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
3635         originResponse.addHeader("Cache-Control","no-transform");
3636         originResponse.setHeader(header, value);
3637 
3638         backendExpectsAnyRequest().andReturn(originResponse);
3639 
3640         replayMocks();
3641         final HttpResponse result = impl.execute(route, request, context, null);
3642         verifyMocks();
3643 
3644         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3645     }
3646 
3647     @Test
3648     public void testDoesNotModifyContentEncodingHeaderFromOriginResponseWithNoTransform() throws Exception {
3649         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Encoding","gzip");
3650     }
3651 
3652     @Test
3653     public void testDoesNotModifyContentRangeHeaderFromOriginResponseWithNoTransform() throws Exception {
3654         request.setHeader("If-Range","\"etag\"");
3655         request.setHeader("Range","bytes=0-49");
3656 
3657         originResponse = Proxies.enhanceResponse(
3658                 new BasicHttpResponse(HttpVersion.HTTP_1_1, 206, "Partial Content"));
3659         originResponse.setEntity(HttpTestUtils.makeBody(50));
3660         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128");
3661     }
3662 
3663     @Test
3664     public void testDoesNotModifyContentTypeHeaderFromOriginResponseWithNoTransform() throws Exception {
3665         testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3666     }
3667 
3668     private void testDoesNotModifyHeaderOnCachedResponseWithNoTransform(final String header, final String value) throws Exception {
3669         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3670                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3671         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3672                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3673 
3674         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3675         originResponse.setHeader(header, value);
3676 
3677         backendExpectsAnyRequest().andReturn(originResponse);
3678 
3679         replayMocks();
3680         impl.execute(route, req1, context, null);
3681         final HttpResponse result = impl.execute(route, req2, context, null);
3682         verifyMocks();
3683 
3684         Assert.assertEquals(value, result.getFirstHeader(header).getValue());
3685     }
3686 
3687     @Test
3688     public void testDoesNotModifyContentEncodingHeaderOnCachedResponseWithNoTransform() throws Exception {
3689         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Encoding","gzip");
3690     }
3691 
3692     @Test
3693     public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform() throws Exception {
3694         testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
3695     }
3696 
3697     @Test
3698     public void testDoesNotModifyContentRangeHeaderOnCachedResponseWithNoTransform() throws Exception {
3699         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3700                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3701         req1.setHeader("If-Range","\"etag\"");
3702         req1.setHeader("Range","bytes=0-49");
3703         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3704                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3705         req2.setHeader("If-Range","\"etag\"");
3706         req2.setHeader("Range","bytes=0-49");
3707 
3708         originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
3709         originResponse.setHeader("Content-Range", "bytes 0-49/128");
3710 
3711         backendExpectsAnyRequest().andReturn(originResponse).times(1,2);
3712 
3713         replayMocks();
3714         impl.execute(route, req1, context, null);
3715         final HttpResponse result = impl.execute(route, req2, context, null);
3716         verifyMocks();
3717 
3718         Assert.assertEquals("bytes 0-49/128",
3719                             result.getFirstHeader("Content-Range").getValue());
3720     }
3721 
3722     @Test
3723     public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3724         originResponse.addHeader("Cache-Control","no-transform");
3725         testDoesNotAddHeaderToOriginResponse("Content-Encoding");
3726     }
3727 
3728     @Test
3729     public void testDoesNotAddContentRangeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3730         originResponse.addHeader("Cache-Control","no-transform");
3731         testDoesNotAddHeaderToOriginResponse("Content-Range");
3732     }
3733 
3734     @Test
3735     public void testDoesNotAddContentTypeHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
3736         originResponse.addHeader("Cache-Control","no-transform");
3737         testDoesNotAddHeaderToOriginResponse("Content-Type");
3738     }
3739 
3740     /* no add on cache hit with no-transform */
3741     @Test
3742     public void testDoesNotAddContentEncodingHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3743         originResponse.addHeader("Cache-Control","no-transform");
3744         testDoesNotAddHeaderOnCacheHit("Content-Encoding");
3745     }
3746 
3747     @Test
3748     public void testDoesNotAddContentRangeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3749         originResponse.addHeader("Cache-Control","no-transform");
3750         testDoesNotAddHeaderOnCacheHit("Content-Range");
3751     }
3752 
3753     @Test
3754     public void testDoesNotAddContentTypeHeaderToCachedResponseWithNoTransformIfNotPresent() throws Exception {
3755         originResponse.addHeader("Cache-Control","no-transform");
3756         testDoesNotAddHeaderOnCacheHit("Content-Type");
3757     }
3758 
3759     /* no modify on request */
3760     @Test
3761     public void testDoesNotAddContentEncodingToRequestIfNotPresent() throws Exception {
3762         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3763     }
3764 
3765     @Test
3766     public void testDoesNotAddContentRangeToRequestIfNotPresent() throws Exception {
3767         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3768     }
3769 
3770     @Test
3771     public void testDoesNotAddContentTypeToRequestIfNotPresent() throws Exception {
3772         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3773     }
3774 
3775     @Test
3776     public void testDoesNotAddContentEncodingHeaderToRequestIfNotPresent() throws Exception {
3777         testDoesNotAddHeaderToRequestIfNotPresent("Content-Encoding");
3778     }
3779 
3780     @Test
3781     public void testDoesNotAddContentRangeHeaderToRequestIfNotPresent() throws Exception {
3782         testDoesNotAddHeaderToRequestIfNotPresent("Content-Range");
3783     }
3784 
3785     @Test
3786     public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Exception {
3787         testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
3788     }
3789 
3790     /* "When a cache makes a validating request to a server, and the
3791      * server provides a 304 (Not Modified) response or a 206 (Partial
3792      * Content) response, the cache then constructs a response to send
3793      * to the requesting client.
3794      *
3795      * If the status code is 304 (Not Modified), the cache uses the
3796      * entity-body stored in the cache entry as the entity-body of
3797      * this outgoing response.
3798      *
3799      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.3
3800      */
3801     public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
3802         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3803                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3804         final HttpResponse resp1 = HttpTestUtils.make200Response();
3805         resp1.setHeader("Cache-Control","max-age=3600");
3806         resp1.setHeader("ETag","\"etag\"");
3807 
3808         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3809                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3810         req2.setHeader("Cache-Control","max-age=0, max-stale=0");
3811         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3812 
3813         backendExpectsAnyRequestAndReturn(resp1);
3814         backendExpectsAnyRequestAndReturn(resp2);
3815 
3816         replayMocks();
3817         impl.execute(route, req1, context, null);
3818         final HttpResponse result = impl.execute(route, req2, context, null);
3819         verifyMocks();
3820 
3821         final InputStream i1 = resp1.getEntity().getContent();
3822         final InputStream i2 = result.getEntity().getContent();
3823         int b1, b2;
3824         while((b1 = i1.read()) != -1) {
3825             b2 = i2.read();
3826             Assert.assertEquals(b1, b2);
3827         }
3828         b2 = i2.read();
3829         Assert.assertEquals(-1, b2);
3830         i1.close();
3831         i2.close();
3832     }
3833 
3834     /* "The end-to-end headers stored in the cache entry are used for
3835      * the constructed response, except that ...
3836      *
3837      * - any end-to-end headers provided in the 304 or 206 response MUST
3838      *  replace the corresponding headers from the cache entry.
3839      *
3840      * Unless the cache decides to remove the cache entry, it MUST
3841      * also replace the end-to-end headers stored with the cache entry
3842      * with corresponding headers received in the incoming response,
3843      * except for Warning headers as described immediately above."
3844      */
3845     private void decorateWithEndToEndHeaders(final HttpResponse r) {
3846         r.setHeader("Allow","GET");
3847         r.setHeader("Content-Encoding","gzip");
3848         r.setHeader("Content-Language","en");
3849         r.setHeader("Content-Length", "128");
3850         r.setHeader("Content-Location","http://foo.example.com/other");
3851         r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
3852         r.setHeader("Content-Type", "text/html;charset=utf-8");
3853         r.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 10 * 1000L)));
3854         r.setHeader("Last-Modified", DateUtils.formatDate(new Date(System.currentTimeMillis() - 10 * 1000L)));
3855         r.setHeader("Location", "http://foo.example.com/other2");
3856         r.setHeader("Pragma", "x-pragma");
3857         r.setHeader("Retry-After","180");
3858     }
3859 
3860     @Test
3861     public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
3862         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3863                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3864         final HttpResponse resp1 = HttpTestUtils.make200Response();
3865         resp1.setHeader("Cache-Control","max-age=3600");
3866         resp1.setHeader("ETag","\"etag\"");
3867         decorateWithEndToEndHeaders(resp1);
3868 
3869         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3870                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3871         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3872         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3873         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3874         resp2.setHeader("Server", "MockServer/1.0");
3875 
3876         backendExpectsAnyRequestAndReturn(resp1);
3877         backendExpectsAnyRequestAndReturn(resp2);
3878 
3879         replayMocks();
3880         impl.execute(route, req1, context, null);
3881         final HttpResponse result = impl.execute(route, req2, context, null);
3882         verifyMocks();
3883 
3884         final String[] endToEndHeaders = {
3885             "Cache-Control", "ETag", "Allow", "Content-Encoding",
3886             "Content-Language", "Content-Length", "Content-Location",
3887             "Content-MD5", "Content-Type", "Expires", "Last-Modified",
3888             "Location", "Pragma", "Retry-After"
3889         };
3890         for(final String h : endToEndHeaders) {
3891             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
3892                                 HttpTestUtils.getCanonicalHeaderValue(result, h));
3893         }
3894     }
3895 
3896     @Test
3897     public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
3898 
3899         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3900                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3901         final HttpResponse resp1 = HttpTestUtils.make200Response();
3902         resp1.setHeader("Cache-Control","max-age=3600");
3903         resp1.setHeader("ETag","\"etag\"");
3904         decorateWithEndToEndHeaders(resp1);
3905 
3906         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3907                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3908         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3909         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3910         resp2.setHeader("Cache-Control", "max-age=1800");
3911         resp2.setHeader("Date", DateUtils.formatDate(new Date()));
3912         resp2.setHeader("Server", "MockServer/1.0");
3913         resp2.setHeader("Allow", "GET,HEAD");
3914         resp2.setHeader("Content-Language", "en,en-us");
3915         resp2.setHeader("Content-Location", "http://foo.example.com/new");
3916         resp2.setHeader("Content-Type","text/html");
3917         resp2.setHeader("Expires", DateUtils.formatDate(new Date(System.currentTimeMillis() + 5 * 1000L)));
3918         resp2.setHeader("Location", "http://foo.example.com/new2");
3919         resp2.setHeader("Pragma","x-new-pragma");
3920         resp2.setHeader("Retry-After","120");
3921 
3922         backendExpectsAnyRequestAndReturn(resp1);
3923         backendExpectsAnyRequestAndReturn(resp2);
3924 
3925         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
3926                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3927 
3928         replayMocks();
3929         impl.execute(route, req1, context, null);
3930         final HttpResponse result1 = impl.execute(route, req2, context, null);
3931         final HttpResponse result2 = impl.execute(route, req3, context, null);
3932         verifyMocks();
3933 
3934         final String[] endToEndHeaders = {
3935             "Date", "Cache-Control", "Allow", "Content-Language",
3936             "Content-Location", "Content-Type", "Expires", "Location",
3937             "Pragma", "Retry-After"
3938         };
3939         for(final String h : endToEndHeaders) {
3940             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3941                                 HttpTestUtils.getCanonicalHeaderValue(result1, h));
3942             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3943                                 HttpTestUtils.getCanonicalHeaderValue(result2, h));
3944         }
3945     }
3946 
3947     /* "If a header field-name in the incoming response matches more
3948      * than one header in the cache entry, all such old headers MUST
3949      * be replaced."
3950      */
3951     @Test
3952     public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
3953         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
3954                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3955         final HttpResponse resp1 = HttpTestUtils.make200Response();
3956         resp1.addHeader("Cache-Control","max-age=3600");
3957         resp1.addHeader("Cache-Control","public");
3958         resp1.setHeader("ETag","\"etag\"");
3959 
3960         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
3961                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3962         req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
3963         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
3964         resp2.setHeader("Cache-Control", "max-age=1800");
3965 
3966         backendExpectsAnyRequestAndReturn(resp1);
3967         backendExpectsAnyRequestAndReturn(resp2);
3968 
3969         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
3970                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
3971 
3972         replayMocks();
3973         impl.execute(route, req1, context, null);
3974         final HttpResponse result1 = impl.execute(route, req2, context, null);
3975         final HttpResponse result2 = impl.execute(route, req3, context, null);
3976         verifyMocks();
3977 
3978         final String h = "Cache-Control";
3979         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3980                             HttpTestUtils.getCanonicalHeaderValue(result1, h));
3981         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
3982                             HttpTestUtils.getCanonicalHeaderValue(result2, h));
3983     }
3984 
3985     /* "If a cache has a stored non-empty set of subranges for an
3986      * entity, and an incoming response transfers another subrange,
3987      * the cache MAY combine the new subrange with the existing set if
3988      * both the following conditions are met:
3989      *
3990      * - Both the incoming response and the cache entry have a cache
3991      * validator.
3992      *
3993      * - The two cache validators match using the strong comparison
3994      * function (see section 13.3.3).
3995      *
3996      * If either requirement is not met, the cache MUST use only the
3997      * most recent partial response (based on the Date values
3998      * transmitted with every response, and using the incoming
3999      * response if these values are equal or missing), and MUST
4000      * discard the other partial information."
4001      *
4002      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.4
4003      */
4004     @Test
4005     public void testCannotCombinePartialResponseIfIncomingResponseDoesNotHaveACacheValidator()
4006         throws Exception {
4007 
4008         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4009                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4010         req1.setHeader("Range","bytes=0-49");
4011 
4012         final Date now = new Date();
4013         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4014         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4015 
4016         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4017         resp1.setEntity(HttpTestUtils.makeBody(50));
4018         resp1.setHeader("Server","MockServer/1.0");
4019         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4020         resp1.setHeader("Cache-Control","max-age=3600");
4021         resp1.setHeader("Content-Range","bytes 0-49/128");
4022         resp1.setHeader("ETag","\"etag1\"");
4023 
4024         backendExpectsAnyRequestAndReturn(resp1);
4025 
4026         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4027                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4028         req2.setHeader("Range","bytes=50-127");
4029 
4030         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4031         resp2.setEntity(HttpTestUtils.makeBody(78));
4032         resp2.setHeader("Cache-Control","max-age=3600");
4033         resp2.setHeader("Content-Range","bytes 50-127/128");
4034         resp2.setHeader("Server","MockServer/1.0");
4035         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4036 
4037         backendExpectsAnyRequestAndReturn(resp2);
4038 
4039         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4040                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4041 
4042         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4043         resp3.setEntity(HttpTestUtils.makeBody(128));
4044         resp3.setHeader("Server","MockServer/1.0");
4045         resp3.setHeader("Date", DateUtils.formatDate(now));
4046 
4047         backendExpectsAnyRequestAndReturn(resp3);
4048 
4049         replayMocks();
4050         impl.execute(route, req1, context, null);
4051         impl.execute(route, req2, context, null);
4052         impl.execute(route, req3, context, null);
4053         verifyMocks();
4054     }
4055 
4056     @Test
4057     public void testCannotCombinePartialResponseIfCacheEntryDoesNotHaveACacheValidator()
4058         throws Exception {
4059 
4060         final Date now = new Date();
4061         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4062         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4063 
4064         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4065                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4066         req1.setHeader("Range","bytes=0-49");
4067 
4068         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4069         resp1.setEntity(HttpTestUtils.makeBody(50));
4070         resp1.setHeader("Cache-Control","max-age=3600");
4071         resp1.setHeader("Content-Range","bytes 0-49/128");
4072         resp1.setHeader("Server","MockServer/1.0");
4073         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4074 
4075         backendExpectsAnyRequestAndReturn(resp1);
4076 
4077         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4078                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4079         req2.setHeader("Range","bytes=50-127");
4080 
4081         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4082         resp2.setEntity(HttpTestUtils.makeBody(78));
4083         resp2.setHeader("Cache-Control","max-age=3600");
4084         resp2.setHeader("Content-Range","bytes 50-127/128");
4085         resp2.setHeader("ETag","\"etag1\"");
4086         resp2.setHeader("Server","MockServer/1.0");
4087         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4088 
4089         backendExpectsAnyRequestAndReturn(resp2);
4090 
4091         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4092                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4093 
4094         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4095         resp3.setEntity(HttpTestUtils.makeBody(128));
4096         resp3.setHeader("Server","MockServer/1.0");
4097         resp3.setHeader("Date", DateUtils.formatDate(now));
4098 
4099         backendExpectsAnyRequestAndReturn(resp3);
4100 
4101         replayMocks();
4102         impl.execute(route, req1, context, null);
4103         impl.execute(route, req2, context, null);
4104         impl.execute(route, req3, context, null);
4105         verifyMocks();
4106     }
4107 
4108     @Test
4109     public void testCannotCombinePartialResponseIfCacheValidatorsDoNotStronglyMatch()
4110         throws Exception {
4111 
4112         final Date now = new Date();
4113         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4114         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4115 
4116         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4117                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4118         req1.setHeader("Range","bytes=0-49");
4119 
4120         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4121         resp1.setEntity(HttpTestUtils.makeBody(50));
4122         resp1.setHeader("Cache-Control","max-age=3600");
4123         resp1.setHeader("Content-Range","bytes 0-49/128");
4124         resp1.setHeader("ETag","\"etag1\"");
4125         resp1.setHeader("Server","MockServer/1.0");
4126         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4127 
4128         backendExpectsAnyRequestAndReturn(resp1);
4129 
4130         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4131                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4132         req2.setHeader("Range","bytes=50-127");
4133 
4134         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4135         resp2.setEntity(HttpTestUtils.makeBody(78));
4136         resp2.setHeader("Cache-Control","max-age=3600");
4137         resp2.setHeader("Content-Range","bytes 50-127/128");
4138         resp2.setHeader("ETag","\"etag2\"");
4139         resp2.setHeader("Server","MockServer/1.0");
4140         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4141 
4142         backendExpectsAnyRequestAndReturn(resp2);
4143 
4144         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4145                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4146 
4147         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4148         resp3.setEntity(HttpTestUtils.makeBody(128));
4149         resp3.setHeader("Server","MockServer/1.0");
4150         resp3.setHeader("Date", DateUtils.formatDate(now));
4151 
4152         backendExpectsAnyRequestAndReturn(resp3);
4153 
4154         replayMocks();
4155         impl.execute(route, req1, context, null);
4156         impl.execute(route, req2, context, null);
4157         impl.execute(route, req3, context, null);
4158         verifyMocks();
4159     }
4160 
4161     @Test
4162     public void testMustDiscardLeastRecentPartialResponseIfIncomingRequestDoesNotHaveCacheValidator()
4163         throws Exception {
4164 
4165         final Date now = new Date();
4166         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4167         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4168 
4169         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4170                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4171         req1.setHeader("Range","bytes=0-49");
4172 
4173         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4174         resp1.setEntity(HttpTestUtils.makeBody(50));
4175         resp1.setHeader("Cache-Control","max-age=3600");
4176         resp1.setHeader("Content-Range","bytes 0-49/128");
4177         resp1.setHeader("ETag","\"etag1\"");
4178         resp1.setHeader("Server","MockServer/1.0");
4179         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4180 
4181         backendExpectsAnyRequestAndReturn(resp1);
4182 
4183         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4184                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4185         req2.setHeader("Range","bytes=50-127");
4186 
4187         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4188         resp2.setEntity(HttpTestUtils.makeBody(78));
4189         resp2.setHeader("Cache-Control","max-age=3600");
4190         resp2.setHeader("Content-Range","bytes 50-127/128");
4191         resp2.setHeader("Server","MockServer/1.0");
4192         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4193 
4194         backendExpectsAnyRequestAndReturn(resp2);
4195 
4196         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4197                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4198         req3.setHeader("Range","bytes=0-49");
4199 
4200         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4201         resp3.setEntity(HttpTestUtils.makeBody(128));
4202         resp3.setHeader("Server","MockServer/1.0");
4203         resp3.setHeader("Date", DateUtils.formatDate(now));
4204 
4205         // must make this request; cannot serve from cache
4206         backendExpectsAnyRequestAndReturn(resp3);
4207 
4208         replayMocks();
4209         impl.execute(route, req1, context, null);
4210         impl.execute(route, req2, context, null);
4211         impl.execute(route, req3, context, null);
4212         verifyMocks();
4213     }
4214 
4215     @Test
4216     public void testMustDiscardLeastRecentPartialResponseIfCachedResponseDoesNotHaveCacheValidator()
4217         throws Exception {
4218 
4219         final Date now = new Date();
4220         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4221         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4222 
4223         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4224                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4225         req1.setHeader("Range","bytes=0-49");
4226 
4227         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4228         resp1.setEntity(HttpTestUtils.makeBody(50));
4229         resp1.setHeader("Cache-Control","max-age=3600");
4230         resp1.setHeader("Content-Range","bytes 0-49/128");
4231         resp1.setHeader("Server","MockServer/1.0");
4232         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4233 
4234         backendExpectsAnyRequestAndReturn(resp1);
4235 
4236         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4237                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4238         req2.setHeader("Range","bytes=50-127");
4239 
4240         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4241         resp2.setEntity(HttpTestUtils.makeBody(78));
4242         resp2.setHeader("Cache-Control","max-age=3600");
4243         resp2.setHeader("Content-Range","bytes 50-127/128");
4244         resp2.setHeader("ETag","\"etag1\"");
4245         resp2.setHeader("Server","MockServer/1.0");
4246         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4247 
4248         backendExpectsAnyRequestAndReturn(resp2);
4249 
4250         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4251                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4252         req3.setHeader("Range","bytes=0-49");
4253 
4254         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4255         resp3.setEntity(HttpTestUtils.makeBody(128));
4256         resp3.setHeader("Server","MockServer/1.0");
4257         resp3.setHeader("Date", DateUtils.formatDate(now));
4258 
4259         // must make this request; cannot serve from cache
4260         backendExpectsAnyRequestAndReturn(resp3);
4261 
4262         replayMocks();
4263         impl.execute(route, req1, context, null);
4264         impl.execute(route, req2, context, null);
4265         impl.execute(route, req3, context, null);
4266         verifyMocks();
4267     }
4268 
4269     @Test
4270     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatch()
4271         throws Exception {
4272 
4273         final Date now = new Date();
4274         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4275         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4276 
4277         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4278                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4279         req1.setHeader("Range","bytes=0-49");
4280 
4281         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4282         resp1.setEntity(HttpTestUtils.makeBody(50));
4283         resp1.setHeader("Cache-Control","max-age=3600");
4284         resp1.setHeader("Content-Range","bytes 0-49/128");
4285         resp1.setHeader("Etag","\"etag1\"");
4286         resp1.setHeader("Server","MockServer/1.0");
4287         resp1.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4288 
4289         backendExpectsAnyRequestAndReturn(resp1);
4290 
4291         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4292                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4293         req2.setHeader("Range","bytes=50-127");
4294 
4295         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4296         resp2.setEntity(HttpTestUtils.makeBody(78));
4297         resp2.setHeader("Cache-Control","max-age=3600");
4298         resp2.setHeader("Content-Range","bytes 50-127/128");
4299         resp2.setHeader("ETag","\"etag2\"");
4300         resp2.setHeader("Server","MockServer/1.0");
4301         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4302 
4303         backendExpectsAnyRequestAndReturn(resp2);
4304 
4305         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4306                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4307         req3.setHeader("Range","bytes=0-49");
4308 
4309         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4310         resp3.setEntity(HttpTestUtils.makeBody(128));
4311         resp3.setHeader("Server","MockServer/1.0");
4312         resp3.setHeader("Date", DateUtils.formatDate(now));
4313 
4314         // must make this request; cannot serve from cache
4315         backendExpectsAnyRequestAndReturn(resp3);
4316 
4317         replayMocks();
4318         impl.execute(route, req1, context, null);
4319         impl.execute(route, req2, context, null);
4320         impl.execute(route, req3, context, null);
4321         verifyMocks();
4322     }
4323 
4324     @Test
4325     public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatchEvenIfResponsesOutOfOrder()
4326         throws Exception {
4327 
4328         final Date now = new Date();
4329         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4330         final Date twoSecondsAgo = new Date(now.getTime() - 2 * 1000L);
4331 
4332         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4333                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4334         req1.setHeader("Range","bytes=0-49");
4335 
4336         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4337         resp1.setEntity(HttpTestUtils.makeBody(50));
4338         resp1.setHeader("Cache-Control","max-age=3600");
4339         resp1.setHeader("Content-Range","bytes 0-49/128");
4340         resp1.setHeader("Etag","\"etag1\"");
4341         resp1.setHeader("Server","MockServer/1.0");
4342         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4343 
4344         backendExpectsAnyRequestAndReturn(resp1);
4345 
4346         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4347                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4348         req2.setHeader("Range","bytes=50-127");
4349 
4350         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4351         resp2.setEntity(HttpTestUtils.makeBody(78));
4352         resp2.setHeader("Cache-Control","max-age=3600");
4353         resp2.setHeader("Content-Range","bytes 50-127/128");
4354         resp2.setHeader("ETag","\"etag2\"");
4355         resp2.setHeader("Server","MockServer/1.0");
4356         resp2.setHeader("Date", DateUtils.formatDate(twoSecondsAgo));
4357 
4358         backendExpectsAnyRequestAndReturn(resp2);
4359 
4360         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4361                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4362         req3.setHeader("Range","bytes=50-127");
4363 
4364         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4365         resp3.setEntity(HttpTestUtils.makeBody(128));
4366         resp3.setHeader("Server","MockServer/1.0");
4367         resp3.setHeader("Date", DateUtils.formatDate(now));
4368 
4369         // must make this request; cannot serve from cache
4370         backendExpectsAnyRequestAndReturn(resp3);
4371 
4372         replayMocks();
4373         impl.execute(route, req1, context, null);
4374         impl.execute(route, req2, context, null);
4375         impl.execute(route, req3, context, null);
4376         verifyMocks();
4377     }
4378 
4379     @Test
4380     public void testMustDiscardCachedPartialResponseIfCacheValidatorsDoNotStronglyMatchAndDateHeadersAreEqual()
4381         throws Exception {
4382 
4383         final Date now = new Date();
4384         final Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
4385 
4386         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4387                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4388         req1.setHeader("Range","bytes=0-49");
4389 
4390         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4391         resp1.setEntity(HttpTestUtils.makeBody(50));
4392         resp1.setHeader("Cache-Control","max-age=3600");
4393         resp1.setHeader("Content-Range","bytes 0-49/128");
4394         resp1.setHeader("Etag","\"etag1\"");
4395         resp1.setHeader("Server","MockServer/1.0");
4396         resp1.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4397 
4398         backendExpectsAnyRequestAndReturn(resp1);
4399 
4400         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4401                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4402         req2.setHeader("Range","bytes=50-127");
4403 
4404         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
4405         resp2.setEntity(HttpTestUtils.makeBody(78));
4406         resp2.setHeader("Cache-Control","max-age=3600");
4407         resp2.setHeader("Content-Range","bytes 50-127/128");
4408         resp2.setHeader("ETag","\"etag2\"");
4409         resp2.setHeader("Server","MockServer/1.0");
4410         resp2.setHeader("Date", DateUtils.formatDate(oneSecondAgo));
4411 
4412         backendExpectsAnyRequestAndReturn(resp2);
4413 
4414         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4415                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4416         req3.setHeader("Range","bytes=0-49");
4417 
4418         final HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
4419         resp3.setEntity(HttpTestUtils.makeBody(128));
4420         resp3.setHeader("Server","MockServer/1.0");
4421         resp3.setHeader("Date", DateUtils.formatDate(now));
4422 
4423         // must make this request; cannot serve from cache
4424         backendExpectsAnyRequestAndReturn(resp3);
4425 
4426         replayMocks();
4427         impl.execute(route, req1, context, null);
4428         impl.execute(route, req2, context, null);
4429         impl.execute(route, req3, context, null);
4430         verifyMocks();
4431     }
4432 
4433     /* "When the cache receives a subsequent request whose Request-URI
4434      * specifies one or more cache entries including a Vary header
4435      * field, the cache MUST NOT use such a cache entry to construct a
4436      * response to the new request unless all of the selecting
4437      * request-headers present in the new request match the
4438      * corresponding stored request-headers in the original request."
4439      *
4440      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4441      */
4442     @Test
4443     public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch()
4444         throws Exception {
4445 
4446         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4447                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4448         req1.setHeader("Accept-Encoding","gzip");
4449 
4450         final HttpResponse resp1 = HttpTestUtils.make200Response();
4451         resp1.setHeader("ETag","\"etag1\"");
4452         resp1.setHeader("Cache-Control","max-age=3600");
4453         resp1.setHeader("Vary","Accept-Encoding");
4454 
4455         backendExpectsAnyRequestAndReturn(resp1);
4456 
4457         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4458                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4459         req2.removeHeaders("Accept-Encoding");
4460 
4461         final HttpResponse resp2 = HttpTestUtils.make200Response();
4462         resp2.setHeader("ETag","\"etag1\"");
4463         resp2.setHeader("Cache-Control","max-age=3600");
4464 
4465         // not allowed to have a cache hit; must forward request
4466         backendExpectsAnyRequestAndReturn(resp2);
4467 
4468         replayMocks();
4469         impl.execute(route, req1, context, null);
4470         impl.execute(route, req2, context, null);
4471         verifyMocks();
4472     }
4473 
4474     /* "A Vary header field-value of "*" always fails to match and
4475      * subsequent requests on that resource can only be properly
4476      * interpreted by the origin server."
4477      *
4478      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4479      */
4480     @Test
4481     public void testCannotServeFromCacheForVaryStar() throws Exception {
4482         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4483                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4484 
4485         final HttpResponse resp1 = HttpTestUtils.make200Response();
4486         resp1.setHeader("ETag","\"etag1\"");
4487         resp1.setHeader("Cache-Control","max-age=3600");
4488         resp1.setHeader("Vary","*");
4489 
4490         backendExpectsAnyRequestAndReturn(resp1);
4491 
4492         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4493                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4494 
4495         final HttpResponse resp2 = HttpTestUtils.make200Response();
4496         resp2.setHeader("ETag","\"etag1\"");
4497         resp2.setHeader("Cache-Control","max-age=3600");
4498 
4499         // not allowed to have a cache hit; must forward request
4500         backendExpectsAnyRequestAndReturn(resp2);
4501 
4502         replayMocks();
4503         impl.execute(route, req1, context, null);
4504         impl.execute(route, req2, context, null);
4505         verifyMocks();
4506     }
4507 
4508     /* " If the selecting request header fields for the cached entry
4509      * do not match the selecting request header fields of the new
4510      * request, then the cache MUST NOT use a cached entry to satisfy
4511      * the request unless it first relays the new request to the
4512      * origin server in a conditional request and the server responds
4513      * with 304 (Not Modified), including an entity tag or
4514      * Content-Location that indicates the entity to be used.
4515      *
4516      * If an entity tag was assigned to a cached representation, the
4517      * forwarded request SHOULD be conditional and include the entity
4518      * tags in an If-None-Match header field from all its cache
4519      * entries for the resource. This conveys to the server the set of
4520      * entities currently held by the cache, so that if any one of
4521      * these entities matches the requested entity, the server can use
4522      * the ETag header field in its 304 (Not Modified) response to
4523      * tell the cache which entry is appropriate. If the entity-tag of
4524      * the new response matches that of an existing entry, the new
4525      * response SHOULD be used to update the header fields of the
4526      * existing entry, and the result MUST be returned to the client.
4527      *
4528      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
4529      */
4530     @Test
4531     public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated()
4532         throws Exception {
4533 
4534         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4535                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4536         req1.setHeader("User-Agent","MyBrowser/1.0");
4537 
4538         final HttpResponse resp1 = HttpTestUtils.make200Response();
4539         resp1.setHeader("ETag","\"etag1\"");
4540         resp1.setHeader("Cache-Control","max-age=3600");
4541         resp1.setHeader("Vary","User-Agent");
4542         resp1.setHeader("Content-Type","application/octet-stream");
4543 
4544         backendExpectsAnyRequestAndReturn(resp1);
4545 
4546         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
4547                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4548         req2.setHeader("User-Agent","MyBrowser/1.5");
4549 
4550         final HttpRequestWrapper conditional = HttpRequestWrapper.wrap(
4551                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4552         conditional.setHeader("User-Agent","MyBrowser/1.5");
4553         conditional.setHeader("If-None-Match","\"etag1\"");
4554 
4555         final HttpResponse resp200 = HttpTestUtils.make200Response();
4556         resp200.setHeader("ETag","\"etag1\"");
4557         resp200.setHeader("Vary","User-Agent");
4558 
4559         final HttpResponse resp304 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
4560         resp304.setHeader("ETag","\"etag1\"");
4561         resp304.setHeader("Vary","User-Agent");
4562 
4563         final Capture<HttpRequestWrapper> condCap = new Capture<HttpRequestWrapper>();
4564         final Capture<HttpRequestWrapper> uncondCap = new Capture<HttpRequestWrapper>();
4565 
4566         EasyMock.expect(
4567                 mockBackend.execute(
4568                         EasyMock.isA(HttpRoute.class),
4569                         EasyMock.and(eqRequest(conditional), EasyMock.capture(condCap)),
4570                         EasyMock.isA(HttpClientContext.class),
4571                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4572                                 Proxies.enhanceResponse(resp304)).times(0,1);
4573         EasyMock.expect(
4574                 mockBackend.execute(
4575                         EasyMock.isA(HttpRoute.class),
4576                         EasyMock.and(eqRequest(req2), EasyMock.capture(uncondCap)),
4577                         EasyMock.isA(HttpClientContext.class),
4578                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4579                                 Proxies.enhanceResponse(resp200)).times(0,1);
4580 
4581         replayMocks();
4582         impl.execute(route, req1, context, null);
4583         final HttpResponse result = impl.execute(route, req2, context, null);
4584         verifyMocks();
4585 
4586         if (HttpStatus.SC_OK == result.getStatusLine().getStatusCode()) {
4587             Assert.assertTrue(condCap.hasCaptured()
4588                               || uncondCap.hasCaptured());
4589             if (uncondCap.hasCaptured()) {
4590                 Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
4591             }
4592         }
4593     }
4594 
4595     /* "A cache that receives an incomplete response (for example,
4596      * with fewer bytes of data than specified in a Content-Length
4597      * header) MAY store the response. However, the cache MUST treat
4598      * this as a partial response. Partial responses MAY be combined
4599      * as described in section 13.5.4; the result might be a full
4600      * response or might still be partial. A cache MUST NOT return a
4601      * partial response to a client without explicitly marking it as
4602      * such, using the 206 (Partial Content) status code. A cache MUST
4603      * NOT return a partial response using a status code of 200 (OK)."
4604      *
4605      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.8
4606      */
4607     @Test
4608     public void testIncompleteResponseMustNotBeReturnedToClientWithoutMarkingItAs206() throws Exception {
4609         originResponse.setEntity(HttpTestUtils.makeBody(128));
4610         originResponse.setHeader("Content-Length","256");
4611 
4612         backendExpectsAnyRequest().andReturn(originResponse);
4613 
4614         replayMocks();
4615         final HttpResponse result = impl.execute(route, request, context, null);
4616         verifyMocks();
4617 
4618         final int status = result.getStatusLine().getStatusCode();
4619         Assert.assertFalse(HttpStatus.SC_OK == status);
4620         if (status > 200 && status <= 299
4621             && HttpTestUtils.equivalent(originResponse.getEntity(),
4622                                         result.getEntity())) {
4623             Assert.assertTrue(HttpStatus.SC_PARTIAL_CONTENT == status);
4624         }
4625     }
4626 
4627     /* "Some HTTP methods MUST cause a cache to invalidate an
4628      * entity. This is either the entity referred to by the
4629      * Request-URI, or by the Location or Content-Location headers (if
4630      * present). These methods are:
4631      * - PUT
4632      * - DELETE
4633      * - POST
4634      *
4635      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
4636      */
4637     protected void testUnsafeOperationInvalidatesCacheForThatUri(
4638             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4639         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4640                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4641         final HttpResponse resp1 = HttpTestUtils.make200Response();
4642         resp1.setHeader("Cache-Control","public, max-age=3600");
4643 
4644         backendExpectsAnyRequestAndReturn(resp1);
4645 
4646         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4647 
4648         backendExpectsAnyRequestAndReturn(resp2);
4649 
4650         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4651                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
4652         final HttpResponse resp3 = HttpTestUtils.make200Response();
4653         resp3.setHeader("Cache-Control","public, max-age=3600");
4654 
4655         // this origin request MUST happen due to invalidation
4656         backendExpectsAnyRequestAndReturn(resp3);
4657 
4658         replayMocks();
4659         impl.execute(route, req1, context, null);
4660         impl.execute(route, unsafeReq, context, null);
4661         impl.execute(route, req3, context, null);
4662         verifyMocks();
4663     }
4664 
4665     @Test
4666     public void testPutToUriInvalidatesCacheForThatUri() throws Exception {
4667         final HttpRequest req = makeRequestWithBody("PUT","/");
4668         testUnsafeOperationInvalidatesCacheForThatUri(HttpRequestWrapper.wrap(req));
4669     }
4670 
4671     @Test
4672     public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception {
4673         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE","/"));
4674         testUnsafeOperationInvalidatesCacheForThatUri(req);
4675     }
4676 
4677     @Test
4678     public void testPostToUriInvalidatesCacheForThatUri() throws Exception {
4679         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4680         testUnsafeOperationInvalidatesCacheForThatUri(req);
4681     }
4682 
4683     protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
4684             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4685         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4686                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4687         final HttpResponse resp1 = HttpTestUtils.make200Response();
4688         resp1.setHeader("Cache-Control","public, max-age=3600");
4689 
4690         backendExpectsAnyRequestAndReturn(resp1);
4691 
4692         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4693 
4694         backendExpectsAnyRequestAndReturn(resp2);
4695 
4696         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4697                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4698         final HttpResponse resp3 = HttpTestUtils.make200Response();
4699         resp3.setHeader("Cache-Control","public, max-age=3600");
4700 
4701         // this origin request MUST happen due to invalidation
4702         backendExpectsAnyRequestAndReturn(resp3);
4703 
4704         replayMocks();
4705         impl.execute(route, req1, context, null);
4706         impl.execute(route, unsafeReq, context, null);
4707         impl.execute(route, req3, context, null);
4708         verifyMocks();
4709     }
4710 
4711     protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(
4712             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4713         unsafeReq.setHeader("Content-Location","http://foo.example.com/content");
4714         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4715     }
4716 
4717     protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(
4718             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4719         unsafeReq.setHeader("Content-Location","/content");
4720         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4721     }
4722 
4723     protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader(
4724             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4725         unsafeReq.setHeader("Location","http://foo.example.com/content");
4726         testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq);
4727     }
4728 
4729     @Test
4730     public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4731         final HttpRequestWrapper req2 = makeRequestWithBody("PUT","/");
4732         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2);
4733     }
4734 
4735     @Test
4736     public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4737         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4738         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4739     }
4740 
4741     @Test
4742     public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4743         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4744         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4745     }
4746 
4747     @Test
4748     public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4749         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4750         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4751     }
4752 
4753     @Test
4754     public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception {
4755         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4756         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4757     }
4758 
4759     @Test
4760     public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4761         final HttpRequestWrapper req = HttpRequestWrapper.wrap(new BasicHttpRequest("DELETE", "/"));
4762         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4763     }
4764 
4765     @Test
4766     public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception {
4767         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4768         testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req);
4769     }
4770 
4771     @Test
4772     public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception {
4773         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4774         testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req);
4775     }
4776 
4777     @Test
4778     public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception {
4779         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4780         testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
4781     }
4782 
4783     /* "In order to prevent denial of service attacks, an invalidation based on the URI
4784      *  in a Location or Content-Location header MUST only be performed if the host part
4785      *  is the same as in the Request-URI."
4786      *
4787      *  http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
4788      */
4789     protected void testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(
4790             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4791 
4792         final HttpHost otherHost = new HttpHost("bar.example.com", 80);
4793         final HttpRoute otherRoute = new HttpRoute(otherHost);
4794         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
4795                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4796         final HttpResponse resp1 = HttpTestUtils.make200Response();
4797         resp1.setHeader("Cache-Control","public, max-age=3600");
4798 
4799         backendExpectsAnyRequestAndReturn(resp1);
4800 
4801         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4802 
4803         backendExpectsAnyRequestAndReturn(resp2);
4804 
4805         final HttpRequestWrapper req3 = HttpRequestWrapper.wrap(
4806                 new BasicHttpRequest("GET", "/content", HttpVersion.HTTP_1_1));
4807 
4808         replayMocks();
4809         impl.execute(otherRoute, req1, context, null);
4810         impl.execute(route, unsafeReq, context, null);
4811         impl.execute(otherRoute, req3, context, null);
4812         verifyMocks();
4813     }
4814 
4815     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(
4816             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4817         unsafeReq.setHeader("Content-Location","http://bar.example.com/content");
4818         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4819     }
4820 
4821     protected void testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(
4822             final HttpRequestWrapper unsafeReq) throws Exception, IOException {
4823         unsafeReq.setHeader("Location","http://bar.example.com/content");
4824         testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
4825     }
4826 
4827     protected HttpRequestWrapper makeRequestWithBody(final String method, final String requestUri) {
4828         final HttpEntityEnclosingRequest req =
4829             new BasicHttpEntityEnclosingRequest(method, requestUri, HttpVersion.HTTP_1_1);
4830         final int nbytes = 128;
4831         req.setEntity(HttpTestUtils.makeBody(nbytes));
4832         req.setHeader("Content-Length",""+nbytes);
4833         return HttpRequestWrapper.wrap(req);
4834     }
4835 
4836     @Test
4837     public void testPutDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4838         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4839         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4840     }
4841 
4842     @Test
4843     public void testPutDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4844         final HttpRequestWrapper req = makeRequestWithBody("PUT","/");
4845         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4846     }
4847 
4848     @Test
4849     public void testPostDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4850         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4851         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4852     }
4853 
4854     @Test
4855     public void testPostDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4856         final HttpRequestWrapper req = makeRequestWithBody("POST","/");
4857         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4858     }
4859 
4860     @Test
4861     public void testDeleteDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
4862         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
4863                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4864         testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
4865     }
4866 
4867     @Test
4868     public void testDeleteDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
4869         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
4870                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4871         testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
4872     }
4873 
4874     /* "All methods that might be expected to cause modifications to the origin
4875      * server's resources MUST be written through to the origin server. This
4876      * currently includes all methods except for GET and HEAD. A cache MUST NOT
4877      * reply to such a request from a client before having transmitted the
4878      * request to the inbound server, and having received a corresponding
4879      * response from the inbound server."
4880      *
4881      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.11
4882      */
4883     private void testRequestIsWrittenThroughToOrigin(final HttpRequest req)
4884         throws Exception {
4885         final HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
4886         final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(req);
4887         EasyMock.expect(
4888                 mockBackend.execute(
4889                         EasyMock.eq(route),
4890                         eqRequest(wrapper),
4891                         EasyMock.isA(HttpClientContext.class),
4892                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
4893                                 Proxies.enhanceResponse(resp));
4894 
4895         replayMocks();
4896         impl.execute(route, wrapper, context, null);
4897         verifyMocks();
4898     }
4899 
4900     @Test @Ignore
4901     public void testOPTIONSRequestsAreWrittenThroughToOrigin()
4902         throws Exception {
4903         final HttpRequest req = HttpRequestWrapper.wrap(
4904                 new BasicHttpRequest("OPTIONS","*",HttpVersion.HTTP_1_1));
4905         testRequestIsWrittenThroughToOrigin(req);
4906     }
4907 
4908     @Test
4909     public void testPOSTRequestsAreWrittenThroughToOrigin()
4910         throws Exception {
4911         final HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST","/",HttpVersion.HTTP_1_1);
4912         req.setEntity(HttpTestUtils.makeBody(128));
4913         req.setHeader("Content-Length","128");
4914         testRequestIsWrittenThroughToOrigin(req);
4915     }
4916 
4917     @Test
4918     public void testPUTRequestsAreWrittenThroughToOrigin()
4919         throws Exception {
4920         final HttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("PUT","/",HttpVersion.HTTP_1_1);
4921         req.setEntity(HttpTestUtils.makeBody(128));
4922         req.setHeader("Content-Length","128");
4923         testRequestIsWrittenThroughToOrigin(req);
4924     }
4925 
4926     @Test
4927     public void testDELETERequestsAreWrittenThroughToOrigin()
4928         throws Exception {
4929         final HttpRequest req = HttpRequestWrapper.wrap(
4930                 new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1));
4931         testRequestIsWrittenThroughToOrigin(req);
4932     }
4933 
4934     @Test
4935     public void testTRACERequestsAreWrittenThroughToOrigin()
4936         throws Exception {
4937         final HttpRequest req = HttpRequestWrapper.wrap(
4938                 new BasicHttpRequest("TRACE","/",HttpVersion.HTTP_1_1));
4939         testRequestIsWrittenThroughToOrigin(req);
4940     }
4941 
4942     @Test
4943     public void testCONNECTRequestsAreWrittenThroughToOrigin()
4944         throws Exception {
4945         final HttpRequest req = HttpRequestWrapper.wrap(
4946                 new BasicHttpRequest("CONNECT","/",HttpVersion.HTTP_1_1));
4947         testRequestIsWrittenThroughToOrigin(req);
4948     }
4949 
4950     @Test
4951     public void testUnknownMethodRequestsAreWrittenThroughToOrigin()
4952         throws Exception {
4953         final HttpRequest req = HttpRequestWrapper.wrap(
4954                 new BasicHttpRequest("UNKNOWN","/",HttpVersion.HTTP_1_1));
4955         testRequestIsWrittenThroughToOrigin(req);
4956     }
4957 
4958     /* "If a cache receives a value larger than the largest positive
4959      * integer it can represent, or if any of its age calculations
4960      * overflows, it MUST transmit an Age header with a value of
4961      * 2147483648 (2^31)."
4962      *
4963      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6
4964      */
4965     @Test
4966     public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig()
4967         throws Exception {
4968         final String reallyOldAge = "1" + Long.MAX_VALUE;
4969         originResponse.setHeader("Age",reallyOldAge);
4970 
4971         backendExpectsAnyRequest().andReturn(originResponse);
4972 
4973         replayMocks();
4974         final HttpResponse result = impl.execute(route, request, context, null);
4975         verifyMocks();
4976 
4977         Assert.assertEquals("2147483648",
4978                             result.getFirstHeader("Age").getValue());
4979     }
4980 
4981     /* "A proxy MUST NOT modify the Allow header field even if it does not
4982      * understand all the methods specified, since the user agent might
4983      * have other means of communicating with the origin server.
4984      *
4985      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
4986      */
4987     @Test
4988     public void testDoesNotModifyAllowHeaderWithUnknownMethods()
4989         throws Exception {
4990         final String allowHeaderValue = "GET, HEAD, FOOBAR";
4991         originResponse.setHeader("Allow",allowHeaderValue);
4992         backendExpectsAnyRequest().andReturn(originResponse);
4993         replayMocks();
4994         final HttpResponse result = impl.execute(route, request, context, null);
4995         verifyMocks();
4996         Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse,"Allow"),
4997                             HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
4998     }
4999 
5000     /* "When a shared cache (see section 13.7) receives a request
5001      * containing an Authorization field, it MUST NOT return the
5002      * corresponding response as a reply to any other request, unless one
5003      * of the following specific exceptions holds:
5004      *
5005      * 1. If the response includes the "s-maxage" cache-control
5006      *    directive, the cache MAY use that response in replying to a
5007      *    subsequent request. But (if the specified maximum age has
5008      *    passed) a proxy cache MUST first revalidate it with the origin
5009      *    server, using the request-headers from the new request to allow
5010      *    the origin server to authenticate the new request. (This is the
5011      *    defined behavior for s-maxage.) If the response includes "s-
5012      *    maxage=0", the proxy MUST always revalidate it before re-using
5013      *    it.
5014      *
5015      * 2. If the response includes the "must-revalidate" cache-control
5016      *    directive, the cache MAY use that response in replying to a
5017      *    subsequent request. But if the response is stale, all caches
5018      *    MUST first revalidate it with the origin server, using the
5019      *    request-headers from the new request to allow the origin server
5020      *    to authenticate the new request.
5021      *
5022      * 3. If the response includes the "public" cache-control directive,
5023      *    it MAY be returned in reply to any subsequent request.
5024      */
5025     protected void testSharedCacheRevalidatesAuthorizedResponse(
5026             final HttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception,
5027             IOException {
5028         if (config.isSharedCache()) {
5029             final String authorization = "Basic dXNlcjpwYXNzd2Q=";
5030             final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5031                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5032             req1.setHeader("Authorization",authorization);
5033 
5034             backendExpectsAnyRequestAndReturn(authorizedResponse);
5035 
5036             final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5037                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5038             final HttpResponse resp2 = HttpTestUtils.make200Response();
5039             resp2.setHeader("Cache-Control","max-age=3600");
5040 
5041             if (maxTimes > 0) {
5042                 // this request MUST happen
5043                 backendExpectsAnyRequest().andReturn(
5044                         Proxies.enhanceResponse(resp2)).times(minTimes,maxTimes);
5045             }
5046 
5047             replayMocks();
5048             impl.execute(route, req1, context, null);
5049             impl.execute(route, req2, context, null);
5050             verifyMocks();
5051         }
5052     }
5053 
5054     @Test
5055     public void testSharedCacheMustNotNormallyCacheAuthorizedResponses()
5056         throws Exception {
5057         final HttpResponse resp = HttpTestUtils.make200Response();
5058         resp.setHeader("Cache-Control","max-age=3600");
5059         resp.setHeader("ETag","\"etag\"");
5060         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
5061     }
5062 
5063     @Test
5064     public void testSharedCacheMayCacheAuthorizedResponsesWithSMaxAgeHeader()
5065         throws Exception {
5066         final HttpResponse resp = HttpTestUtils.make200Response();
5067         resp.setHeader("Cache-Control","s-maxage=3600");
5068         resp.setHeader("ETag","\"etag\"");
5069         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5070     }
5071 
5072     @Test
5073     public void testSharedCacheMustRevalidateAuthorizedResponsesWhenSMaxAgeIsZero()
5074         throws Exception {
5075         final HttpResponse resp = HttpTestUtils.make200Response();
5076         resp.setHeader("Cache-Control","s-maxage=0");
5077         resp.setHeader("ETag","\"etag\"");
5078         testSharedCacheRevalidatesAuthorizedResponse(resp, 1, 1);
5079     }
5080 
5081     @Test
5082     public void testSharedCacheMayCacheAuthorizedResponsesWithMustRevalidate()
5083         throws Exception {
5084         final HttpResponse resp = HttpTestUtils.make200Response();
5085         resp.setHeader("Cache-Control","must-revalidate");
5086         resp.setHeader("ETag","\"etag\"");
5087         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5088     }
5089 
5090     @Test
5091     public void testSharedCacheMayCacheAuthorizedResponsesWithCacheControlPublic()
5092         throws Exception {
5093         final HttpResponse resp = HttpTestUtils.make200Response();
5094         resp.setHeader("Cache-Control","public");
5095         testSharedCacheRevalidatesAuthorizedResponse(resp, 0, 1);
5096     }
5097 
5098     protected void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(
5099             final HttpResponse authorizedResponse) throws Exception, IOException,
5100             ClientProtocolException {
5101         if (config.isSharedCache()) {
5102             final String authorization1 = "Basic dXNlcjpwYXNzd2Q=";
5103             final String authorization2 = "Basic dXNlcjpwYXNzd2Qy";
5104 
5105             final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5106                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5107             req1.setHeader("Authorization",authorization1);
5108 
5109             backendExpectsAnyRequestAndReturn(authorizedResponse);
5110 
5111             final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5112                     new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5113             req2.setHeader("Authorization",authorization2);
5114 
5115             final HttpResponse resp2 = HttpTestUtils.make200Response();
5116 
5117             final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5118             EasyMock.expect(
5119                     mockBackend.execute(
5120                             EasyMock.eq(route),
5121                             EasyMock.capture(cap),
5122                             EasyMock.isA(HttpClientContext.class),
5123                             EasyMock.<HttpExecutionAware>isNull())).andReturn(
5124                                     Proxies.enhanceResponse(resp2));
5125 
5126             replayMocks();
5127             impl.execute(route, req1, context, null);
5128             impl.execute(route, req2, context, null);
5129             verifyMocks();
5130 
5131             final HttpRequest captured = cap.getValue();
5132             Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(req2, "Authorization"),
5133                     HttpTestUtils.getCanonicalHeaderValue(captured, "Authorization"));
5134         }
5135     }
5136 
5137     @Test
5138     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithSMaxAge()
5139     throws Exception {
5140         final Date now = new Date();
5141         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5142         final HttpResponse resp1 = HttpTestUtils.make200Response();
5143         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5144         resp1.setHeader("ETag","\"etag\"");
5145         resp1.setHeader("Cache-Control","s-maxage=5");
5146 
5147         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
5148     }
5149 
5150     @Test
5151     public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponsesWithMustRevalidate()
5152     throws Exception {
5153         final Date now = new Date();
5154         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5155         final HttpResponse resp1 = HttpTestUtils.make200Response();
5156         resp1.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5157         resp1.setHeader("ETag","\"etag\"");
5158         resp1.setHeader("Cache-Control","maxage=5, must-revalidate");
5159 
5160         testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
5161     }
5162 
5163     /* "If a cache returns a stale response, either because of a max-stale
5164      * directive on a request, or because the cache is configured to
5165      * override the expiration time of a response, the cache MUST attach a
5166      * Warning header to the stale response, using Warning 110 (Response
5167      * is stale).
5168      *
5169      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
5170      *
5171      * "110 Response is stale MUST be included whenever the returned
5172      * response is stale."
5173      *
5174      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
5175      */
5176     @Test
5177     public void testWarning110IsAddedToStaleResponses()
5178         throws Exception {
5179         final Date now = new Date();
5180         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5181         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5182                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5183         final HttpResponse resp1 = HttpTestUtils.make200Response();
5184         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5185         resp1.setHeader("Cache-Control","max-age=5");
5186         resp1.setHeader("Etag","\"etag\"");
5187 
5188         backendExpectsAnyRequestAndReturn(resp1);
5189 
5190         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5191                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5192         req2.setHeader("Cache-Control","max-stale=60");
5193         final HttpResponse resp2 = HttpTestUtils.make200Response();
5194 
5195         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5196         EasyMock.expect(
5197                 mockBackend.execute(
5198                         EasyMock.eq(route),
5199                         EasyMock.capture(cap),
5200                         EasyMock.isA(HttpClientContext.class),
5201                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5202                                 Proxies.enhanceResponse(resp2)).times(0,1);
5203 
5204         replayMocks();
5205         impl.execute(route, req1, context, null);
5206         final HttpResponse result = impl.execute(route, req2, context, null);
5207         verifyMocks();
5208 
5209         if (!cap.hasCaptured()) {
5210             boolean found110Warning = false;
5211             for(final Header h : result.getHeaders("Warning")) {
5212                 for(final HeaderElement elt : h.getElements()) {
5213                     final String[] parts = elt.getName().split("\\s");
5214                     if ("110".equals(parts[0])) {
5215                         found110Warning = true;
5216                         break;
5217                     }
5218                 }
5219             }
5220             Assert.assertTrue(found110Warning);
5221         }
5222     }
5223 
5224     /* "Field names MUST NOT be included with the no-cache directive in a
5225      * request."
5226      *
5227      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5228      */
5229     @Test
5230     public void testDoesNotTransmitNoCacheDirectivesWithFieldsDownstream()
5231         throws Exception {
5232         request.setHeader("Cache-Control","no-cache=\"X-Field\"");
5233         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5234         EasyMock.expect(mockBackend.execute(
5235                 EasyMock.eq(route),
5236                 EasyMock.capture(cap),
5237                 EasyMock.isA(HttpClientContext.class),
5238                 EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse).times(0,1);
5239 
5240         replayMocks();
5241         try {
5242             impl.execute(route, request, context, null);
5243         } catch (final ClientProtocolException acceptable) {
5244         }
5245         verifyMocks();
5246 
5247         if (cap.hasCaptured()) {
5248             final HttpRequest captured = cap.getValue();
5249             for(final Header h : captured.getHeaders("Cache-Control")) {
5250                 for(final HeaderElement elt : h.getElements()) {
5251                     if ("no-cache".equals(elt.getName())) {
5252                         Assert.assertNull(elt.getValue());
5253                     }
5254                 }
5255             }
5256         }
5257     }
5258 
5259     /* "The request includes a "no-cache" cache-control directive or, for
5260      * compatibility with HTTP/1.0 clients, "Pragma: no-cache".... The
5261      * server MUST NOT use a cached copy when responding to such a request."
5262      *
5263      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5264      */
5265     protected void testCacheIsNotUsedWhenRespondingToRequest(final HttpRequestWrapper req)
5266         throws Exception {
5267         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5268                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5269         final HttpResponse resp1 = HttpTestUtils.make200Response();
5270         resp1.setHeader("Etag","\"etag\"");
5271         resp1.setHeader("Cache-Control","max-age=3600");
5272 
5273         backendExpectsAnyRequestAndReturn(resp1);
5274 
5275         final HttpResponse resp2 = HttpTestUtils.make200Response();
5276         resp2.setHeader("Etag","\"etag2\"");
5277         resp2.setHeader("Cache-Control","max-age=1200");
5278 
5279         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5280         EasyMock.expect(mockBackend.execute(
5281                 EasyMock.eq(route),
5282                 EasyMock.capture(cap),
5283                 EasyMock.isA(HttpClientContext.class),
5284                 EasyMock.<HttpExecutionAware>isNull())).andReturn(
5285                         Proxies.enhanceResponse(resp2));
5286 
5287         replayMocks();
5288         impl.execute(route, req1, context, null);
5289         final HttpResponse result = impl.execute(route, req, context, null);
5290         verifyMocks();
5291 
5292         Assert.assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
5293         final HttpRequest captured = cap.getValue();
5294         Assert.assertTrue(HttpTestUtils.equivalent(req, captured));
5295     }
5296 
5297     @Test
5298     public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache()
5299         throws Exception {
5300         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
5301                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5302         req.setHeader("Cache-Control","no-cache");
5303         testCacheIsNotUsedWhenRespondingToRequest(req);
5304     }
5305 
5306     @Test
5307     public void testCacheIsNotUsedWhenRespondingToRequestWithPragmaNoCache()
5308         throws Exception {
5309         final HttpRequestWrapper req = HttpRequestWrapper.wrap(
5310                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5311         req.setHeader("Pragma","no-cache");
5312         testCacheIsNotUsedWhenRespondingToRequest(req);
5313     }
5314 
5315     /* "When the must-revalidate directive is present in a response received
5316      * by a cache, that cache MUST NOT use the entry after it becomes stale
5317      * to respond to a subsequent request without first revalidating it with
5318      * the origin server. (I.e., the cache MUST do an end-to-end
5319      * revalidation every time, if, based solely on the origin server's
5320      * Expires or max-age value, the cached response is stale.)"
5321      *
5322      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5323      */
5324     protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
5325             final HttpResponse staleResponse) throws Exception {
5326         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5327                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5328 
5329         backendExpectsAnyRequestAndReturn(staleResponse);
5330 
5331         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5332                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5333         req2.setHeader("Cache-Control","max-stale=3600");
5334         final HttpResponse resp2 = HttpTestUtils.make200Response();
5335         resp2.setHeader("ETag","\"etag2\"");
5336         resp2.setHeader("Cache-Control","max-age=5, must-revalidate");
5337 
5338         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5339         // this request MUST happen
5340         EasyMock.expect(
5341                 mockBackend.execute(
5342                         EasyMock.eq(route),
5343                         EasyMock.capture(cap),
5344                         EasyMock.isA(HttpClientContext.class),
5345                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5346                                 Proxies.enhanceResponse(resp2));
5347 
5348         replayMocks();
5349         impl.execute(route, req1, context, null);
5350         impl.execute(route, req2, context, null);
5351         verifyMocks();
5352 
5353         final HttpRequest reval = cap.getValue();
5354         boolean foundMaxAge0 = false;
5355         for(final Header h : reval.getHeaders("Cache-Control")) {
5356             for(final HeaderElement elt : h.getElements()) {
5357                 if ("max-age".equalsIgnoreCase(elt.getName())
5358                     && "0".equals(elt.getValue())) {
5359                     foundMaxAge0 = true;
5360                 }
5361             }
5362         }
5363         Assert.assertTrue(foundMaxAge0);
5364     }
5365 
5366     @Test
5367     public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrigin()
5368         throws Exception {
5369         final HttpResponse response = HttpTestUtils.make200Response();
5370         final Date now = new Date();
5371         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5372         response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5373         response.setHeader("ETag","\"etag1\"");
5374         response.setHeader("Cache-Control","max-age=5, must-revalidate");
5375 
5376         testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
5377     }
5378 
5379 
5380     /* "In all circumstances an HTTP/1.1 cache MUST obey the must-revalidate
5381      * directive; in particular, if the cache cannot reach the origin server
5382      * for any reason, it MUST generate a 504 (Gateway Timeout) response."
5383      */
5384     protected void testGenerates504IfCannotRevalidateStaleResponse(
5385             final HttpResponse staleResponse) throws Exception {
5386         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5387                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5388 
5389         backendExpectsAnyRequestAndReturn(staleResponse);
5390 
5391         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5392                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5393 
5394         backendExpectsAnyRequest().andThrow(new SocketTimeoutException());
5395 
5396         replayMocks();
5397         impl.execute(route, req1, context, null);
5398         final HttpResponse result = impl.execute(route, req2, context, null);
5399         verifyMocks();
5400 
5401         Assert.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
5402                             result.getStatusLine().getStatusCode());
5403     }
5404 
5405     @Test
5406     public void testGenerates504IfCannotRevalidateAMustRevalidateEntry()
5407         throws Exception {
5408         final HttpResponse resp1 = HttpTestUtils.make200Response();
5409         final Date now = new Date();
5410         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5411         resp1.setHeader("ETag","\"etag\"");
5412         resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5413         resp1.setHeader("Cache-Control","max-age=5,must-revalidate");
5414 
5415         testGenerates504IfCannotRevalidateStaleResponse(resp1);
5416     }
5417 
5418     /* "The proxy-revalidate directive has the same meaning as the must-
5419      * revalidate directive, except that it does not apply to non-shared
5420      * user agent caches."
5421      *
5422      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
5423      */
5424     @Test
5425     public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin()
5426         throws Exception {
5427         if (config.isSharedCache()) {
5428             final HttpResponse response = HttpTestUtils.make200Response();
5429             final Date now = new Date();
5430             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5431             response.setHeader("Date",DateUtils.formatDate(tenSecondsAgo));
5432             response.setHeader("ETag","\"etag1\"");
5433             response.setHeader("Cache-Control","max-age=5, proxy-revalidate");
5434 
5435             testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
5436         }
5437     }
5438 
5439     @Test
5440     public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry()
5441         throws Exception {
5442         if (config.isSharedCache()) {
5443             final HttpResponse resp1 = HttpTestUtils.make200Response();
5444             final Date now = new Date();
5445             final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
5446             resp1.setHeader("ETag","\"etag\"");
5447             resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
5448             resp1.setHeader("Cache-Control","max-age=5,proxy-revalidate");
5449 
5450             testGenerates504IfCannotRevalidateStaleResponse(resp1);
5451         }
5452     }
5453 
5454     /* "[The cache control directive] "private" Indicates that all or part of
5455      * the response message is intended for a single user and MUST NOT be
5456      * cached by a shared cache."
5457      *
5458      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
5459      */
5460     @Test
5461     public void testCacheControlPrivateIsNotCacheableBySharedCache()
5462     throws Exception {
5463        if (config.isSharedCache()) {
5464                final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5465                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5466                final HttpResponse resp1 = HttpTestUtils.make200Response();
5467                resp1.setHeader("Cache-Control","private,max-age=3600");
5468 
5469                backendExpectsAnyRequestAndReturn(resp1);
5470 
5471                final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5472                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5473                final HttpResponse resp2 = HttpTestUtils.make200Response();
5474                // this backend request MUST happen
5475                backendExpectsAnyRequestAndReturn(resp2);
5476 
5477                replayMocks();
5478                impl.execute(route, req1, context, null);
5479                impl.execute(route, req2, context, null);
5480                verifyMocks();
5481        }
5482     }
5483 
5484     @Test
5485     public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache()
5486     throws Exception {
5487        if (config.isSharedCache()) {
5488                final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5489                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5490                final HttpResponse resp1 = HttpTestUtils.make200Response();
5491                resp1.setHeader("X-Personal","stuff");
5492                resp1.setHeader("Cache-Control","private=\"X-Personal\",s-maxage=3600");
5493 
5494                backendExpectsAnyRequestAndReturn(resp1);
5495 
5496                final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5497                        new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5498                final HttpResponse resp2 = HttpTestUtils.make200Response();
5499 
5500                // this backend request MAY happen
5501                backendExpectsAnyRequestAndReturn(resp2).times(0,1);
5502 
5503                replayMocks();
5504                impl.execute(route, req1, context, null);
5505                final HttpResponse result = impl.execute(route, req2, context, null);
5506                verifyMocks();
5507                Assert.assertNull(result.getFirstHeader("X-Personal"));
5508        }
5509     }
5510 
5511     /* "If the no-cache directive does not specify a field-name, then a
5512      * cache MUST NOT use the response to satisfy a subsequent request
5513      * without successful revalidation with the origin server. This allows
5514      * an origin server to prevent caching even by caches that have been
5515      * configured to return stale responses to client requests."
5516      *
5517      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
5518      */
5519     @Test
5520     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation()
5521     throws Exception {
5522         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5523                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5524         final HttpResponse resp1 = HttpTestUtils.make200Response();
5525         resp1.setHeader("ETag","\"etag\"");
5526         resp1.setHeader("Cache-Control","no-cache");
5527 
5528         backendExpectsAnyRequestAndReturn(resp1);
5529 
5530         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5531                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5532         final HttpResponse resp2 = HttpTestUtils.make200Response();
5533 
5534         // this MUST happen
5535         backendExpectsAnyRequestAndReturn(resp2);
5536 
5537         replayMocks();
5538         impl.execute(route, req1, context, null);
5539         impl.execute(route, req2, context, null);
5540         verifyMocks();
5541     }
5542 
5543     @Test
5544     public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWithContraryIndications()
5545     throws Exception {
5546         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5547                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5548         final HttpResponse resp1 = HttpTestUtils.make200Response();
5549         resp1.setHeader("ETag","\"etag\"");
5550         resp1.setHeader("Cache-Control","no-cache,s-maxage=3600");
5551 
5552         backendExpectsAnyRequestAndReturn(resp1);
5553 
5554         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5555                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5556         req2.setHeader("Cache-Control","max-stale=7200");
5557         final HttpResponse resp2 = HttpTestUtils.make200Response();
5558 
5559         // this MUST happen
5560         backendExpectsAnyRequestAndReturn(resp2);
5561 
5562         replayMocks();
5563         impl.execute(route, req1, context, null);
5564         impl.execute(route, req2, context, null);
5565         verifyMocks();
5566     }
5567 
5568     /* "If the no-cache directive does specify one or more field-names, then
5569      * a cache MAY use the response to satisfy a subsequent request, subject
5570      * to any other restrictions on caching. However, the specified
5571      * field-name(s) MUST NOT be sent in the response to a subsequent request
5572      * without successful revalidation with the origin server."
5573      */
5574     @Test
5575     public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation()
5576     throws Exception {
5577         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5578                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5579         final HttpResponse resp1 = HttpTestUtils.make200Response();
5580         resp1.setHeader("ETag","\"etag\"");
5581         resp1.setHeader("X-Stuff","things");
5582         resp1.setHeader("Cache-Control","no-cache=\"X-Stuff\", max-age=3600");
5583 
5584         backendExpectsAnyRequestAndReturn(resp1);
5585 
5586         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5587                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5588         final HttpResponse resp2 = HttpTestUtils.make200Response();
5589         resp2.setHeader("ETag","\"etag\"");
5590         resp2.setHeader("X-Stuff","things");
5591         resp2.setHeader("Cache-Control","no-cache=\"X-Stuff\",max-age=3600");
5592 
5593         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5594         EasyMock.expect(
5595                 mockBackend.execute(
5596                         EasyMock.eq(route),
5597                         EasyMock.capture(cap),
5598                         EasyMock.isA(HttpClientContext.class),
5599                         EasyMock.<HttpExecutionAware>isNull())).andReturn(
5600                                 Proxies.enhanceResponse(resp2)).times(0,1);
5601 
5602         replayMocks();
5603         impl.execute(route, req1, context, null);
5604         final HttpResponse result = impl.execute(route, req2, context, null);
5605         verifyMocks();
5606 
5607         if (!cap.hasCaptured()) {
5608             Assert.assertNull(result.getFirstHeader("X-Stuff"));
5609         }
5610     }
5611 
5612     /* "The purpose of the no-store directive is to prevent the inadvertent
5613      * release or retention of sensitive information (for example, on backup
5614      * tapes). The no-store directive applies to the entire message, and MAY
5615      * be sent either in a response or in a request. If sent in a request, a
5616      * cache MUST NOT store any part of either this request or any response
5617      * to it. If sent in a response, a cache MUST NOT store any part of
5618      * either this response or the request that elicited it. This directive
5619      * applies to both non- shared and shared caches. "MUST NOT store" in
5620      * this context means that the cache MUST NOT intentionally store the
5621      * information in non-volatile storage, and MUST make a best-effort
5622      * attempt to remove the information from volatile storage as promptly
5623      * as possible after forwarding it."
5624      *
5625      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
5626      */
5627     @Test
5628     public void testNoStoreOnRequestIsNotStoredInCache()
5629     throws Exception {
5630         emptyMockCacheExpectsNoPuts();
5631         request.setHeader("Cache-Control","no-store");
5632         backendExpectsAnyRequest().andReturn(originResponse);
5633 
5634         replayMocks();
5635         impl.execute(route, request, context, null);
5636         verifyMocks();
5637     }
5638 
5639     @Test
5640     public void testNoStoreOnRequestIsNotStoredInCacheEvenIfResponseMarkedCacheable()
5641     throws Exception {
5642         emptyMockCacheExpectsNoPuts();
5643         request.setHeader("Cache-Control","no-store");
5644         originResponse.setHeader("Cache-Control","max-age=3600");
5645         backendExpectsAnyRequest().andReturn(originResponse);
5646 
5647         replayMocks();
5648         impl.execute(route, request, context, null);
5649         verifyMocks();
5650     }
5651 
5652     @Test
5653     public void testNoStoreOnResponseIsNotStoredInCache()
5654     throws Exception {
5655         emptyMockCacheExpectsNoPuts();
5656         originResponse.setHeader("Cache-Control","no-store");
5657         backendExpectsAnyRequest().andReturn(originResponse);
5658 
5659         replayMocks();
5660         impl.execute(route, request, context, null);
5661         verifyMocks();
5662     }
5663 
5664     @Test
5665     public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators()
5666     throws Exception {
5667         emptyMockCacheExpectsNoPuts();
5668         originResponse.setHeader("Cache-Control","no-store,max-age=3600");
5669         backendExpectsAnyRequest().andReturn(originResponse);
5670 
5671         replayMocks();
5672         impl.execute(route, request, context, null);
5673         verifyMocks();
5674     }
5675 
5676     /* "If multiple encodings have been applied to an entity, the content
5677      * codings MUST be listed in the order in which they were applied."
5678      *
5679      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
5680      */
5681     @Test
5682     public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved()
5683         throws Exception {
5684         originResponse.addHeader("Content-Encoding","gzip");
5685         originResponse.addHeader("Content-Encoding","deflate");
5686         backendExpectsAnyRequest().andReturn(originResponse);
5687 
5688         replayMocks();
5689         final HttpResponse result = impl.execute(route, request, context, null);
5690         verifyMocks();
5691         int total_encodings = 0;
5692         for(final Header hdr : result.getHeaders("Content-Encoding")) {
5693             for(final HeaderElement elt : hdr.getElements()) {
5694                 switch(total_encodings) {
5695                 case 0:
5696                     Assert.assertEquals("gzip", elt.getName());
5697                     break;
5698                 case 1:
5699                     Assert.assertEquals("deflate", elt.getName());
5700                     break;
5701                 default:
5702                     Assert.fail("too many encodings");
5703                 }
5704                 total_encodings++;
5705             }
5706         }
5707         Assert.assertEquals(2, total_encodings);
5708     }
5709 
5710     @Test
5711     public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved()
5712         throws Exception {
5713         originResponse.addHeader("Content-Encoding","gzip,deflate");
5714         backendExpectsAnyRequest().andReturn(originResponse);
5715 
5716         replayMocks();
5717         final HttpResponse result = impl.execute(route, request, context, null);
5718         verifyMocks();
5719         int total_encodings = 0;
5720         for(final Header hdr : result.getHeaders("Content-Encoding")) {
5721             for(final HeaderElement elt : hdr.getElements()) {
5722                 switch(total_encodings) {
5723                 case 0:
5724                     Assert.assertEquals("gzip", elt.getName());
5725                     break;
5726                 case 1:
5727                     Assert.assertEquals("deflate", elt.getName());
5728                     break;
5729                 default:
5730                     Assert.fail("too many encodings");
5731                 }
5732                 total_encodings++;
5733             }
5734         }
5735         Assert.assertEquals(2, total_encodings);
5736     }
5737 
5738     /* "A cache cannot assume that an entity with a Content-Location
5739      * different from the URI used to retrieve it can be used to respond
5740      * to later requests on that Content-Location URI."
5741      *
5742      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.14
5743      */
5744     @Test
5745     public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource()
5746         throws Exception {
5747         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5748                 new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1));
5749         final HttpResponse resp1 = HttpTestUtils.make200Response();
5750         resp1.setHeader("Cache-Control","public,max-age=3600");
5751         resp1.setHeader("Etag","\"etag\"");
5752         resp1.setHeader("Content-Location","http://foo.example.com/bar");
5753 
5754         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5755                 new BasicHttpRequest("GET", "/bar", HttpVersion.HTTP_1_1));
5756         final HttpResponse resp2 = HttpTestUtils.make200Response();
5757         resp2.setHeader("Cache-Control","public,max-age=3600");
5758         resp2.setHeader("Etag","\"etag\"");
5759 
5760         backendExpectsAnyRequestAndReturn(resp1);
5761         backendExpectsAnyRequestAndReturn(resp2);
5762 
5763         replayMocks();
5764         impl.execute(route, req1, context, null);
5765         impl.execute(route, req2, context, null);
5766         verifyMocks();
5767     }
5768 
5769     /* "A received message that does not have a Date header field MUST be
5770      * assigned one by the recipient if the message will be cached by that
5771      * recipient or gatewayed via a protocol which requires a Date."
5772      *
5773      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
5774      */
5775     @Test
5776     public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne()
5777         throws Exception {
5778         originResponse.removeHeaders("Date");
5779         originResponse.setHeader("Cache-Control","public");
5780         originResponse.setHeader("ETag","\"etag\"");
5781 
5782         backendExpectsAnyRequest().andReturn(originResponse);
5783 
5784         replayMocks();
5785         final HttpResponse result = impl.execute(route, request, context, null);
5786         verifyMocks();
5787         Assert.assertNotNull(result.getFirstHeader("Date"));
5788     }
5789 
5790     /* "The Expires entity-header field gives the date/time after which the
5791      * response is considered stale.... HTTP/1.1 clients and caches MUST
5792      * treat other invalid date formats, especially including the value '0',
5793      * as in the past (i.e., 'already expired')."
5794      *
5795      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5796      */
5797     private void testInvalidExpiresHeaderIsTreatedAsStale(
5798             final String expiresHeader) throws Exception {
5799         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5800                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5801         final HttpResponse resp1 = HttpTestUtils.make200Response();
5802         resp1.setHeader("Cache-Control","public");
5803         resp1.setHeader("ETag","\"etag\"");
5804         resp1.setHeader("Expires", expiresHeader);
5805 
5806         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5807                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5808         final HttpResponse resp2 = HttpTestUtils.make200Response();
5809 
5810         backendExpectsAnyRequestAndReturn(resp1);
5811         // second request to origin MUST happen
5812         backendExpectsAnyRequestAndReturn(resp2);
5813 
5814         replayMocks();
5815         impl.execute(route, req1, context, null);
5816         impl.execute(route, req2, context, null);
5817         verifyMocks();
5818     }
5819 
5820     @Test
5821     public void testMalformedExpiresHeaderIsTreatedAsStale()
5822         throws Exception {
5823         testInvalidExpiresHeaderIsTreatedAsStale("garbage");
5824     }
5825 
5826     @Test
5827     public void testExpiresZeroHeaderIsTreatedAsStale()
5828         throws Exception {
5829         testInvalidExpiresHeaderIsTreatedAsStale("0");
5830     }
5831 
5832     /* "To mark a response as 'already expired,' an origin server sends
5833      * an Expires date that is equal to the Date header value."
5834      *
5835      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
5836      */
5837     @Test
5838     public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale()
5839         throws Exception {
5840         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
5841                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5842         final HttpResponse resp1 = HttpTestUtils.make200Response();
5843         resp1.setHeader("Cache-Control","public");
5844         resp1.setHeader("ETag","\"etag\"");
5845         resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
5846 
5847         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
5848                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
5849         final HttpResponse resp2 = HttpTestUtils.make200Response();
5850 
5851         backendExpectsAnyRequestAndReturn(resp1);
5852         // second request to origin MUST happen
5853         backendExpectsAnyRequestAndReturn(resp2);
5854 
5855         replayMocks();
5856         impl.execute(route, req1, context, null);
5857         impl.execute(route, req2, context, null);
5858         verifyMocks();
5859     }
5860 
5861     /* "If the response is being forwarded through a proxy, the proxy
5862      * application MUST NOT modify the Server response-header."
5863      *
5864      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38
5865      */
5866     @Test
5867     public void testDoesNotModifyServerResponseHeader()
5868         throws Exception {
5869         final String server = "MockServer/1.0";
5870         originResponse.setHeader("Server", server);
5871 
5872         backendExpectsAnyRequest().andReturn(originResponse);
5873 
5874         replayMocks();
5875         final HttpResponse result = impl.execute(route, request, context, null);
5876         verifyMocks();
5877         Assert.assertEquals(server, result.getFirstHeader("Server").getValue());
5878     }
5879 
5880     /* "If multiple encodings have been applied to an entity, the transfer-
5881      * codings MUST be listed in the order in which they were applied."
5882      *
5883      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
5884      */
5885     @Test
5886     public void testOrderOfMultipleTransferEncodingHeadersIsPreserved()
5887         throws Exception {
5888         originResponse.addHeader("Transfer-Encoding","chunked");
5889         originResponse.addHeader("Transfer-Encoding","x-transfer");
5890 
5891         backendExpectsAnyRequest().andReturn(originResponse);
5892 
5893         replayMocks();
5894         final HttpResponse result = impl.execute(route, request, context, null);
5895         verifyMocks();
5896         int transfer_encodings = 0;
5897         for(final Header h : result.getHeaders("Transfer-Encoding")) {
5898             for(final HeaderElement elt : h.getElements()) {
5899                 switch(transfer_encodings) {
5900                 case 0:
5901                     Assert.assertEquals("chunked",elt.getName());
5902                     break;
5903                 case 1:
5904                     Assert.assertEquals("x-transfer",elt.getName());
5905                     break;
5906                 default:
5907                     Assert.fail("too many transfer encodings");
5908                 }
5909                 transfer_encodings++;
5910             }
5911         }
5912         Assert.assertEquals(2, transfer_encodings);
5913     }
5914 
5915     @Test
5916     public void testOrderOfMultipleTransferEncodingsInSingleHeadersIsPreserved()
5917         throws Exception {
5918         originResponse.addHeader("Transfer-Encoding","chunked, x-transfer");
5919 
5920         backendExpectsAnyRequest().andReturn(originResponse);
5921 
5922         replayMocks();
5923         final HttpResponse result = impl.execute(route, request, context, null);
5924         verifyMocks();
5925         int transfer_encodings = 0;
5926         for(final Header h : result.getHeaders("Transfer-Encoding")) {
5927             for(final HeaderElement elt : h.getElements()) {
5928                 switch(transfer_encodings) {
5929                 case 0:
5930                     Assert.assertEquals("chunked",elt.getName());
5931                     break;
5932                 case 1:
5933                     Assert.assertEquals("x-transfer",elt.getName());
5934                     break;
5935                 default:
5936                     Assert.fail("too many transfer encodings");
5937                 }
5938                 transfer_encodings++;
5939             }
5940         }
5941         Assert.assertEquals(2, transfer_encodings);
5942     }
5943 
5944     /* "A Vary field value of '*' signals that unspecified parameters
5945      * not limited to the request-headers (e.g., the network address
5946      * of the client), play a role in the selection of the response
5947      * representation. The '*' value MUST NOT be generated by a proxy
5948      * server; it may only be generated by an origin server."
5949      *
5950      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
5951      */
5952     @Test
5953     public void testVaryStarIsNotGeneratedByProxy()
5954         throws Exception {
5955         request.setHeader("User-Agent","my-agent/1.0");
5956         originResponse.setHeader("Cache-Control","public, max-age=3600");
5957         originResponse.setHeader("Vary","User-Agent");
5958         originResponse.setHeader("ETag","\"etag\"");
5959 
5960         backendExpectsAnyRequest().andReturn(originResponse);
5961 
5962         replayMocks();
5963         final HttpResponse result = impl.execute(route, request, context, null);
5964         verifyMocks();
5965         for(final Header h : result.getHeaders("Vary")) {
5966             for(final HeaderElement elt : h.getElements()) {
5967                 Assert.assertFalse("*".equals(elt.getName()));
5968             }
5969         }
5970     }
5971 
5972     /* "The Via general-header field MUST be used by gateways and proxies
5973      * to indicate the intermediate protocols and recipients between the
5974      * user agent and the server on requests, and between the origin server
5975      * and the client on responses."
5976      *
5977      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
5978      */
5979     @Test
5980     public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
5981         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
5982         request.removeHeaders("Via");
5983         EasyMock.expect(
5984                 mockBackend.execute(
5985                         EasyMock.isA(HttpRoute.class),
5986                         EasyMock.capture(cap),
5987                         EasyMock.isA(HttpClientContext.class),
5988                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
5989 
5990         replayMocks();
5991         impl.execute(route, request, context, null);
5992         verifyMocks();
5993 
5994         final HttpRequest captured = cap.getValue();
5995         final String via = captured.getFirstHeader("Via").getValue();
5996         assertValidViaHeader(via);
5997     }
5998 
5999     @Test
6000     public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception {
6001         originResponse.removeHeaders("Via");
6002         backendExpectsAnyRequest().andReturn(originResponse);
6003         replayMocks();
6004         final HttpResponse result = impl.execute(route, request, context, null);
6005         verifyMocks();
6006         assertValidViaHeader(result.getFirstHeader("Via").getValue());
6007     }
6008 
6009 
6010     private void assertValidViaHeader(final String via) {
6011         //        Via =  "Via" ":" 1#( received-protocol received-by [ comment ] )
6012         //        received-protocol = [ protocol-name "/" ] protocol-version
6013         //        protocol-name     = token
6014         //        protocol-version  = token
6015         //        received-by       = ( host [ ":" port ] ) | pseudonym
6016         //        pseudonym         = token
6017 
6018         final String[] parts = via.split("\\s+");
6019         Assert.assertTrue(parts.length >= 2);
6020 
6021         // received protocol
6022         final String receivedProtocol = parts[0];
6023         final String[] protocolParts = receivedProtocol.split("/");
6024         Assert.assertTrue(protocolParts.length >= 1);
6025         Assert.assertTrue(protocolParts.length <= 2);
6026 
6027         final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+";
6028         for(final String protocolPart : protocolParts) {
6029             Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart));
6030         }
6031 
6032         // received-by
6033         if (!Pattern.matches(tokenRegexp, parts[1])) {
6034             // host : port
6035             new HttpHost(parts[1]); // TODO - unused - is this a test bug? else use Assert.assertNotNull
6036         }
6037 
6038         // comment
6039         if (parts.length > 2) {
6040             final StringBuilder buf = new StringBuilder(parts[2]);
6041             for(int i=3; i<parts.length; i++) {
6042                 buf.append(" "); buf.append(parts[i]);
6043             }
6044             Assert.assertTrue(isValidComment(buf.toString()));
6045         }
6046     }
6047 
6048     private boolean isValidComment(final String s) {
6049         final String leafComment = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
6050         final String nestedPrefix = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\(";
6051         final String nestedSuffix = "\\)([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
6052 
6053         if (Pattern.matches(leafComment,s)) {
6054             return true;
6055         }
6056         final Matcher pref = Pattern.compile(nestedPrefix).matcher(s);
6057         final Matcher suff = Pattern.compile(nestedSuffix).matcher(s);
6058         if (!pref.find()) {
6059             return false;
6060         }
6061         if (!suff.find()) {
6062             return false;
6063         }
6064         return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
6065     }
6066 
6067 
6068     /*
6069      * "The received-protocol indicates the protocol version of the message
6070      * received by the server or client along each segment of the request/
6071      * response chain. The received-protocol version is appended to the Via
6072      * field value when the message is forwarded so that information about
6073      * the protocol capabilities of upstream applications remains visible
6074      * to all recipients."
6075      *
6076      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
6077      */
6078     @Test
6079     public void testViaHeaderOnRequestProperlyRecordsClientProtocol()
6080     throws Exception {
6081         request = HttpRequestWrapper.wrap(new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0));
6082         request.removeHeaders("Via");
6083         final Capture<HttpRequestWrapper> cap = new Capture<HttpRequestWrapper>();
6084         EasyMock.expect(
6085                 mockBackend.execute(
6086                         EasyMock.isA(HttpRoute.class),
6087                         EasyMock.capture(cap),
6088                         EasyMock.isA(HttpClientContext.class),
6089                         EasyMock.<HttpExecutionAware>isNull())).andReturn(originResponse);
6090 
6091         replayMocks();
6092         impl.execute(route, request, context, null);
6093         verifyMocks();
6094 
6095         final HttpRequest captured = cap.getValue();
6096         final String via = captured.getFirstHeader("Via").getValue();
6097         final String protocol = via.split("\\s+")[0];
6098         final String[] protoParts = protocol.split("/");
6099         if (protoParts.length > 1) {
6100             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
6101         }
6102         Assert.assertEquals("1.0",protoParts[protoParts.length-1]);
6103     }
6104 
6105     @Test
6106     public void testViaHeaderOnResponseProperlyRecordsOriginProtocol()
6107     throws Exception {
6108 
6109         originResponse = Proxies.enhanceResponse(
6110                 new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content"));
6111 
6112         backendExpectsAnyRequest().andReturn(originResponse);
6113 
6114         replayMocks();
6115         final HttpResponse result = impl.execute(route, request, context, null);
6116         verifyMocks();
6117 
6118         final String via = result.getFirstHeader("Via").getValue();
6119         final String protocol = via.split("\\s+")[0];
6120         final String[] protoParts = protocol.split("/");
6121         Assert.assertTrue(protoParts.length >= 1);
6122         Assert.assertTrue(protoParts.length <= 2);
6123         if (protoParts.length > 1) {
6124             Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
6125         }
6126         Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
6127     }
6128 
6129     /* "A cache MUST NOT delete any Warning header that it received with
6130      * a message."
6131      *
6132      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6133      */
6134     @Test
6135     public void testRetainsWarningHeadersReceivedFromUpstream()
6136         throws Exception {
6137         originResponse.removeHeaders("Warning");
6138         final String warning = "199 fred \"misc\"";
6139         originResponse.addHeader("Warning", warning);
6140         backendExpectsAnyRequest().andReturn(originResponse);
6141 
6142         replayMocks();
6143         final HttpResponse result = impl.execute(route, request, context, null);
6144         verifyMocks();
6145         Assert.assertEquals(warning,
6146                 result.getFirstHeader("Warning").getValue());
6147     }
6148 
6149     /* "However, if a cache successfully validates a cache entry, it
6150      * SHOULD remove any Warning headers previously attached to that
6151      * entry except as specified for specific Warning codes. It MUST
6152      * then add any Warning headers received in the validating response."
6153      *
6154      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6155      */
6156     @Test
6157     public void testUpdatesWarningHeadersOnValidation()
6158         throws Exception {
6159         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(
6160                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
6161         final HttpRequestWrapper req2 = HttpRequestWrapper.wrap(
6162                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
6163 
6164         final Date now = new Date();
6165         final Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
6166         final HttpResponse resp1 = HttpTestUtils.make200Response();
6167         resp1.setHeader("Date", DateUtils.formatDate(twentySecondsAgo));
6168         resp1.setHeader("Cache-Control","public,max-age=5");
6169         resp1.setHeader("ETag", "\"etag1\"");
6170         final String oldWarning = "113 wilma \"stale\"";
6171         resp1.setHeader("Warning", oldWarning);
6172 
6173         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6174         final HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
6175         resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
6176         resp2.setHeader("ETag", "\"etag1\"");
6177         final String newWarning = "113 betty \"stale too\"";
6178         resp2.setHeader("Warning", newWarning);
6179 
6180         backendExpectsAnyRequestAndReturn(resp1);
6181         backendExpectsAnyRequestAndReturn(resp2);
6182 
6183         replayMocks();
6184         impl.execute(route, req1, context, null);
6185         final HttpResponse result = impl.execute(route, req2, context, null);
6186         verifyMocks();
6187 
6188         boolean oldWarningFound = false;
6189         boolean newWarningFound = false;
6190         for(final Header h : result.getHeaders("Warning")) {
6191             for(final String warnValue : h.getValue().split("\\s*,\\s*")) {
6192                 if (oldWarning.equals(warnValue)) {
6193                     oldWarningFound = true;
6194                 } else if (newWarning.equals(warnValue)) {
6195                     newWarningFound = true;
6196                 }
6197             }
6198         }
6199         Assert.assertFalse(oldWarningFound);
6200         Assert.assertTrue(newWarningFound);
6201     }
6202 
6203     /* "If an implementation sends a message with one or more Warning
6204      * headers whose version is HTTP/1.0 or lower, then the sender MUST
6205      * include in each warning-value a warn-date that matches the date
6206      * in the response."
6207      *
6208      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6209      */
6210     @Test
6211     public void testWarnDatesAreAddedToWarningsOnLowerProtocolVersions()
6212         throws Exception {
6213         final String dateHdr = DateUtils.formatDate(new Date());
6214         final String origWarning = "110 fred \"stale\"";
6215         originResponse.setStatusLine(HttpVersion.HTTP_1_0, HttpStatus.SC_OK);
6216         originResponse.addHeader("Warning", origWarning);
6217         originResponse.setHeader("Date", dateHdr);
6218         backendExpectsAnyRequest().andReturn(originResponse);
6219         replayMocks();
6220         final HttpResponse result = impl.execute(route, request, context, null);
6221         verifyMocks();
6222         // note that currently the implementation acts as an HTTP/1.1 proxy,
6223         // which means that all the responses from the caching module should
6224         // be HTTP/1.1, so we won't actually be testing anything here until
6225         // that changes.
6226         if (HttpVersion.HTTP_1_0.greaterEquals(result.getProtocolVersion())) {
6227             Assert.assertEquals(dateHdr, result.getFirstHeader("Date").getValue());
6228             boolean warningFound = false;
6229             final String targetWarning = origWarning + " \"" + dateHdr + "\"";
6230             for(final Header h : result.getHeaders("Warning")) {
6231                 for(final String warning : h.getValue().split("\\s*,\\s*")) {
6232                     if (targetWarning.equals(warning)) {
6233                         warningFound = true;
6234                         break;
6235                     }
6236                 }
6237             }
6238             Assert.assertTrue(warningFound);
6239         }
6240     }
6241 
6242     /* "If an implementation receives a message with a warning-value that
6243      * includes a warn-date, and that warn-date is different from the Date
6244      * value in the response, then that warning-value MUST be deleted from
6245      * the message before storing, forwarding, or using it. (This prevents
6246      * bad consequences of naive caching of Warning header fields.) If all
6247      * of the warning-values are deleted for this reason, the Warning
6248      * header MUST be deleted as well."
6249      *
6250      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
6251      */
6252     @Test
6253     public void testStripsBadlyDatedWarningsFromForwardedResponses()
6254         throws Exception {
6255         final Date now = new Date();
6256         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6257         originResponse.setHeader("Date", DateUtils.formatDate(now));
6258         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
6259                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6260         originResponse.setHeader("Cache-Control","no-cache,no-store");
6261         backendExpectsAnyRequest().andReturn(originResponse);
6262 
6263         replayMocks();
6264         final HttpResponse result = impl.execute(route, request, context, null);
6265         verifyMocks();
6266 
6267         for(final Header h : result.getHeaders("Warning")) {
6268             Assert.assertFalse(h.getValue().contains("wilma"));
6269         }
6270     }
6271 
6272     @Test
6273     public void testStripsBadlyDatedWarningsFromStoredResponses()
6274         throws Exception {
6275         final Date now = new Date();
6276         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6277         originResponse.setHeader("Date", DateUtils.formatDate(now));
6278         originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
6279                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6280         originResponse.setHeader("Cache-Control","public,max-age=3600");
6281         backendExpectsAnyRequest().andReturn(originResponse);
6282 
6283         replayMocks();
6284         final HttpResponse result = impl.execute(route, request, context, null);
6285         verifyMocks();
6286 
6287         for(final Header h : result.getHeaders("Warning")) {
6288             Assert.assertFalse(h.getValue().contains("wilma"));
6289         }
6290     }
6291 
6292     @Test
6293     public void testRemovesWarningHeaderIfAllWarnValuesAreBadlyDated()
6294     throws Exception {
6295         final Date now = new Date();
6296         final Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
6297         originResponse.setHeader("Date", DateUtils.formatDate(now));
6298         originResponse.addHeader("Warning", "110 wilma \"stale\" \""
6299                 + DateUtils.formatDate(tenSecondsAgo) + "\"");
6300         backendExpectsAnyRequest().andReturn(originResponse);
6301 
6302         replayMocks();
6303         final HttpResponse result = impl.execute(route, request, context, null);
6304         verifyMocks();
6305 
6306         final Header[] warningHeaders = result.getHeaders("Warning");
6307         Assert.assertTrue(warningHeaders == null || warningHeaders.length == 0);
6308     }
6309 
6310 }