View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  
30  import static org.junit.jupiter.api.Assertions.assertEquals;
31  import static org.junit.jupiter.api.Assertions.assertFalse;
32  import static org.junit.jupiter.api.Assertions.assertNotEquals;
33  import static org.junit.jupiter.api.Assertions.assertNotNull;
34  import static org.junit.jupiter.api.Assertions.assertNull;
35  import static org.junit.jupiter.api.Assertions.assertTrue;
36  
37  import java.io.IOException;
38  import java.time.Instant;
39  import java.time.temporal.ChronoUnit;
40  import java.util.Arrays;
41  import java.util.Iterator;
42  import java.util.List;
43  
44  import org.apache.hc.client5.http.HttpRoute;
45  import org.apache.hc.client5.http.auth.StandardAuthScheme;
46  import org.apache.hc.client5.http.classic.ExecChain;
47  import org.apache.hc.client5.http.classic.ExecRuntime;
48  import org.apache.hc.client5.http.classic.methods.HttpGet;
49  import org.apache.hc.client5.http.classic.methods.HttpPost;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.utils.DateUtils;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.Header;
55  import org.apache.hc.core5.http.HeaderElement;
56  import org.apache.hc.core5.http.HttpEntity;
57  import org.apache.hc.core5.http.HttpException;
58  import org.apache.hc.core5.http.HttpHeaders;
59  import org.apache.hc.core5.http.HttpHost;
60  import org.apache.hc.core5.http.HttpStatus;
61  import org.apache.hc.core5.http.HttpVersion;
62  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
63  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
64  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
65  import org.apache.hc.core5.http.message.MessageSupport;
66  import org.junit.jupiter.api.BeforeEach;
67  import org.junit.jupiter.api.Test;
68  import org.mockito.ArgumentCaptor;
69  import org.mockito.Mock;
70  import org.mockito.Mockito;
71  import org.mockito.MockitoAnnotations;
72  
73  /*
74   * This test class captures functionality required to achieve unconditional
75   * compliance with the HTTP/1.1 spec, i.e. all the SHOULD, SHOULD NOT,
76   * RECOMMENDED, and NOT RECOMMENDED behaviors.
77   */
78  public class TestProtocolRecommendations {
79  
80      static final int MAX_BYTES = 1024;
81      static final int MAX_ENTRIES = 100;
82      static final int ENTITY_LENGTH = 128;
83  
84      HttpHost host;
85      HttpRoute route;
86      HttpEntity body;
87      HttpClientContext context;
88      @Mock
89      ExecChain mockExecChain;
90      @Mock
91      ExecRuntime mockExecRuntime;
92      ClassicHttpRequest request;
93      ClassicHttpResponse originResponse;
94      CacheConfig config;
95      CachingExec impl;
96      HttpCache cache;
97      Instant now;
98      Instant tenSecondsAgo;
99      Instant twoMinutesAgo;
100 
101     @BeforeEach
102     public void setUp() throws Exception {
103         MockitoAnnotations.openMocks(this);
104         host = new HttpHost("foo.example.com", 80);
105 
106         route = new HttpRoute(host);
107 
108         body = HttpTestUtils.makeBody(ENTITY_LENGTH);
109 
110         request = new BasicClassicHttpRequest("GET", "/foo");
111 
112         context = HttpClientContext.create();
113 
114         originResponse = HttpTestUtils.make200Response();
115 
116         config = CacheConfig.custom()
117                 .setMaxCacheEntries(MAX_ENTRIES)
118                 .setMaxObjectSize(MAX_BYTES)
119                 .build();
120 
121         cache = new BasicHttpCache(config);
122         impl = new CachingExec(cache, null, config);
123 
124         now = Instant.now();
125         tenSecondsAgo = now.minus(10, ChronoUnit.SECONDS);
126         twoMinutesAgo = now.minus(1, ChronoUnit.MINUTES);
127 
128         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
129     }
130 
131     public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
132         return impl.execute(
133                 ClassicRequestBuilder.copy(request).build(),
134                 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
135                 mockExecChain);
136     }
137 
138     /* "identity: The default (identity) encoding; the use of no
139      * transformation whatsoever. This content-coding is used only in the
140      * Accept-Encoding header, and SHOULD NOT be used in the
141      * Content-Encoding header."
142      *
143      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5
144      */
145     @Test
146     public void testIdentityCodingIsNotUsedInContentEncodingHeader() throws Exception {
147         originResponse.setHeader("Content-Encoding", "identity");
148         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
149 
150         final ClassicHttpResponse result = execute(request);
151 
152         boolean foundIdentity = false;
153         final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_ENCODING);
154         while (it.hasNext()) {
155             final HeaderElement elt = it.next();
156             if ("identity".equalsIgnoreCase(elt.getName())) {
157                 foundIdentity = true;
158             }
159         }
160         assertFalse(foundIdentity);
161     }
162 
163     /*
164      * "304 Not Modified. ... If the conditional GET used a strong cache
165      * validator (see section 13.3.3), the response SHOULD NOT include
166      * other entity-headers."
167      *
168      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
169      */
170     private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
171             final String headerName, final String headerValue, final String validatorHeader,
172             final String validator, final String conditionalHeader) throws Exception {
173         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
174         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
175         resp1.setHeader("Cache-Control","max-age=3600");
176         resp1.setHeader(validatorHeader, validator);
177         resp1.setHeader(headerName, headerValue);
178 
179         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
180 
181         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
182         req2.setHeader(conditionalHeader, validator);
183 
184         execute(req1);
185         final ClassicHttpResponse result = execute(req2);
186 
187 
188         if (HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
189             assertNull(result.getFirstHeader(headerName));
190         }
191     }
192 
193     private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
194             final String headerName, final String headerValue) throws Exception {
195         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
196                 headerValue, "ETag", "\"etag\"", "If-None-Match");
197     }
198 
199     private void cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
200             final String headerName, final String headerValue) throws Exception {
201         cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
202                 headerValue, "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo),
203                 "If-Modified-Since");
204     }
205 
206     @Test
207     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainAllow() throws Exception {
208         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
209                 "Allow", "GET,HEAD");
210     }
211 
212     @Test
213     public void cacheGenerated304ForStrongDateValidatorShouldNotContainAllow() throws Exception {
214         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
215                 "Allow", "GET,HEAD");
216     }
217 
218     @Test
219     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentEncoding() throws Exception {
220         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
221                 "Content-Encoding", "gzip");
222     }
223 
224     @Test
225     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentEncoding() throws Exception {
226         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
227                 "Content-Encoding", "gzip");
228     }
229 
230     @Test
231     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentLanguage() throws Exception {
232         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
233                 "Content-Language", "en");
234     }
235 
236     @Test
237     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLanguage() throws Exception {
238         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
239                 "Content-Language", "en");
240     }
241 
242     @Test
243     public void cacheGenerated304ForStrongValidatorShouldNotContainContentLength() throws Exception {
244         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
245                 "Content-Length", "128");
246     }
247 
248     @Test
249     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLength() throws Exception {
250         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
251                 "Content-Length", "128");
252     }
253 
254     @Test
255     public void cacheGenerated304ForStrongValidatorShouldNotContainContentMD5() throws Exception {
256         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
257                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
258     }
259 
260     @Test
261     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5() throws Exception {
262         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
263                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
264     }
265 
266     private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
267             final String validatorHeader, final String validator, final String conditionalHeader) throws Exception {
268         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
269         req1.setHeader("Range","bytes=0-127");
270         final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
271         resp1.setHeader("Cache-Control","max-age=3600");
272         resp1.setHeader(validatorHeader, validator);
273         resp1.setHeader("Content-Range", "bytes 0-127/256");
274 
275         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
276 
277         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
278         req2.setHeader("If-Range", validator);
279         req2.setHeader("Range","bytes=0-127");
280         req2.setHeader(conditionalHeader, validator);
281 
282         try (final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified")) {
283             resp2.setHeader("Date", DateUtils.formatStandardDate(now));
284             resp2.setHeader(validatorHeader, validator);
285         }
286 
287         // cache module does not currently deal with byte ranges, but we want
288         // this test to work even if it does some day
289 
290         execute(req1);
291         final ClassicHttpResponse result = execute(req2);
292 
293         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
294         Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
295 
296         final List<ClassicHttpRequest> allRequests = reqCapture.getAllValues();
297         if (allRequests.isEmpty() && HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
298             // cache generated a 304
299             assertNull(result.getFirstHeader("Content-Range"));
300         }
301     }
302 
303     @Test
304     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange() throws Exception {
305         cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
306                 "ETag", "\"etag\"", "If-None-Match");
307     }
308 
309     @Test
310     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange() throws Exception {
311         cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
312                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo), "If-Modified-Since");
313     }
314 
315     @Test
316     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType() throws Exception {
317         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
318                 "Content-Type", "text/html");
319     }
320 
321     @Test
322     public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentType() throws Exception {
323         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
324                 "Content-Type", "text/html");
325     }
326 
327     @Test
328     public void cacheGenerated304ForStrongEtagValidatorShouldNotContainLastModified() throws Exception {
329         cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
330                 "Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo));
331     }
332 
333     @Test
334     public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified() throws Exception {
335         cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
336                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
337     }
338 
339     private void shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
340             final String entityHeader, final String entityHeaderValue) throws Exception {
341         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
342         req.setHeader("If-None-Match", "\"etag\"");
343 
344         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
345         resp.setHeader("Date", DateUtils.formatStandardDate(now));
346         resp.setHeader("Etag", "\"etag\"");
347         resp.setHeader(entityHeader, entityHeaderValue);
348 
349         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
350 
351         final ClassicHttpResponse result = execute(req);
352 
353         assertNull(result.getFirstHeader(entityHeader));
354     }
355 
356     @Test
357     public void shouldStripAllowFromOrigin304ResponseToStrongValidation() throws Exception {
358         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
359                 "Allow", "GET,HEAD");
360     }
361 
362     @Test
363     public void shouldStripContentEncodingFromOrigin304ResponseToStrongValidation() throws Exception {
364         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
365                 "Content-Encoding", "gzip");
366     }
367 
368     @Test
369     public void shouldStripContentLanguageFromOrigin304ResponseToStrongValidation() throws Exception {
370         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
371                 "Content-Language", "en");
372     }
373 
374     @Test
375     public void shouldStripContentLengthFromOrigin304ResponseToStrongValidation() throws Exception {
376         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
377                 "Content-Length", "128");
378     }
379 
380     @Test
381     public void shouldStripContentMD5FromOrigin304ResponseToStrongValidation() throws Exception {
382         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
383                 "Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
384     }
385 
386     @Test
387     public void shouldStripContentTypeFromOrigin304ResponseToStrongValidation() throws Exception {
388         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
389                 "Content-Type", "text/html;charset=utf-8");
390     }
391 
392     @Test
393     public void shouldStripContentRangeFromOrigin304ResponseToStringValidation() throws Exception {
394         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
395         req.setHeader("If-Range","\"etag\"");
396         req.setHeader("Range","bytes=0-127");
397 
398         final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
399         resp.setHeader("Date", DateUtils.formatStandardDate(now));
400         resp.setHeader("ETag", "\"etag\"");
401         resp.setHeader("Content-Range", "bytes 0-127/256");
402 
403         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp);
404 
405         final ClassicHttpResponse result = execute(req);
406 
407         assertNull(result.getFirstHeader("Content-Range"));
408     }
409 
410     @Test
411     public void shouldStripLastModifiedFromOrigin304ResponseToStrongValidation() throws Exception {
412         shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
413                 "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
414     }
415 
416     /*
417      * "For this reason, a cache SHOULD NOT return a stale response if the
418      * client explicitly requests a first-hand or fresh one, unless it is
419      * impossible to comply for technical or policy reasons."
420      */
421     private ClassicHttpRequest requestToPopulateStaleCacheEntry() throws Exception {
422         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
423         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
424         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
425         resp1.setHeader("Cache-Control","public,max-age=5");
426         resp1.setHeader("Etag","\"etag\"");
427 
428         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
429         return req1;
430     }
431 
432     private void testDoesNotReturnStaleResponseOnError(final ClassicHttpRequest req2) throws Exception {
433         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
434 
435         execute(req1);
436 
437         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
438 
439         ClassicHttpResponse result = null;
440         try {
441             result = execute(req2);
442         } catch (final IOException acceptable) {
443         }
444 
445         if (result != null) {
446             assertNotEquals(HttpStatus.SC_OK, result.getCode());
447         }
448     }
449 
450     @Test
451     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithCacheControl() throws Exception {
452         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
453         req.setHeader("Cache-Control","no-cache");
454         testDoesNotReturnStaleResponseOnError(req);
455     }
456 
457     @Test
458     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithPragma() throws Exception {
459         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
460         req.setHeader("Pragma","no-cache");
461         testDoesNotReturnStaleResponseOnError(req);
462     }
463 
464     @Test
465     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxAge() throws Exception {
466         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
467         req.setHeader("Cache-Control","max-age=0");
468         testDoesNotReturnStaleResponseOnError(req);
469     }
470 
471     @Test
472     public void testDoesNotReturnStaleResponseIfClientExplicitlySpecifiesLargerMaxAge() throws Exception {
473         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
474         req.setHeader("Cache-Control","max-age=20");
475         testDoesNotReturnStaleResponseOnError(req);
476     }
477 
478 
479     @Test
480     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMinFresh() throws Exception {
481         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
482         req.setHeader("Cache-Control","min-fresh=2");
483 
484         testDoesNotReturnStaleResponseOnError(req);
485     }
486 
487     @Test
488     public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxStale() throws Exception {
489         final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
490         req.setHeader("Cache-Control","max-stale=2");
491 
492         testDoesNotReturnStaleResponseOnError(req);
493     }
494 
495     @Test
496     public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxStale() throws Exception {
497         final ClassicHttpRequest req1 = requestToPopulateStaleCacheEntry();
498         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
499         req2.setHeader("Cache-Control","max-stale=20");
500 
501         execute(req1);
502 
503         final ClassicHttpResponse result = execute(req2);
504 
505         assertEquals(HttpStatus.SC_OK, result.getCode());
506         assertNotNull(result.getFirstHeader("Warning"));
507 
508         Mockito.verify(mockExecChain, Mockito.atMost(1)).proceed(Mockito.any(), Mockito.any());
509     }
510 
511     /*
512      * "A correct cache MUST respond to a request with the most up-to-date
513      * response held by the cache that is appropriate to the request
514      * (see sections 13.2.5, 13.2.6, and 13.12) which meets one of the
515      * following conditions:
516      *
517      * 1. It has been checked for equivalence with what the origin server
518      * would have returned by revalidating the response with the
519      * origin server (section 13.3);
520      *
521      * 2. It is "fresh enough" (see section 13.2). In the default case,
522      * this means it meets the least restrictive freshness requirement
523      * of the client, origin server, and cache (see section 14.9); if
524      * the origin server so specifies, it is the freshness requirement
525      * of the origin server alone.
526      *
527      * If a stored response is not "fresh enough" by the most
528      * restrictive freshness requirement of both the client and the
529      * origin server, in carefully considered circumstances the cache
530      * MAY still return the response with the appropriate Warning
531      * header (see section 13.1.5 and 14.46), unless such a response
532      * is prohibited (e.g., by a "no-store" cache-directive, or by a
533      * "no-cache" cache-request-directive; see section 14.9).
534      *
535      * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect),
536      * or error (4xx or 5xx) response message.
537      *
538      * If the cache can not communicate with the origin server, then a
539      * correct cache SHOULD respond as above if the response can be
540      * correctly served from the cache..."
541      *
542      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
543      */
544     @Test
545     public void testReturnsCachedResponsesAppropriatelyWhenNoOriginCommunication() throws Exception {
546         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
547         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
548         resp1.setHeader("Cache-Control", "public, max-age=5");
549         resp1.setHeader("ETag","\"etag\"");
550         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
551 
552         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
553 
554         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
555 
556         execute(req1);
557 
558         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
559 
560         final ClassicHttpResponse result = execute(req2);
561 
562         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
563 
564         assertEquals(HttpStatus.SC_OK, result.getCode());
565         boolean warning111Found = false;
566         for(final Header h : result.getHeaders("Warning")) {
567             for(final WarningValue wv : WarningValue.getWarningValues(h)) {
568                 if (wv.getWarnCode() == 111) {
569                     warning111Found = true;
570                     break;
571                 }
572             }
573         }
574         assertTrue(warning111Found);
575     }
576 
577     /*
578      * "If a cache receives a response (either an entire response, or a
579      * 304 (Not Modified) response) that it would normally forward to the
580      * requesting client, and the received response is no longer fresh,
581      * the cache SHOULD forward it to the requesting client without adding
582      * a new Warning (but without removing any existing Warning headers).
583      * A cache SHOULD NOT attempt to revalidate a response simply because
584      * that response became stale in transit; this might lead to an
585      * infinite loop."
586      *
587      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
588      */
589     @Test
590     public void testDoesNotAddNewWarningHeaderIfResponseArrivesStale() throws Exception {
591         originResponse.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
592         originResponse.setHeader("Cache-Control","public, max-age=5");
593         originResponse.setHeader("ETag","\"etag\"");
594 
595         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
596 
597         final ClassicHttpResponse result = execute(request);
598 
599         assertNull(result.getFirstHeader("Warning"));
600     }
601 
602     @Test
603     public void testForwardsExistingWarningHeadersOnResponseThatArrivesStale() throws Exception {
604         originResponse.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
605         originResponse.setHeader("Cache-Control","public, max-age=5");
606         originResponse.setHeader("ETag","\"etag\"");
607         originResponse.addHeader("Age","10");
608         final String warning = "110 fred \"Response is stale\"";
609         originResponse.addHeader("Warning",warning);
610 
611         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
612 
613         final ClassicHttpResponse result = execute(request);
614 
615         assertEquals(warning, result.getFirstHeader("Warning").getValue());
616     }
617 
618     /*
619      * "A transparent proxy SHOULD NOT modify an end-to-end header unless
620      * the definition of that header requires or specifically allows that."
621      *
622      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
623      */
624     private void testDoesNotModifyHeaderOnResponses(final String headerName) throws Exception {
625         final String headerValue = HttpTestUtils
626             .getCanonicalHeaderValue(originResponse, headerName);
627         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
628 
629         final ClassicHttpResponse result = execute(request);
630 
631         assertEquals(headerValue, result.getFirstHeader(headerName).getValue());
632     }
633 
634     private void testDoesNotModifyHeaderOnRequests(final String headerName) throws Exception {
635         final String headerValue = HttpTestUtils.getCanonicalHeaderValue(request, headerName);
636         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
637 
638         execute(request);
639 
640         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
641         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
642 
643         assertEquals(headerValue, HttpTestUtils.getCanonicalHeaderValue(reqCapture.getValue(), headerName));
644     }
645 
646     @Test
647     public void testDoesNotModifyAcceptRangesOnResponses() throws Exception {
648         final String headerName = "Accept-Ranges";
649         originResponse.setHeader(headerName,"bytes");
650         testDoesNotModifyHeaderOnResponses(headerName);
651     }
652 
653     @Test
654     public void testDoesNotModifyAuthorizationOnRequests() throws Exception {
655         request.setHeader("Authorization", StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=");
656         testDoesNotModifyHeaderOnRequests("Authorization");
657     }
658 
659     @Test
660     public void testDoesNotModifyContentLengthOnRequests() throws Exception {
661         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
662         post.setEntity(HttpTestUtils.makeBody(128));
663         post.setHeader("Content-Length","128");
664         request = post;
665         testDoesNotModifyHeaderOnRequests("Content-Length");
666     }
667 
668     @Test
669     public void testDoesNotModifyContentLengthOnResponses() throws Exception {
670         originResponse.setEntity(HttpTestUtils.makeBody(128));
671         originResponse.setHeader("Content-Length","128");
672         testDoesNotModifyHeaderOnResponses("Content-Length");
673     }
674 
675     @Test
676     public void testDoesNotModifyContentMD5OnRequests() throws Exception {
677         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
678         post.setEntity(HttpTestUtils.makeBody(128));
679         post.setHeader("Content-Length","128");
680         post.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
681         request = post;
682         testDoesNotModifyHeaderOnRequests("Content-MD5");
683     }
684 
685     @Test
686     public void testDoesNotModifyContentMD5OnResponses() throws Exception {
687         originResponse.setEntity(HttpTestUtils.makeBody(128));
688         originResponse.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
689         testDoesNotModifyHeaderOnResponses("Content-MD5");
690     }
691 
692     @Test
693     public void testDoesNotModifyContentRangeOnRequests() throws Exception {
694         final ClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
695         put.setEntity(HttpTestUtils.makeBody(128));
696         put.setHeader("Content-Length","128");
697         put.setHeader("Content-Range","bytes 0-127/256");
698         request = put;
699         testDoesNotModifyHeaderOnRequests("Content-Range");
700     }
701 
702     @Test
703     public void testDoesNotModifyContentRangeOnResponses() throws Exception {
704         request.setHeader("Range","bytes=0-128");
705         originResponse.setCode(HttpStatus.SC_PARTIAL_CONTENT);
706         originResponse.setReasonPhrase("Partial Content");
707         originResponse.setEntity(HttpTestUtils.makeBody(128));
708         originResponse.setHeader("Content-Range","bytes 0-127/256");
709         testDoesNotModifyHeaderOnResponses("Content-Range");
710     }
711 
712     @Test
713     public void testDoesNotModifyContentTypeOnRequests() throws Exception {
714         final ClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
715         post.setEntity(HttpTestUtils.makeBody(128));
716         post.setHeader("Content-Length","128");
717         post.setHeader("Content-Type","application/octet-stream");
718         request = post;
719         testDoesNotModifyHeaderOnRequests("Content-Type");
720     }
721 
722     @Test
723     public void testDoesNotModifyContentTypeOnResponses() throws Exception {
724         originResponse.setHeader("Content-Type","application/octet-stream");
725         testDoesNotModifyHeaderOnResponses("Content-Type");
726     }
727 
728     @Test
729     public void testDoesNotModifyDateOnRequests() throws Exception {
730         request.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
731         testDoesNotModifyHeaderOnRequests("Date");
732     }
733 
734     @Test
735     public void testDoesNotModifyDateOnResponses() throws Exception {
736         originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
737         testDoesNotModifyHeaderOnResponses("Date");
738     }
739 
740     @Test
741     public void testDoesNotModifyETagOnResponses() throws Exception {
742         originResponse.setHeader("ETag", "\"random-etag\"");
743         testDoesNotModifyHeaderOnResponses("ETag");
744     }
745 
746     @Test
747     public void testDoesNotModifyExpiresOnResponses() throws Exception {
748         originResponse.setHeader("Expires", DateUtils.formatStandardDate(Instant.now()));
749         testDoesNotModifyHeaderOnResponses("Expires");
750     }
751 
752     @Test
753     public void testDoesNotModifyFromOnRequests() throws Exception {
754         request.setHeader("From", "foo@example.com");
755         testDoesNotModifyHeaderOnRequests("From");
756     }
757 
758     @Test
759     public void testDoesNotModifyIfMatchOnRequests() throws Exception {
760         request = new BasicClassicHttpRequest("DELETE", "/");
761         request.setHeader("If-Match", "\"etag\"");
762         testDoesNotModifyHeaderOnRequests("If-Match");
763     }
764 
765     @Test
766     public void testDoesNotModifyIfModifiedSinceOnRequests() throws Exception {
767         request.setHeader("If-Modified-Since", DateUtils.formatStandardDate(Instant.now()));
768         testDoesNotModifyHeaderOnRequests("If-Modified-Since");
769     }
770 
771     @Test
772     public void testDoesNotModifyIfNoneMatchOnRequests() throws Exception {
773         request.setHeader("If-None-Match", "\"etag\"");
774         testDoesNotModifyHeaderOnRequests("If-None-Match");
775     }
776 
777     @Test
778     public void testDoesNotModifyIfRangeOnRequests() throws Exception {
779         request.setHeader("Range","bytes=0-128");
780         request.setHeader("If-Range", "\"etag\"");
781         testDoesNotModifyHeaderOnRequests("If-Range");
782     }
783 
784     @Test
785     public void testDoesNotModifyIfUnmodifiedSinceOnRequests() throws Exception {
786         request = new BasicClassicHttpRequest("DELETE", "/");
787         request.setHeader("If-Unmodified-Since", DateUtils.formatStandardDate(Instant.now()));
788         testDoesNotModifyHeaderOnRequests("If-Unmodified-Since");
789     }
790 
791     @Test
792     public void testDoesNotModifyLastModifiedOnResponses() throws Exception {
793         originResponse.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now()));
794         testDoesNotModifyHeaderOnResponses("Last-Modified");
795     }
796 
797     @Test
798     public void testDoesNotModifyLocationOnResponses() throws Exception {
799         originResponse.setCode(HttpStatus.SC_TEMPORARY_REDIRECT);
800         originResponse.setReasonPhrase("Temporary Redirect");
801         originResponse.setHeader("Location", "http://foo.example.com/bar");
802         testDoesNotModifyHeaderOnResponses("Location");
803     }
804 
805     @Test
806     public void testDoesNotModifyRangeOnRequests() throws Exception {
807         request.setHeader("Range", "bytes=0-128");
808         testDoesNotModifyHeaderOnRequests("Range");
809     }
810 
811     @Test
812     public void testDoesNotModifyRefererOnRequests() throws Exception {
813         request.setHeader("Referer", "http://foo.example.com/bar");
814         testDoesNotModifyHeaderOnRequests("Referer");
815     }
816 
817     @Test
818     public void testDoesNotModifyRetryAfterOnResponses() throws Exception {
819         originResponse.setCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
820         originResponse.setReasonPhrase("Service Unavailable");
821         originResponse.setHeader("Retry-After", "120");
822         testDoesNotModifyHeaderOnResponses("Retry-After");
823     }
824 
825     @Test
826     public void testDoesNotModifyServerOnResponses() throws Exception {
827         originResponse.setHeader("Server", "SomeServer/1.0");
828         testDoesNotModifyHeaderOnResponses("Server");
829     }
830 
831     @Test
832     public void testDoesNotModifyUserAgentOnRequests() throws Exception {
833         request.setHeader("User-Agent", "MyClient/1.0");
834         testDoesNotModifyHeaderOnRequests("User-Agent");
835     }
836 
837     @Test
838     public void testDoesNotModifyVaryOnResponses() throws Exception {
839         request.setHeader("Accept-Encoding","identity");
840         originResponse.setHeader("Vary", "Accept-Encoding");
841         testDoesNotModifyHeaderOnResponses("Vary");
842     }
843 
844     @Test
845     public void testDoesNotModifyExtensionHeaderOnRequests() throws Exception {
846         request.setHeader("X-Extension","x-value");
847         testDoesNotModifyHeaderOnRequests("X-Extension");
848     }
849 
850     @Test
851     public void testDoesNotModifyExtensionHeaderOnResponses() throws Exception {
852         originResponse.setHeader("X-Extension", "x-value");
853         testDoesNotModifyHeaderOnResponses("X-Extension");
854     }
855 
856 
857     /*
858      * "[HTTP/1.1 clients], If only a Last-Modified value has been provided
859      * by the origin server, SHOULD use that value in non-subrange cache-
860      * conditional requests (using If-Modified-Since)."
861      *
862      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
863      */
864     @Test
865     public void testUsesLastModifiedDateForCacheConditionalRequests() throws Exception {
866         final Instant twentySecondsAgo = now.plusSeconds(20);
867         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
868 
869         final ClassicHttpRequest req1 =
870             new BasicClassicHttpRequest("GET", "/");
871         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
872         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
873         resp1.setHeader("Last-Modified", lmDate);
874         resp1.setHeader("Cache-Control","max-age=5");
875 
876         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
877         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
878 
879         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
880 
881         execute(req1);
882 
883         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
884 
885         execute(req2);
886 
887         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
888         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
889 
890         final ClassicHttpRequest captured = reqCapture.getValue();
891         final Header ifModifiedSince = captured.getFirstHeader("If-Modified-Since");
892         assertEquals(lmDate, ifModifiedSince.getValue());
893     }
894 
895     /*
896      * "[HTTP/1.1 clients], if both an entity tag and a Last-Modified value
897      * have been provided by the origin server, SHOULD use both validators
898      * in cache-conditional requests. This allows both HTTP/1.0 and
899      * HTTP/1.1 caches to respond appropriately."
900      *
901      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
902      */
903     @Test
904     public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() throws Exception {
905         final Instant twentySecondsAgo = now.plusSeconds(20);
906         final String lmDate = DateUtils.formatStandardDate(twentySecondsAgo);
907         final String etag = "\"etag\"";
908 
909         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
910         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
911         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
912         resp1.setHeader("Last-Modified", lmDate);
913         resp1.setHeader("Cache-Control","max-age=5");
914         resp1.setHeader("ETag", etag);
915 
916         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
917         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
918 
919         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
920 
921         execute(req1);
922 
923         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
924 
925         execute(req2);
926 
927         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
928         Mockito.verify(mockExecChain, Mockito.times(2)).proceed(reqCapture.capture(), Mockito.any());
929 
930         final ClassicHttpRequest captured = reqCapture.getValue();
931         final Header ifModifiedSince = captured.getFirstHeader("If-Modified-Since");
932         assertEquals(lmDate, ifModifiedSince.getValue());
933         final Header ifNoneMatch = captured.getFirstHeader("If-None-Match");
934         assertEquals(etag, ifNoneMatch.getValue());
935     }
936 
937     /*
938      * "If an origin server wishes to force a semantically transparent cache
939      * to validate every request, it MAY assign an explicit expiration time
940      * in the past. This means that the response is always stale, and so the
941      * cache SHOULD validate it before using it for subsequent requests."
942      *
943      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.1
944      */
945     @Test
946     public void testRevalidatesCachedResponseWithExpirationInThePast() throws Exception {
947         final Instant oneSecondAgo = now.minusSeconds(1);
948         final Instant oneSecondFromNow = now.plusSeconds(1);
949         final Instant twoSecondsFromNow = now.plusSeconds(2);
950         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
951         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
952         resp1.setHeader("ETag","\"etag\"");
953         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
954         resp1.setHeader("Expires",DateUtils.formatStandardDate(oneSecondAgo));
955         resp1.setHeader("Cache-Control", "public");
956 
957         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
958 
959         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
960         final ClassicHttpRequest revalidate = new BasicClassicHttpRequest("GET", "/");
961         revalidate.setHeader("If-None-Match","\"etag\"");
962 
963         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
964         resp2.setHeader("Date", DateUtils.formatStandardDate(twoSecondsFromNow));
965         resp2.setHeader("Expires", DateUtils.formatStandardDate(oneSecondFromNow));
966         resp2.setHeader("ETag","\"etag\"");
967 
968         Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(revalidate), Mockito.any())).thenReturn(resp2);
969 
970         execute(req1);
971         final ClassicHttpResponse result = execute(req2);
972 
973         assertEquals(HttpStatus.SC_OK, result.getCode());
974     }
975 
976     /* "When a client tries to revalidate a cache entry, and the response
977      * it receives contains a Date header that appears to be older than the
978      * one for the existing entry, then the client SHOULD repeat the
979      * request unconditionally, and include
980      *     Cache-Control: max-age=0
981      * to force any intermediate caches to validate their copies directly
982      * with the origin server, or
983      *     Cache-Control: no-cache
984      * to force any intermediate caches to obtain a new copy from the
985      * origin server."
986      *
987      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.6
988      */
989     @Test
990     public void testRetriesValidationThatResultsInAnOlderDated304Response() throws Exception {
991         final Instant elevenSecondsAgo = now.minusSeconds(11);
992         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
993         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
994         resp1.setHeader("ETag","\"etag\"");
995         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
996         resp1.setHeader("Cache-Control","max-age=5");
997 
998         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
999         final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
1000         resp2.setHeader("ETag","\"etag\"");
1001         resp2.setHeader("Date", DateUtils.formatStandardDate(elevenSecondsAgo));
1002 
1003         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1004 
1005         execute(req1);
1006 
1007         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1008 
1009         execute(req2);
1010 
1011         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1012         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1013 
1014         final ClassicHttpRequest captured = reqCapture.getValue();
1015         boolean hasMaxAge0 = false;
1016         boolean hasNoCache = false;
1017         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
1018         while (it.hasNext()) {
1019             final HeaderElement elt = it.next();
1020             if ("max-age".equals(elt.getName())) {
1021                 try {
1022                     final int maxage = Integer.parseInt(elt.getValue());
1023                     if (maxage == 0) {
1024                         hasMaxAge0 = true;
1025                     }
1026                 } catch (final NumberFormatException nfe) {
1027                     // nop
1028                 }
1029             } else if ("no-cache".equals(elt.getName())) {
1030                 hasNoCache = true;
1031             }
1032         }
1033         assertTrue(hasMaxAge0 || hasNoCache);
1034         assertNull(captured.getFirstHeader("If-None-Match"));
1035         assertNull(captured.getFirstHeader("If-Modified-Since"));
1036         assertNull(captured.getFirstHeader("If-Range"));
1037         assertNull(captured.getFirstHeader("If-Match"));
1038         assertNull(captured.getFirstHeader("If-Unmodified-Since"));
1039     }
1040 
1041     /* "If an entity tag was assigned to a cached representation, the
1042      * forwarded request SHOULD be conditional and include the entity
1043      * tags in an If-None-Match header field from all its cache entries
1044      * for the resource."
1045      *
1046      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1047      */
1048     @Test
1049     public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
1050         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
1051         req1.setHeader("User-Agent","agent1");
1052         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1053         resp1.setHeader("Cache-Control","max-age=3600");
1054         resp1.setHeader("Vary","User-Agent");
1055         resp1.setHeader("Etag","\"etag1\"");
1056 
1057         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET","/");
1058         req2.setHeader("User-Agent","agent2");
1059         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1060         resp2.setHeader("Cache-Control","max-age=3600");
1061         resp2.setHeader("Vary","User-Agent");
1062         resp2.setHeader("Etag","\"etag2\"");
1063 
1064         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET","/");
1065         req3.setHeader("User-Agent","agent3");
1066         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1067 
1068         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1069 
1070         execute(req1);
1071 
1072         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1073 
1074         execute(req2);
1075 
1076         Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp3);
1077 
1078         execute(req3);
1079 
1080         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1081         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1082 
1083         final ClassicHttpRequest captured = reqCapture.getValue();
1084         boolean foundEtag1 = false;
1085         boolean foundEtag2 = false;
1086         for(final Header h : captured.getHeaders("If-None-Match")) {
1087             for(final String etag : h.getValue().split(",")) {
1088                 if ("\"etag1\"".equals(etag.trim())) {
1089                     foundEtag1 = true;
1090                 }
1091                 if ("\"etag2\"".equals(etag.trim())) {
1092                     foundEtag2 = true;
1093                 }
1094             }
1095         }
1096         assertTrue(foundEtag1 && foundEtag2);
1097     }
1098 
1099     /* "If the entity-tag of the new response matches that of an existing
1100      * entry, the new response SHOULD be used to processChallenge the header fields
1101      * of the existing entry, and the result MUST be returned to the
1102      * client."
1103      *
1104      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1105      */
1106     @Test
1107     public void testResponseToExistingVariantsUpdatesEntry() throws Exception {
1108 
1109         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1110         req1.setHeader("User-Agent", "agent1");
1111 
1112         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1113         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1114         resp1.setHeader("Vary", "User-Agent");
1115         resp1.setHeader("Cache-Control", "max-age=3600");
1116         resp1.setHeader("ETag", "\"etag1\"");
1117 
1118         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1119         req2.setHeader("User-Agent", "agent2");
1120 
1121         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1122         resp2.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1123         resp2.setHeader("Vary", "User-Agent");
1124         resp2.setHeader("Cache-Control", "max-age=3600");
1125         resp2.setHeader("ETag", "\"etag2\"");
1126 
1127         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1128         req3.setHeader("User-Agent", "agent3");
1129 
1130         final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1131         resp3.setHeader("Date", DateUtils.formatStandardDate(now));
1132         resp3.setHeader("ETag", "\"etag1\"");
1133 
1134         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
1135         req4.setHeader("User-Agent", "agent1");
1136 
1137         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1138 
1139         execute(req1);
1140 
1141         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1142 
1143         execute(req2);
1144 
1145         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1146 
1147         final ClassicHttpResponse result1 = execute(req3);
1148         final ClassicHttpResponse result2 = execute(req4);
1149 
1150         assertEquals(HttpStatus.SC_OK, result1.getCode());
1151         assertEquals("\"etag1\"", result1.getFirstHeader("ETag").getValue());
1152         assertEquals(DateUtils.formatStandardDate(now), result1.getFirstHeader("Date").getValue());
1153         assertEquals(DateUtils.formatStandardDate(now), result2.getFirstHeader("Date").getValue());
1154     }
1155 
1156     @Test
1157     public void testResponseToExistingVariantsIsCachedForFutureResponses() throws Exception {
1158 
1159         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1160         req1.setHeader("User-Agent", "agent1");
1161 
1162         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1163         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1164         resp1.setHeader("Vary", "User-Agent");
1165         resp1.setHeader("Cache-Control", "max-age=3600");
1166         resp1.setHeader("ETag", "\"etag1\"");
1167 
1168         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1169 
1170         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1171         req2.setHeader("User-Agent", "agent2");
1172 
1173         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
1174         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1175         resp2.setHeader("ETag", "\"etag1\"");
1176 
1177         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1178 
1179         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1180         req3.setHeader("User-Agent", "agent2");
1181 
1182         execute(req1);
1183         execute(req2);
1184         execute(req3);
1185     }
1186 
1187     /* "If any of the existing cache entries contains only partial content
1188      * for the associated entity, its entity-tag SHOULD NOT be included in
1189      * the If-None-Match header field unless the request is for a range
1190      * that would be fully satisfied by that entry."
1191      *
1192      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1193      */
1194     @Test
1195     public void variantNegotiationsDoNotIncludeEtagsForPartialResponses() throws Exception {
1196         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1197         req1.setHeader("User-Agent", "agent1");
1198         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1199         resp1.setHeader("Cache-Control", "max-age=3600");
1200         resp1.setHeader("Vary", "User-Agent");
1201         resp1.setHeader("ETag", "\"etag1\"");
1202 
1203         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1204         req2.setHeader("User-Agent", "agent2");
1205         req2.setHeader("Range", "bytes=0-49");
1206         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
1207         resp2.setEntity(HttpTestUtils.makeBody(50));
1208         resp2.setHeader("Content-Length","50");
1209         resp2.setHeader("Content-Range","bytes 0-49/100");
1210         resp2.setHeader("Vary","User-Agent");
1211         resp2.setHeader("ETag", "\"etag2\"");
1212         resp2.setHeader("Cache-Control","max-age=3600");
1213         resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
1214 
1215         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
1216         req3.setHeader("User-Agent", "agent3");
1217 
1218         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1219         resp1.setHeader("Cache-Control", "max-age=3600");
1220         resp1.setHeader("Vary", "User-Agent");
1221         resp1.setHeader("ETag", "\"etag3\"");
1222 
1223         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1224 
1225         execute(req1);
1226 
1227         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1228 
1229         execute(req2);
1230 
1231         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1232 
1233         execute(req3);
1234 
1235         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1236         Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
1237 
1238         final ClassicHttpRequest captured = reqCapture.getValue();
1239         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.IF_NONE_MATCH);
1240         while (it.hasNext()) {
1241             final HeaderElement elt = it.next();
1242             assertNotEquals("\"etag2\"", elt.toString());
1243         }
1244     }
1245 
1246     /* "If a cache receives a successful response whose Content-Location
1247      * field matches that of an existing cache entry for the same Request-
1248      * URI, whose entity-tag differs from that of the existing entry, and
1249      * whose Date is more recent than that of the existing entry, the
1250      * existing entry SHOULD NOT be returned in response to future requests
1251      * and SHOULD be deleted from the cache.
1252      *
1253      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
1254      */
1255     @Test
1256     public void cachedEntryShouldNotBeUsedIfMoreRecentMentionInContentLocation() throws Exception {
1257         final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
1258         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1259         resp1.setHeader("Cache-Control","max-age=3600");
1260         resp1.setHeader("ETag", "\"old-etag\"");
1261         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1262 
1263         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1264 
1265         final ClassicHttpRequest req2 = new HttpPost("http://foo.example.com/bar");
1266         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1267         resp2.setHeader("ETag", "\"new-etag\"");
1268         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1269         resp2.setHeader("Content-Location", "http://foo.example.com/");
1270 
1271         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1272 
1273         final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
1274         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1275 
1276         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1277 
1278         execute(req1);
1279         execute(req2);
1280         execute(req3);
1281     }
1282 
1283     /*
1284      * "This specifically means that responses from HTTP/1.0 servers for such
1285      * URIs [those containing a '?' in the rel_path part] SHOULD NOT be taken
1286      * from a cache."
1287      *
1288      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
1289      */
1290     @Test
1291     public void responseToGetWithQueryFrom1_0OriginAndNoExpiresIsNotCached() throws Exception {
1292         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
1293         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1294         resp2.setVersion(HttpVersion.HTTP_1_0);
1295         resp2.setEntity(HttpTestUtils.makeBody(200));
1296         resp2.setHeader("Content-Length","200");
1297         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1298 
1299         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1300 
1301         execute(req2);
1302     }
1303 
1304     @Test
1305     public void responseToGetWithQueryFrom1_0OriginVia1_1ProxyAndNoExpiresIsNotCached() throws Exception {
1306         final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
1307         final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
1308         resp2.setVersion(HttpVersion.HTTP_1_0);
1309         resp2.setEntity(HttpTestUtils.makeBody(200));
1310         resp2.setHeader("Content-Length","200");
1311         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1312         resp2.setHeader("Via","1.0 someproxy");
1313 
1314         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1315 
1316         execute(req2);
1317     }
1318 
1319     /*
1320      * "A cache that passes through requests for methods it does not
1321      * understand SHOULD invalidate any entities referred to by the
1322      * Request-URI."
1323      *
1324      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
1325      */
1326     @Test
1327     public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Exception {
1328         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1329         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1330         resp1.setHeader("Cache-Control","max-age=3600");
1331 
1332         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1333 
1334         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("FROB", "/");
1335         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1336         resp2.setHeader("Cache-Control","max-age=3600");
1337 
1338         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1339 
1340         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
1341         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1342         resp3.setHeader("ETag", "\"etag\"");
1343 
1344         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1345 
1346         execute(req1);
1347         execute(req2);
1348         final ClassicHttpResponse result = execute(req3);
1349 
1350         assertTrue(HttpTestUtils.semanticallyTransparent(resp3, result));
1351     }
1352 
1353     @Test
1354     public void shouldInvalidateAllVariantsForUnknownMethod() throws Exception {
1355         final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
1356         req1.setHeader("User-Agent", "agent1");
1357         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1358         resp1.setHeader("Cache-Control","max-age=3600");
1359         resp1.setHeader("Vary", "User-Agent");
1360 
1361         final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
1362         req2.setHeader("User-Agent", "agent2");
1363         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1364         resp2.setHeader("Cache-Control","max-age=3600");
1365         resp2.setHeader("Vary", "User-Agent");
1366 
1367         final ClassicHttpRequest req3 = new BasicClassicHttpRequest("FROB", "/");
1368         req3.setHeader("User-Agent", "agent3");
1369         final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
1370         resp3.setHeader("Cache-Control","max-age=3600");
1371 
1372         final ClassicHttpRequest req4 = new BasicClassicHttpRequest("GET", "/");
1373         req4.setHeader("User-Agent", "agent1");
1374         final ClassicHttpResponse resp4 = HttpTestUtils.make200Response();
1375         resp4.setHeader("ETag", "\"etag1\"");
1376 
1377         final ClassicHttpRequest req5 = new BasicClassicHttpRequest("GET", "/");
1378         req5.setHeader("User-Agent", "agent2");
1379         final ClassicHttpResponse resp5 = HttpTestUtils.make200Response();
1380         resp5.setHeader("ETag", "\"etag2\"");
1381 
1382         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1383 
1384         execute(req1);
1385 
1386         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1387 
1388         execute(req2);
1389 
1390         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
1391 
1392         execute(req3);
1393 
1394         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp4);
1395 
1396         final ClassicHttpResponse result4 = execute(req4);
1397 
1398         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp5);
1399 
1400         final ClassicHttpResponse result5 = execute(req5);
1401 
1402         assertTrue(HttpTestUtils.semanticallyTransparent(resp4, result4));
1403         assertTrue(HttpTestUtils.semanticallyTransparent(resp5, result5));
1404     }
1405 
1406     /*
1407      * "If a new cacheable response is received from a resource while any
1408      * existing responses for the same resource are cached, the cache
1409      * SHOULD use the new response to reply to the current request."
1410      *
1411      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12
1412      */
1413     @Test
1414     public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
1415         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1416         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1417         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1418         resp1.setHeader("Cache-Control", "max-age=3600");
1419         resp1.setHeader("ETag", "\"etag1\"");
1420 
1421         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1422 
1423         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1424         req2.setHeader("Cache-Control", "max-age=0");
1425         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1426         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
1427         resp2.setHeader("Cache-Control", "max-age=3600");
1428         resp2.setHeader("ETag", "\"etag2\"");
1429 
1430         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1431 
1432         final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
1433 
1434         execute(req1);
1435         execute(req2);
1436         final ClassicHttpResponse result = execute(req3);
1437 
1438         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1439     }
1440 
1441     /*
1442      * "Many HTTP/1.0 cache implementations will treat an Expires value
1443      * that is less than or equal to the response Date value as being
1444      * equivalent to the Cache-Control response directive 'no-cache'.
1445      * If an HTTP/1.1 cache receives such a response, and the response
1446      * does not include a Cache-Control header field, it SHOULD consider
1447      * the response to be non-cacheable in order to retain compatibility
1448      * with HTTP/1.0 servers."
1449      *
1450      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
1451      */
1452     @Test
1453     public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exception {
1454         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1455         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1456         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1457         resp1.setHeader("Expires", DateUtils.formatStandardDate(now));
1458         resp1.removeHeaders("Cache-Control");
1459 
1460         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1461 
1462         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1463         req2.setHeader("Cache-Control", "max-stale=1000");
1464         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1465         resp2.setHeader("ETag", "\"etag2\"");
1466 
1467         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1468 
1469         execute(req1);
1470         final ClassicHttpResponse result = execute(req2);
1471 
1472         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1473     }
1474 
1475     @Test
1476     public void expiresPriorToDateWithNoCacheControlIsNotCacheable() throws Exception {
1477         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1478         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1479         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
1480         resp1.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsAgo));
1481         resp1.removeHeaders("Cache-Control");
1482 
1483         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1484 
1485         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1486         req2.setHeader("Cache-Control", "max-stale=1000");
1487         final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
1488         resp2.setHeader("ETag", "\"etag2\"");
1489 
1490         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
1491 
1492         execute(req1);
1493         final ClassicHttpResponse result = execute(req2);
1494 
1495         assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
1496     }
1497 
1498     /*
1499      * "If a request includes the no-cache directive, it SHOULD NOT
1500      * include min-fresh, max-stale, or max-age."
1501      *
1502      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
1503      */
1504     @Test
1505     public void otherFreshnessRequestDirectivesNotAllowedWithNoCache() throws Exception {
1506         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1507         req1.setHeader("Cache-Control", "min-fresh=10, no-cache");
1508         req1.addHeader("Cache-Control", "max-stale=0, max-age=0");
1509 
1510         execute(req1);
1511 
1512         final ArgumentCaptor<ClassicHttpRequest> reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
1513         Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
1514 
1515         final ClassicHttpRequest captured = reqCapture.getValue();
1516         boolean foundNoCache = false;
1517         boolean foundDisallowedDirective = false;
1518         final List<String> disallowed =
1519             Arrays.asList("min-fresh", "max-stale", "max-age");
1520         final Iterator<HeaderElement> it = MessageSupport.iterate(captured, HttpHeaders.CACHE_CONTROL);
1521         while (it.hasNext()) {
1522             final HeaderElement elt = it.next();
1523             if (disallowed.contains(elt.getName())) {
1524                 foundDisallowedDirective = true;
1525             }
1526             if ("no-cache".equals(elt.getName())) {
1527                 foundNoCache = true;
1528             }
1529         }
1530         assertTrue(foundNoCache);
1531         assertFalse(foundDisallowedDirective);
1532     }
1533 
1534     /*
1535      * "To do this, the client may include the only-if-cached directive in
1536      * a request. If it receives this directive, a cache SHOULD either
1537      * respond using a cached entry that is consistent with the other
1538      * constraints of the request, or respond with a 504 (Gateway Timeout)
1539      * status."
1540      *
1541      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
1542      */
1543     @Test
1544     public void cacheMissResultsIn504WithOnlyIfCached() throws Exception {
1545         final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
1546         req.setHeader("Cache-Control", "only-if-cached");
1547 
1548         final ClassicHttpResponse result = execute(req);
1549 
1550         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1551     }
1552 
1553     @Test
1554     public void cacheHitOkWithOnlyIfCached() throws Exception {
1555         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1556         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1557         resp1.setHeader("Cache-Control","max-age=3600");
1558 
1559         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1560 
1561         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1562         req2.setHeader("Cache-Control", "only-if-cached");
1563 
1564         execute(req1);
1565         final ClassicHttpResponse result = execute(req2);
1566 
1567         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1568     }
1569 
1570     @Test
1571     public void returns504ForStaleEntryWithOnlyIfCached() throws Exception {
1572         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1573         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1574         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1575         resp1.setHeader("Cache-Control","max-age=5");
1576 
1577         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1578 
1579         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1580         req2.setHeader("Cache-Control", "only-if-cached");
1581 
1582         execute(req1);
1583         final ClassicHttpResponse result = execute(req2);
1584 
1585         assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, result.getCode());
1586     }
1587 
1588     @Test
1589     public void returnsStaleCacheEntryWithOnlyIfCachedAndMaxStale() throws Exception {
1590 
1591         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1592         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1593         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1594         resp1.setHeader("Cache-Control","max-age=5");
1595 
1596         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1597 
1598         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1599         req2.setHeader("Cache-Control", "max-stale=20, only-if-cached");
1600 
1601         execute(req1);
1602         final ClassicHttpResponse result = execute(req2);
1603 
1604         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
1605     }
1606 
1607     @Test
1608     public void issues304EvenWithWeakETag() throws Exception {
1609         final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
1610         final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
1611         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
1612         resp1.setHeader("Cache-Control", "max-age=300");
1613         resp1.setHeader("ETag","W/\"weak-sauce\"");
1614 
1615         Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
1616 
1617         final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
1618         req2.setHeader("If-None-Match","W/\"weak-sauce\"");
1619 
1620         execute(req1);
1621         final ClassicHttpResponse result = execute(req2);
1622 
1623         assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
1624     }
1625 
1626 }