Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package libcore.net.http;
     18 
     19 import com.google.mockwebserver.MockResponse;
     20 import com.google.mockwebserver.MockWebServer;
     21 import com.google.mockwebserver.RecordedRequest;
     22 import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
     23 import java.io.BufferedReader;
     24 import java.io.ByteArrayOutputStream;
     25 import java.io.File;
     26 import java.io.FileNotFoundException;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.io.InputStreamReader;
     30 import java.io.OutputStream;
     31 import java.lang.reflect.InvocationHandler;
     32 import java.net.CacheRequest;
     33 import java.net.CacheResponse;
     34 import java.net.CookieHandler;
     35 import java.net.CookieManager;
     36 import java.net.HttpCookie;
     37 import java.net.HttpURLConnection;
     38 import java.net.ResponseCache;
     39 import java.net.SecureCacheResponse;
     40 import java.net.URI;
     41 import java.net.URISyntaxException;
     42 import java.net.URL;
     43 import java.net.URLConnection;
     44 import java.security.Principal;
     45 import java.security.cert.Certificate;
     46 import java.text.DateFormat;
     47 import java.text.SimpleDateFormat;
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.Collections;
     51 import java.util.Date;
     52 import java.util.Deque;
     53 import java.util.Iterator;
     54 import java.util.List;
     55 import java.util.Locale;
     56 import java.util.Map;
     57 import java.util.TimeZone;
     58 import java.util.UUID;
     59 import java.util.concurrent.TimeUnit;
     60 import java.util.concurrent.atomic.AtomicInteger;
     61 import java.util.concurrent.atomic.AtomicReference;
     62 import java.util.zip.GZIPOutputStream;
     63 import javax.net.ssl.HttpsURLConnection;
     64 import junit.framework.TestCase;
     65 import libcore.javax.net.ssl.TestSSLContext;
     66 import tests.io.MockOs;
     67 
     68 public final class HttpResponseCacheTest extends TestCase {
     69     private MockWebServer server = new MockWebServer();
     70     private HttpResponseCache cache;
     71     private final MockOs mockOs = new MockOs();
     72     private final CookieManager cookieManager = new CookieManager();
     73 
     74     @Override protected void setUp() throws Exception {
     75         super.setUp();
     76 
     77         String tmp = System.getProperty("java.io.tmpdir");
     78         File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
     79         cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
     80         ResponseCache.setDefault(cache);
     81         mockOs.install();
     82         CookieHandler.setDefault(cookieManager);
     83     }
     84 
     85     @Override protected void tearDown() throws Exception {
     86         mockOs.uninstall();
     87         server.shutdown();
     88         ResponseCache.setDefault(null);
     89         cache.getCache().delete();
     90         CookieHandler.setDefault(null);
     91         super.tearDown();
     92     }
     93 
     94     /**
     95      * Test that response caching is consistent with the RI and the spec.
     96      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
     97      */
     98     public void testResponseCachingByResponseCode() throws Exception {
     99         // Test each documented HTTP/1.1 code, plus the first unused value in each range.
    100         // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
    101 
    102         // We can't test 100 because it's not really a response.
    103         // assertCached(false, 100);
    104         assertCached(false, 101);
    105         assertCached(false, 102);
    106         assertCached(true,  200);
    107         assertCached(false, 201);
    108         assertCached(false, 202);
    109         assertCached(true,  203);
    110         assertCached(false, 204);
    111         assertCached(false, 205);
    112         assertCached(false, 206); // we don't cache partial responses
    113         assertCached(false, 207);
    114         assertCached(true,  300);
    115         assertCached(true,  301);
    116         for (int i = 302; i <= 308; ++i) {
    117             assertCached(false, i);
    118         }
    119         for (int i = 400; i <= 406; ++i) {
    120             assertCached(false, i);
    121         }
    122         // (See test_responseCaching_407.)
    123         assertCached(false, 408);
    124         assertCached(false, 409);
    125         // (See test_responseCaching_410.)
    126         for (int i = 411; i <= 418; ++i) {
    127             assertCached(false, i);
    128         }
    129         for (int i = 500; i <= 506; ++i) {
    130             assertCached(false, i);
    131         }
    132     }
    133 
    134     /**
    135      * Response code 407 should only come from proxy servers. Android's client
    136      * throws if it is sent by an origin server.
    137      */
    138     public void testOriginServerSends407() throws Exception {
    139         server.enqueue(new MockResponse().setResponseCode(407));
    140         server.play();
    141 
    142         URL url = server.getUrl("/");
    143         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    144         try {
    145             conn.getResponseCode();
    146             fail();
    147         } catch (IOException expected) {
    148         }
    149     }
    150 
    151     public void test_responseCaching_410() throws Exception {
    152         // the HTTP spec permits caching 410s, but the RI doesn't.
    153         assertCached(true, 410);
    154     }
    155 
    156     private void assertCached(boolean shouldPut, int responseCode) throws Exception {
    157         server = new MockWebServer();
    158         server.enqueue(new MockResponse()
    159                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    160                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    161                 .setResponseCode(responseCode)
    162                 .setBody("ABCDE")
    163                 .addHeader("WWW-Authenticate: challenge"));
    164         server.play();
    165 
    166         URL url = server.getUrl("/");
    167         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    168         assertEquals(responseCode, conn.getResponseCode());
    169 
    170         // exhaust the content stream
    171         readAscii(conn);
    172 
    173         CacheResponse cached = cache.get(url.toURI(), "GET",
    174                 Collections.<String, List<String>>emptyMap());
    175         if (shouldPut) {
    176             assertNotNull(Integer.toString(responseCode), cached);
    177             cached.getBody().close();
    178         } else {
    179             assertNull(Integer.toString(responseCode), cached);
    180         }
    181         server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
    182     }
    183 
    184     /**
    185      * Test that we can interrogate the response when the cache is being
    186      * populated. http://code.google.com/p/android/issues/detail?id=7787
    187      */
    188     public void testResponseCacheCallbackApis() throws Exception {
    189         final String body = "ABCDE";
    190         final AtomicInteger cacheCount = new AtomicInteger();
    191 
    192         server.enqueue(new MockResponse()
    193                 .setStatus("HTTP/1.1 200 Fantastic")
    194                 .addHeader("fgh: ijk")
    195                 .setBody(body));
    196         server.play();
    197 
    198         ResponseCache.setDefault(new ResponseCache() {
    199             @Override public CacheResponse get(URI uri, String requestMethod,
    200                     Map<String, List<String>> requestHeaders) throws IOException {
    201                 return null;
    202             }
    203             @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
    204                 HttpURLConnection httpConnection = (HttpURLConnection) conn;
    205                 try {
    206                     httpConnection.getRequestProperties();
    207                     fail();
    208                 } catch (IllegalStateException expected) {
    209                 }
    210                 try {
    211                     httpConnection.addRequestProperty("K", "V");
    212                     fail();
    213                 } catch (IllegalStateException expected) {
    214                 }
    215                 assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null));
    216                 assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"),
    217                         httpConnection.getHeaderFields().get(null));
    218                 assertEquals(200, httpConnection.getResponseCode());
    219                 assertEquals("Fantastic", httpConnection.getResponseMessage());
    220                 assertEquals(body.length(), httpConnection.getContentLength());
    221                 assertEquals("ijk", httpConnection.getHeaderField("fgh"));
    222                 try {
    223                     httpConnection.getInputStream(); // the RI doesn't forbid this, but it should
    224                     fail();
    225                 } catch (IOException expected) {
    226                 }
    227                 cacheCount.incrementAndGet();
    228                 return null;
    229             }
    230         });
    231 
    232         URL url = server.getUrl("/");
    233         URLConnection connection = url.openConnection();
    234         assertEquals(body, readAscii(connection));
    235         assertEquals(1, cacheCount.get());
    236     }
    237 
    238 
    239     public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException {
    240         testResponseCaching(TransferKind.FIXED_LENGTH);
    241     }
    242 
    243     public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
    244         testResponseCaching(TransferKind.CHUNKED);
    245     }
    246 
    247     public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
    248         testResponseCaching(TransferKind.END_OF_STREAM);
    249     }
    250 
    251     /**
    252      * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
    253      * http://code.google.com/p/android/issues/detail?id=8175
    254      */
    255     private void testResponseCaching(TransferKind transferKind) throws IOException {
    256         MockResponse response = new MockResponse()
    257                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    258                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    259                 .setStatus("HTTP/1.1 200 Fantastic");
    260         transferKind.setBody(response, "I love puppies but hate spiders", 1);
    261         server.enqueue(response);
    262         server.play();
    263 
    264         // Make sure that calling skip() doesn't omit bytes from the cache.
    265         HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
    266         InputStream in = urlConnection.getInputStream();
    267         assertEquals("I love ", readAscii(urlConnection, "I love ".length()));
    268         reliableSkip(in, "puppies but hate ".length());
    269         assertEquals("spiders", readAscii(urlConnection, "spiders".length()));
    270         assertEquals(-1, in.read());
    271         in.close();
    272         assertEquals(1, cache.getWriteSuccessCount());
    273         assertEquals(0, cache.getWriteAbortCount());
    274 
    275         urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // cached!
    276         in = urlConnection.getInputStream();
    277         assertEquals("I love puppies but hate spiders",
    278                 readAscii(urlConnection, "I love puppies but hate spiders".length()));
    279         assertEquals(200, urlConnection.getResponseCode());
    280         assertEquals("Fantastic", urlConnection.getResponseMessage());
    281 
    282         assertEquals(-1, in.read());
    283         in.close();
    284         assertEquals(1, cache.getWriteSuccessCount());
    285         assertEquals(0, cache.getWriteAbortCount());
    286         assertEquals(2, cache.getRequestCount());
    287         assertEquals(1, cache.getHitCount());
    288     }
    289 
    290     public void testSecureResponseCaching() throws IOException {
    291         TestSSLContext testSSLContext = TestSSLContext.create();
    292         server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
    293         server.enqueue(new MockResponse()
    294                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    295                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    296                 .setBody("ABC"));
    297         server.play();
    298 
    299         HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
    300         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    301         assertEquals("ABC", readAscii(connection));
    302 
    303         // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
    304         String suite = connection.getCipherSuite();
    305         List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
    306         List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
    307         Principal peerPrincipal = connection.getPeerPrincipal();
    308         Principal localPrincipal = connection.getLocalPrincipal();
    309 
    310         connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached!
    311         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    312         assertEquals("ABC", readAscii(connection));
    313 
    314         assertEquals(2, cache.getRequestCount());
    315         assertEquals(1, cache.getNetworkCount());
    316         assertEquals(1, cache.getHitCount());
    317 
    318         assertEquals(suite, connection.getCipherSuite());
    319         assertEquals(localCerts, toListOrNull(connection.getLocalCertificates()));
    320         assertEquals(serverCerts, toListOrNull(connection.getServerCertificates()));
    321         assertEquals(peerPrincipal, connection.getPeerPrincipal());
    322         assertEquals(localPrincipal, connection.getLocalPrincipal());
    323     }
    324 
    325     public void testCacheReturnsInsecureResponseForSecureRequest() throws IOException {
    326         TestSSLContext testSSLContext = TestSSLContext.create();
    327         server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
    328         server.enqueue(new MockResponse().setBody("ABC"));
    329         server.enqueue(new MockResponse().setBody("DEF"));
    330         server.play();
    331 
    332         ResponseCache.setDefault(new InsecureResponseCache());
    333 
    334         HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
    335         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    336         assertEquals("ABC", readAscii(connection));
    337 
    338         connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // not cached!
    339         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    340         assertEquals("DEF", readAscii(connection));
    341     }
    342 
    343     public void testResponseCachingAndRedirects() throws Exception {
    344         server.enqueue(new MockResponse()
    345                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    346                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    347                 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
    348                 .addHeader("Location: /foo"));
    349         server.enqueue(new MockResponse()
    350                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    351                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    352                 .setBody("ABC"));
    353         server.enqueue(new MockResponse().setBody("DEF"));
    354         server.play();
    355 
    356         URLConnection connection = server.getUrl("/").openConnection();
    357         assertEquals("ABC", readAscii(connection));
    358 
    359         connection = server.getUrl("/").openConnection(); // cached!
    360         assertEquals("ABC", readAscii(connection));
    361 
    362         assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
    363         assertEquals(2, cache.getNetworkCount());
    364         assertEquals(2, cache.getHitCount());
    365     }
    366 
    367     public void testRedirectToCachedResult() throws Exception {
    368         server.enqueue(new MockResponse()
    369                 .addHeader("Cache-Control: max-age=60")
    370                 .setBody("ABC"));
    371         server.enqueue(new MockResponse()
    372                 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
    373                 .addHeader("Location: /foo"));
    374         server.enqueue(new MockResponse().setBody("DEF"));
    375         server.play();
    376 
    377         assertEquals("ABC", readAscii(server.getUrl("/foo").openConnection()));
    378         RecordedRequest request1 = server.takeRequest();
    379         assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
    380         assertEquals(0, request1.getSequenceNumber());
    381 
    382         assertEquals("ABC", readAscii(server.getUrl("/bar").openConnection()));
    383         RecordedRequest request2 = server.takeRequest();
    384         assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
    385         assertEquals(1, request2.getSequenceNumber());
    386 
    387         // an unrelated request should reuse the pooled connection
    388         assertEquals("DEF", readAscii(server.getUrl("/baz").openConnection()));
    389         RecordedRequest request3 = server.takeRequest();
    390         assertEquals("GET /baz HTTP/1.1", request3.getRequestLine());
    391         assertEquals(2, request3.getSequenceNumber());
    392     }
    393 
    394     public void testSecureResponseCachingAndRedirects() throws IOException {
    395         TestSSLContext testSSLContext = TestSSLContext.create();
    396         server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
    397         server.enqueue(new MockResponse()
    398                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    399                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    400                 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
    401                 .addHeader("Location: /foo"));
    402         server.enqueue(new MockResponse()
    403                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    404                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    405                 .setBody("ABC"));
    406         server.enqueue(new MockResponse().setBody("DEF"));
    407         server.play();
    408 
    409         HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
    410         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    411         assertEquals("ABC", readAscii(connection));
    412 
    413         connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached!
    414         connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
    415         assertEquals("ABC", readAscii(connection));
    416 
    417         assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
    418         assertEquals(2, cache.getHitCount());
    419     }
    420 
    421     public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException {
    422         server.enqueue(new MockResponse().setBody("ABC"));
    423         server.play();
    424 
    425         final AtomicReference<Map<String, List<String>>> requestHeadersRef
    426                 = new AtomicReference<Map<String, List<String>>>();
    427         ResponseCache.setDefault(new ResponseCache() {
    428             @Override public CacheResponse get(URI uri, String requestMethod,
    429                     Map<String, List<String>> requestHeaders) throws IOException {
    430                 requestHeadersRef.set(requestHeaders);
    431                 return null;
    432             }
    433             @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
    434                 return null;
    435             }
    436         });
    437 
    438         URL url = server.getUrl("/");
    439         URLConnection urlConnection = url.openConnection();
    440         urlConnection.addRequestProperty("A", "android");
    441         readAscii(urlConnection);
    442         assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
    443     }
    444 
    445 
    446     public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
    447         testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
    448     }
    449 
    450     public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
    451         testServerPrematureDisconnect(TransferKind.CHUNKED);
    452     }
    453 
    454     public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
    455         /*
    456          * Intentionally empty. This case doesn't make sense because there's no
    457          * such thing as a premature disconnect when the disconnect itself
    458          * indicates the end of the data stream.
    459          */
    460     }
    461 
    462     private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
    463         MockResponse response = new MockResponse();
    464         transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
    465         server.enqueue(truncateViolently(response, 16));
    466         server.enqueue(new MockResponse().setBody("Request #2"));
    467         server.play();
    468 
    469         BufferedReader reader = new BufferedReader(new InputStreamReader(
    470                 server.getUrl("/").openConnection().getInputStream()));
    471         assertEquals("ABCDE", reader.readLine());
    472         try {
    473             reader.readLine();
    474             fail("This implementation silently ignored a truncated HTTP body.");
    475         } catch (IOException expected) {
    476         } finally {
    477             reader.close();
    478         }
    479 
    480         assertEquals(1, cache.getWriteAbortCount());
    481         assertEquals(0, cache.getWriteSuccessCount());
    482         URLConnection connection = server.getUrl("/").openConnection();
    483         assertEquals("Request #2", readAscii(connection));
    484         assertEquals(1, cache.getWriteAbortCount());
    485         assertEquals(1, cache.getWriteSuccessCount());
    486     }
    487 
    488     public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException {
    489         testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
    490     }
    491 
    492     public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException {
    493         testClientPrematureDisconnect(TransferKind.CHUNKED);
    494     }
    495 
    496     public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException {
    497         testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
    498     }
    499 
    500     private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
    501         MockResponse response = new MockResponse();
    502         transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
    503         server.enqueue(response);
    504         server.enqueue(new MockResponse().setBody("Request #2"));
    505         server.play();
    506 
    507         URLConnection connection = server.getUrl("/").openConnection();
    508         InputStream in = connection.getInputStream();
    509         assertEquals("ABCDE", readAscii(connection, 5));
    510         in.close();
    511         try {
    512             in.read();
    513             fail("Expected an IOException because the stream is closed.");
    514         } catch (IOException expected) {
    515         }
    516 
    517         assertEquals(1, cache.getWriteAbortCount());
    518         assertEquals(0, cache.getWriteSuccessCount());
    519         connection = server.getUrl("/").openConnection();
    520         assertEquals("Request #2", readAscii(connection));
    521         assertEquals(1, cache.getWriteAbortCount());
    522         assertEquals(1, cache.getWriteSuccessCount());
    523     }
    524 
    525     public void testDefaultExpirationDateFullyCachedForLessThan24Hours() throws Exception {
    526         //      last modified: 105 seconds ago
    527         //             served:   5 seconds ago
    528         //   default lifetime: (105 - 5) / 10 = 10 seconds
    529         //            expires:  10 seconds from served date = 5 seconds from now
    530         server.enqueue(new MockResponse()
    531                 .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
    532                 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
    533                 .setBody("A"));
    534         server.play();
    535 
    536         URL url = server.getUrl("/");
    537         assertEquals("A", readAscii(url.openConnection()));
    538         URLConnection connection = url.openConnection();
    539         assertEquals("A", readAscii(connection));
    540         assertNull(connection.getHeaderField("Warning"));
    541     }
    542 
    543     public void testDefaultExpirationDateConditionallyCached() throws Exception {
    544         //      last modified: 115 seconds ago
    545         //             served:  15 seconds ago
    546         //   default lifetime: (115 - 15) / 10 = 10 seconds
    547         //            expires:  10 seconds from served date = 5 seconds ago
    548         String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
    549         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    550                 .addHeader("Last-Modified: " + lastModifiedDate)
    551                 .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
    552         List<String> headers = conditionalRequest.getHeaders();
    553         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    554     }
    555 
    556     public void testDefaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
    557         //      last modified: 105 days ago
    558         //             served:   5 days ago
    559         //   default lifetime: (105 - 5) / 10 = 10 days
    560         //            expires:  10 days from served date = 5 days from now
    561         server.enqueue(new MockResponse()
    562                 .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
    563                 .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
    564                 .setBody("A"));
    565         server.play();
    566 
    567         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    568         URLConnection connection = server.getUrl("/").openConnection();
    569         assertEquals("A", readAscii(connection));
    570         assertEquals("113 HttpURLConnection \"Heuristic expiration\"",
    571                 connection.getHeaderField("Warning"));
    572     }
    573 
    574     public void testNoDefaultExpirationForUrlsWithQueryString() throws Exception {
    575         server.enqueue(new MockResponse()
    576                 .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
    577                 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
    578                 .setBody("A"));
    579         server.enqueue(new MockResponse().setBody("B"));
    580         server.play();
    581 
    582         URL url = server.getUrl("/?foo=bar");
    583         assertEquals("A", readAscii(url.openConnection()));
    584         assertEquals("B", readAscii(url.openConnection()));
    585     }
    586 
    587     public void testExpirationDateInThePastWithLastModifiedHeader() throws Exception {
    588         String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
    589         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    590                 .addHeader("Last-Modified: " + lastModifiedDate)
    591                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
    592         List<String> headers = conditionalRequest.getHeaders();
    593         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    594     }
    595 
    596     public void testExpirationDateInThePastWithNoLastModifiedHeader() throws Exception {
    597         assertNotCached(new MockResponse()
    598                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
    599     }
    600 
    601     public void testExpirationDateInTheFuture() throws Exception {
    602         assertFullyCached(new MockResponse()
    603                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
    604     }
    605 
    606     public void testMaxAgePreferredWithMaxAgeAndExpires() throws Exception {
    607         assertFullyCached(new MockResponse()
    608                 .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
    609                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
    610                 .addHeader("Cache-Control: max-age=60"));
    611     }
    612 
    613     public void testMaxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception {
    614         String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
    615         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    616                 .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
    617                 .addHeader("Last-Modified: " + lastModifiedDate)
    618                 .addHeader("Cache-Control: max-age=60"));
    619         List<String> headers = conditionalRequest.getHeaders();
    620         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    621     }
    622 
    623     public void testMaxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
    624         /*
    625          * Chrome interprets max-age relative to the local clock. Both our cache
    626          * and Firefox both use the earlier of the local and server's clock.
    627          */
    628         assertNotCached(new MockResponse()
    629                 .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
    630                 .addHeader("Cache-Control: max-age=60"));
    631     }
    632 
    633     public void testMaxAgeInTheFutureWithDateHeader() throws Exception {
    634         assertFullyCached(new MockResponse()
    635                 .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
    636                 .addHeader("Cache-Control: max-age=60"));
    637     }
    638 
    639     public void testMaxAgeInTheFutureWithNoDateHeader() throws Exception {
    640         assertFullyCached(new MockResponse()
    641                 .addHeader("Cache-Control: max-age=60"));
    642     }
    643 
    644     public void testMaxAgeWithLastModifiedButNoServedDate() throws Exception {
    645         assertFullyCached(new MockResponse()
    646                 .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
    647                 .addHeader("Cache-Control: max-age=60"));
    648     }
    649 
    650     public void testMaxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception {
    651         assertFullyCached(new MockResponse()
    652                 .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
    653                 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
    654                 .addHeader("Cache-Control: max-age=60"));
    655     }
    656 
    657     public void testMaxAgePreferredOverLowerSharedMaxAge() throws Exception {
    658         assertFullyCached(new MockResponse()
    659                 .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
    660                 .addHeader("Cache-Control: s-maxage=60")
    661                 .addHeader("Cache-Control: max-age=180"));
    662     }
    663 
    664     public void testMaxAgePreferredOverHigherMaxAge() throws Exception {
    665         assertNotCached(new MockResponse()
    666                 .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
    667                 .addHeader("Cache-Control: s-maxage=180")
    668                 .addHeader("Cache-Control: max-age=60"));
    669     }
    670 
    671     public void testRequestMethodOptionsIsNotCached() throws Exception {
    672         testRequestMethod("OPTIONS", false);
    673     }
    674 
    675     public void testRequestMethodGetIsCached() throws Exception {
    676         testRequestMethod("GET", true);
    677     }
    678 
    679     public void testRequestMethodHeadIsNotCached() throws Exception {
    680         // We could support this but choose not to for implementation simplicity
    681         testRequestMethod("HEAD", false);
    682     }
    683 
    684     public void testRequestMethodPostIsNotCached() throws Exception {
    685         // We could support this but choose not to for implementation simplicity
    686         testRequestMethod("POST", false);
    687     }
    688 
    689     public void testRequestMethodPutIsNotCached() throws Exception {
    690         testRequestMethod("PUT", false);
    691     }
    692 
    693     public void testRequestMethodDeleteIsNotCached() throws Exception {
    694         testRequestMethod("DELETE", false);
    695     }
    696 
    697     public void testRequestMethodTraceIsNotCached() throws Exception {
    698         testRequestMethod("TRACE", false);
    699     }
    700 
    701     private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception {
    702         /*
    703          * 1. seed the cache (potentially)
    704          * 2. expect a cache hit or miss
    705          */
    706         server.enqueue(new MockResponse()
    707                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    708                 .addHeader("X-Response-ID: 1"));
    709         server.enqueue(new MockResponse()
    710                 .addHeader("X-Response-ID: 2"));
    711         server.play();
    712 
    713         URL url = server.getUrl("/");
    714 
    715         HttpURLConnection request1 = (HttpURLConnection) url.openConnection();
    716         request1.setRequestMethod(requestMethod);
    717         addRequestBodyIfNecessary(requestMethod, request1);
    718         assertEquals("1", request1.getHeaderField("X-Response-ID"));
    719 
    720         URLConnection request2 = url.openConnection();
    721         if (expectCached) {
    722             assertEquals("1", request1.getHeaderField("X-Response-ID"));
    723         } else {
    724             assertEquals("2", request2.getHeaderField("X-Response-ID"));
    725         }
    726     }
    727 
    728     public void testPostInvalidatesCache() throws Exception {
    729         testMethodInvalidates("POST");
    730     }
    731 
    732     public void testPutInvalidatesCache() throws Exception {
    733         testMethodInvalidates("PUT");
    734     }
    735 
    736     public void testDeleteMethodInvalidatesCache() throws Exception {
    737         testMethodInvalidates("DELETE");
    738     }
    739 
    740     private void testMethodInvalidates(String requestMethod) throws Exception {
    741         /*
    742          * 1. seed the cache
    743          * 2. invalidate it
    744          * 3. expect a cache miss
    745          */
    746         server.enqueue(new MockResponse().setBody("A")
    747                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
    748         server.enqueue(new MockResponse().setBody("B"));
    749         server.enqueue(new MockResponse().setBody("C"));
    750         server.play();
    751 
    752         URL url = server.getUrl("/");
    753 
    754         assertEquals("A", readAscii(url.openConnection()));
    755 
    756         HttpURLConnection invalidate = (HttpURLConnection) url.openConnection();
    757         invalidate.setRequestMethod(requestMethod);
    758         addRequestBodyIfNecessary(requestMethod, invalidate);
    759         assertEquals("B", readAscii(invalidate));
    760 
    761         assertEquals("C", readAscii(url.openConnection()));
    762     }
    763 
    764     public void testEtag() throws Exception {
    765         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    766                 .addHeader("ETag: v1"));
    767         assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1"));
    768     }
    769 
    770     public void testEtagAndExpirationDateInThePast() throws Exception {
    771         String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
    772         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    773                 .addHeader("ETag: v1")
    774                 .addHeader("Last-Modified: " + lastModifiedDate)
    775                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
    776         List<String> headers = conditionalRequest.getHeaders();
    777         assertTrue(headers.contains("If-None-Match: v1"));
    778         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    779     }
    780 
    781     public void testEtagAndExpirationDateInTheFuture() throws Exception {
    782         assertFullyCached(new MockResponse()
    783                 .addHeader("ETag: v1")
    784                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    785                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
    786     }
    787 
    788     public void testCacheControlNoCache() throws Exception {
    789         assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache"));
    790     }
    791 
    792     public void testCacheControlNoCacheAndExpirationDateInTheFuture() throws Exception {
    793         String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
    794         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    795                 .addHeader("Last-Modified: " + lastModifiedDate)
    796                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    797                 .addHeader("Cache-Control: no-cache"));
    798         List<String> headers = conditionalRequest.getHeaders();
    799         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    800     }
    801 
    802     public void testPragmaNoCache() throws Exception {
    803         assertNotCached(new MockResponse().addHeader("Pragma: no-cache"));
    804     }
    805 
    806     public void testPragmaNoCacheAndExpirationDateInTheFuture() throws Exception {
    807         String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
    808         RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
    809                 .addHeader("Last-Modified: " + lastModifiedDate)
    810                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    811                 .addHeader("Pragma: no-cache"));
    812         List<String> headers = conditionalRequest.getHeaders();
    813         assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
    814     }
    815 
    816     public void testCacheControlNoStore() throws Exception {
    817         assertNotCached(new MockResponse().addHeader("Cache-Control: no-store"));
    818     }
    819 
    820     public void testCacheControlNoStoreAndExpirationDateInTheFuture() throws Exception {
    821         assertNotCached(new MockResponse()
    822                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    823                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    824                 .addHeader("Cache-Control: no-store"));
    825     }
    826 
    827     public void testPartialRangeResponsesDoNotCorruptCache() throws Exception {
    828         /*
    829          * 1. request a range
    830          * 2. request a full document, expecting a cache miss
    831          */
    832         server.enqueue(new MockResponse().setBody("AA")
    833                 .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
    834                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
    835                 .addHeader("Content-Range: bytes 1000-1001/2000"));
    836         server.enqueue(new MockResponse().setBody("BB"));
    837         server.play();
    838 
    839         URL url = server.getUrl("/");
    840 
    841         URLConnection range = url.openConnection();
    842         range.addRequestProperty("Range", "bytes=1000-1001");
    843         assertEquals("AA", readAscii(range));
    844 
    845         assertEquals("BB", readAscii(url.openConnection()));
    846     }
    847 
    848     public void testServerReturnsDocumentOlderThanCache() throws Exception {
    849         server.enqueue(new MockResponse().setBody("A")
    850                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    851                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
    852         server.enqueue(new MockResponse().setBody("B")
    853                 .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
    854         server.play();
    855 
    856         URL url = server.getUrl("/");
    857 
    858         assertEquals("A", readAscii(url.openConnection()));
    859         assertEquals("A", readAscii(url.openConnection()));
    860     }
    861 
    862     public void testNonIdentityEncodingAndConditionalCache() throws Exception {
    863         assertNonIdentityEncodingCached(new MockResponse()
    864                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    865                 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
    866     }
    867 
    868     public void testNonIdentityEncodingAndFullCache() throws Exception {
    869         assertNonIdentityEncodingCached(new MockResponse()
    870                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    871                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
    872     }
    873 
    874     private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
    875         server.enqueue(response
    876                 .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
    877                 .addHeader("Content-Encoding: gzip"));
    878         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
    879 
    880         server.play();
    881         assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection()));
    882         assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection()));
    883     }
    884 
    885     public void testExpiresDateBeforeModifiedDate() throws Exception {
    886         assertConditionallyCached(new MockResponse()
    887                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
    888                 .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)));
    889     }
    890 
    891     public void testRequestMaxAge() throws IOException {
    892         server.enqueue(new MockResponse().setBody("A")
    893                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
    894                 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))
    895                 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
    896         server.enqueue(new MockResponse().setBody("B"));
    897 
    898         server.play();
    899         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    900 
    901         URLConnection connection = server.getUrl("/").openConnection();
    902         connection.addRequestProperty("Cache-Control", "max-age=30");
    903         assertEquals("B", readAscii(connection));
    904     }
    905 
    906     public void testRequestMinFresh() throws IOException {
    907         server.enqueue(new MockResponse().setBody("A")
    908                 .addHeader("Cache-Control: max-age=60")
    909                 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
    910         server.enqueue(new MockResponse().setBody("B"));
    911 
    912         server.play();
    913         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    914 
    915         URLConnection connection = server.getUrl("/").openConnection();
    916         connection.addRequestProperty("Cache-Control", "min-fresh=120");
    917         assertEquals("B", readAscii(connection));
    918     }
    919 
    920     public void testRequestMaxStale() throws IOException {
    921         server.enqueue(new MockResponse().setBody("A")
    922                 .addHeader("Cache-Control: max-age=120")
    923                 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
    924         server.enqueue(new MockResponse().setBody("B"));
    925 
    926         server.play();
    927         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    928 
    929         URLConnection connection = server.getUrl("/").openConnection();
    930         connection.addRequestProperty("Cache-Control", "max-stale=180");
    931         assertEquals("A", readAscii(connection));
    932         assertEquals("110 HttpURLConnection \"Response is stale\"",
    933                 connection.getHeaderField("Warning"));
    934     }
    935 
    936     public void testRequestMaxStaleNotHonoredWithMustRevalidate() throws IOException {
    937         server.enqueue(new MockResponse().setBody("A")
    938                 .addHeader("Cache-Control: max-age=120, must-revalidate")
    939                 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
    940         server.enqueue(new MockResponse().setBody("B"));
    941 
    942         server.play();
    943         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    944 
    945         URLConnection connection = server.getUrl("/").openConnection();
    946         connection.addRequestProperty("Cache-Control", "max-stale=180");
    947         assertEquals("B", readAscii(connection));
    948     }
    949 
    950     public void testRequestOnlyIfCachedWithNoResponseCached() throws IOException {
    951         // (no responses enqueued)
    952         server.play();
    953 
    954         HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
    955         connection.addRequestProperty("Cache-Control", "only-if-cached");
    956         assertBadGateway(connection);
    957     }
    958 
    959     public void testRequestOnlyIfCachedWithFullResponseCached() throws IOException {
    960         server.enqueue(new MockResponse().setBody("A")
    961                 .addHeader("Cache-Control: max-age=30")
    962                 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
    963         server.play();
    964 
    965         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    966         URLConnection connection = server.getUrl("/").openConnection();
    967         connection.addRequestProperty("Cache-Control", "only-if-cached");
    968         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    969     }
    970 
    971     public void testRequestOnlyIfCachedWithConditionalResponseCached() throws IOException {
    972         server.enqueue(new MockResponse().setBody("A")
    973                 .addHeader("Cache-Control: max-age=30")
    974                 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
    975         server.play();
    976 
    977         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    978         HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
    979         connection.addRequestProperty("Cache-Control", "only-if-cached");
    980         assertBadGateway(connection);
    981     }
    982 
    983     public void testRequestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
    984         server.enqueue(new MockResponse().setBody("A"));
    985         server.play();
    986 
    987         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
    988         HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
    989         connection.addRequestProperty("Cache-Control", "only-if-cached");
    990         assertBadGateway(connection);
    991     }
    992 
    993     public void testRequestCacheControlNoCache() throws Exception {
    994         server.enqueue(new MockResponse()
    995                 .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
    996                 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
    997                 .addHeader("Cache-Control: max-age=60")
    998                 .setBody("A"));
    999         server.enqueue(new MockResponse().setBody("B"));
   1000         server.play();
   1001 
   1002         URL url = server.getUrl("/");
   1003         assertEquals("A", readAscii(url.openConnection()));
   1004         URLConnection connection = url.openConnection();
   1005         connection.setRequestProperty("Cache-Control", "no-cache");
   1006         assertEquals("B", readAscii(connection));
   1007     }
   1008 
   1009     public void testRequestPragmaNoCache() throws Exception {
   1010         server.enqueue(new MockResponse()
   1011                 .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
   1012                 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
   1013                 .addHeader("Cache-Control: max-age=60")
   1014                 .setBody("A"));
   1015         server.enqueue(new MockResponse().setBody("B"));
   1016         server.play();
   1017 
   1018         URL url = server.getUrl("/");
   1019         assertEquals("A", readAscii(url.openConnection()));
   1020         URLConnection connection = url.openConnection();
   1021         connection.setRequestProperty("Pragma", "no-cache");
   1022         assertEquals("B", readAscii(connection));
   1023     }
   1024 
   1025     public void testClientSuppliedIfModifiedSinceWithCachedResult() throws Exception {
   1026         MockResponse response = new MockResponse()
   1027                 .addHeader("ETag: v3")
   1028                 .addHeader("Cache-Control: max-age=0");
   1029         String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
   1030         RecordedRequest request = assertClientSuppliedCondition(
   1031                 response, "If-Modified-Since", ifModifiedSinceDate);
   1032         List<String> headers = request.getHeaders();
   1033         assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate));
   1034         assertFalse(headers.contains("If-None-Match: v3"));
   1035     }
   1036 
   1037     public void testClientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
   1038         String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES);
   1039         MockResponse response = new MockResponse()
   1040                 .addHeader("Last-Modified: " + lastModifiedDate)
   1041                 .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
   1042                 .addHeader("Cache-Control: max-age=0");
   1043         RecordedRequest request = assertClientSuppliedCondition(
   1044                 response, "If-None-Match", "v1");
   1045         List<String> headers = request.getHeaders();
   1046         assertTrue(headers.contains("If-None-Match: v1"));
   1047         assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate));
   1048     }
   1049 
   1050     private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
   1051             String conditionValue) throws Exception {
   1052         server.enqueue(seed.setBody("A"));
   1053         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1054         server.play();
   1055 
   1056         URL url = server.getUrl("/");
   1057         assertEquals("A", readAscii(url.openConnection()));
   1058 
   1059         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
   1060         connection.addRequestProperty(conditionName, conditionValue);
   1061         assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
   1062         assertEquals("", readAscii(connection));
   1063 
   1064         server.takeRequest(); // seed
   1065         return server.takeRequest();
   1066     }
   1067 
   1068     public void testSetIfModifiedSince() throws Exception {
   1069         Date since = new Date();
   1070         server.enqueue(new MockResponse().setBody("A"));
   1071         server.play();
   1072 
   1073         URL url = server.getUrl("/");
   1074         URLConnection connection = url.openConnection();
   1075         connection.setIfModifiedSince(since.getTime());
   1076         assertEquals("A", readAscii(connection));
   1077         RecordedRequest request = server.takeRequest();
   1078         assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since)));
   1079     }
   1080 
   1081     public void testClientSuppliedConditionWithoutCachedResult() throws Exception {
   1082         server.enqueue(new MockResponse()
   1083                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1084         server.play();
   1085 
   1086         HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
   1087         String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS);
   1088         connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince);
   1089         assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
   1090         assertEquals("", readAscii(connection));
   1091     }
   1092 
   1093     public void testAuthorizationRequestHeaderPreventsCaching() throws Exception {
   1094         server.enqueue(new MockResponse()
   1095                 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES))
   1096                 .addHeader("Cache-Control: max-age=60")
   1097                 .setBody("A"));
   1098         server.enqueue(new MockResponse().setBody("B"));
   1099         server.play();
   1100 
   1101         URL url = server.getUrl("/");
   1102         URLConnection connection = url.openConnection();
   1103         connection.addRequestProperty("Authorization", "password");
   1104         assertEquals("A", readAscii(connection));
   1105         assertEquals("B", readAscii(url.openConnection()));
   1106     }
   1107 
   1108     public void testAuthorizationResponseCachedWithSMaxAge() throws Exception {
   1109         assertAuthorizationRequestFullyCached(new MockResponse()
   1110                 .addHeader("Cache-Control: s-maxage=60"));
   1111     }
   1112 
   1113     public void testAuthorizationResponseCachedWithPublic() throws Exception {
   1114         assertAuthorizationRequestFullyCached(new MockResponse()
   1115                 .addHeader("Cache-Control: public"));
   1116     }
   1117 
   1118     public void testAuthorizationResponseCachedWithMustRevalidate() throws Exception {
   1119         assertAuthorizationRequestFullyCached(new MockResponse()
   1120                 .addHeader("Cache-Control: must-revalidate"));
   1121     }
   1122 
   1123     public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception {
   1124         server.enqueue(response
   1125                 .addHeader("Cache-Control: max-age=60")
   1126                 .setBody("A"));
   1127         server.enqueue(new MockResponse().setBody("B"));
   1128         server.play();
   1129 
   1130         URL url = server.getUrl("/");
   1131         URLConnection connection = url.openConnection();
   1132         connection.addRequestProperty("Authorization", "password");
   1133         assertEquals("A", readAscii(connection));
   1134         assertEquals("A", readAscii(url.openConnection()));
   1135     }
   1136 
   1137     public void testContentLocationDoesNotPopulateCache() throws Exception {
   1138         server.enqueue(new MockResponse()
   1139                 .addHeader("Cache-Control: max-age=60")
   1140                 .addHeader("Content-Location: /bar")
   1141                 .setBody("A"));
   1142         server.enqueue(new MockResponse().setBody("B"));
   1143         server.play();
   1144 
   1145         assertEquals("A", readAscii(server.getUrl("/foo").openConnection()));
   1146         assertEquals("B", readAscii(server.getUrl("/bar").openConnection()));
   1147     }
   1148 
   1149     public void testUseCachesFalseDoesNotWriteToCache() throws Exception {
   1150         server.enqueue(new MockResponse()
   1151                 .addHeader("Cache-Control: max-age=60")
   1152                 .setBody("A").setBody("A"));
   1153         server.enqueue(new MockResponse().setBody("B"));
   1154         server.play();
   1155 
   1156         URLConnection connection = server.getUrl("/").openConnection();
   1157         connection.setUseCaches(false);
   1158         assertEquals("A", readAscii(connection));
   1159         assertEquals("B", readAscii(server.getUrl("/").openConnection()));
   1160     }
   1161 
   1162     public void testUseCachesFalseDoesNotReadFromCache() throws Exception {
   1163         server.enqueue(new MockResponse()
   1164                 .addHeader("Cache-Control: max-age=60")
   1165                 .setBody("A").setBody("A"));
   1166         server.enqueue(new MockResponse().setBody("B"));
   1167         server.play();
   1168 
   1169         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1170         URLConnection connection = server.getUrl("/").openConnection();
   1171         connection.setUseCaches(false);
   1172         assertEquals("B", readAscii(connection));
   1173     }
   1174 
   1175     public void testDefaultUseCachesSetsInitialValueOnly() throws Exception {
   1176         URL url = new URL("http://localhost/");
   1177         URLConnection c1 = url.openConnection();
   1178         URLConnection c2 = url.openConnection();
   1179         assertTrue(c1.getDefaultUseCaches());
   1180         c1.setDefaultUseCaches(false);
   1181         try {
   1182             assertTrue(c1.getUseCaches());
   1183             assertTrue(c2.getUseCaches());
   1184             URLConnection c3 = url.openConnection();
   1185             assertFalse(c3.getUseCaches());
   1186         } finally {
   1187             c1.setDefaultUseCaches(true);
   1188         }
   1189     }
   1190 
   1191     public void testConnectionIsReturnedToPoolAfterConditionalSuccess() throws Exception {
   1192         server.enqueue(new MockResponse()
   1193                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1194                 .addHeader("Cache-Control: max-age=0")
   1195                 .setBody("A"));
   1196         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1197         server.enqueue(new MockResponse().setBody("B"));
   1198         server.play();
   1199 
   1200         assertEquals("A", readAscii(server.getUrl("/a").openConnection()));
   1201         assertEquals("A", readAscii(server.getUrl("/a").openConnection()));
   1202         assertEquals("B", readAscii(server.getUrl("/b").openConnection()));
   1203 
   1204         assertEquals(0, server.takeRequest().getSequenceNumber());
   1205         assertEquals(1, server.takeRequest().getSequenceNumber());
   1206         assertEquals(2, server.takeRequest().getSequenceNumber());
   1207     }
   1208 
   1209     public void testStatisticsConditionalCacheMiss() throws Exception {
   1210         server.enqueue(new MockResponse()
   1211                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1212                 .addHeader("Cache-Control: max-age=0")
   1213                 .setBody("A"));
   1214         server.enqueue(new MockResponse().setBody("B"));
   1215         server.enqueue(new MockResponse().setBody("C"));
   1216         server.play();
   1217 
   1218         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1219         assertEquals(1, cache.getRequestCount());
   1220         assertEquals(1, cache.getNetworkCount());
   1221         assertEquals(0, cache.getHitCount());
   1222         assertEquals("B", readAscii(server.getUrl("/").openConnection()));
   1223         assertEquals("C", readAscii(server.getUrl("/").openConnection()));
   1224         assertEquals(3, cache.getRequestCount());
   1225         assertEquals(3, cache.getNetworkCount());
   1226         assertEquals(0, cache.getHitCount());
   1227     }
   1228 
   1229     public void testStatisticsConditionalCacheHit() throws Exception {
   1230         server.enqueue(new MockResponse()
   1231                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1232                 .addHeader("Cache-Control: max-age=0")
   1233                 .setBody("A"));
   1234         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1235         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1236         server.play();
   1237 
   1238         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1239         assertEquals(1, cache.getRequestCount());
   1240         assertEquals(1, cache.getNetworkCount());
   1241         assertEquals(0, cache.getHitCount());
   1242         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1243         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1244         assertEquals(3, cache.getRequestCount());
   1245         assertEquals(3, cache.getNetworkCount());
   1246         assertEquals(2, cache.getHitCount());
   1247     }
   1248 
   1249     public void testStatisticsFullCacheHit() throws Exception {
   1250         server.enqueue(new MockResponse()
   1251                 .addHeader("Cache-Control: max-age=60")
   1252                 .setBody("A"));
   1253         server.play();
   1254 
   1255         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1256         assertEquals(1, cache.getRequestCount());
   1257         assertEquals(1, cache.getNetworkCount());
   1258         assertEquals(0, cache.getHitCount());
   1259         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1260         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1261         assertEquals(3, cache.getRequestCount());
   1262         assertEquals(1, cache.getNetworkCount());
   1263         assertEquals(2, cache.getHitCount());
   1264     }
   1265 
   1266     public void testVaryMatchesChangedRequestHeaderField() throws Exception {
   1267         server.enqueue(new MockResponse()
   1268                 .addHeader("Cache-Control: max-age=60")
   1269                 .addHeader("Vary: Accept-Language")
   1270                 .setBody("A"));
   1271         server.enqueue(new MockResponse().setBody("B"));
   1272         server.play();
   1273 
   1274         URL url = server.getUrl("/");
   1275         HttpURLConnection frConnection = (HttpURLConnection) url.openConnection();
   1276         frConnection.addRequestProperty("Accept-Language", "fr-CA");
   1277         assertEquals("A", readAscii(frConnection));
   1278 
   1279         HttpURLConnection enConnection = (HttpURLConnection) url.openConnection();
   1280         enConnection.addRequestProperty("Accept-Language", "en-US");
   1281         assertEquals("B", readAscii(enConnection));
   1282     }
   1283 
   1284     public void testVaryMatchesUnchangedRequestHeaderField() throws Exception {
   1285         server.enqueue(new MockResponse()
   1286                 .addHeader("Cache-Control: max-age=60")
   1287                 .addHeader("Vary: Accept-Language")
   1288                 .setBody("A"));
   1289         server.enqueue(new MockResponse().setBody("B"));
   1290         server.play();
   1291 
   1292         URL url = server.getUrl("/");
   1293         URLConnection connection1 = url.openConnection();
   1294         connection1.addRequestProperty("Accept-Language", "fr-CA");
   1295         assertEquals("A", readAscii(connection1));
   1296         URLConnection connection2 = url.openConnection();
   1297         connection2.addRequestProperty("Accept-Language", "fr-CA");
   1298         assertEquals("A", readAscii(connection2));
   1299     }
   1300 
   1301     public void testVaryMatchesAbsentRequestHeaderField() throws Exception {
   1302         server.enqueue(new MockResponse()
   1303                 .addHeader("Cache-Control: max-age=60")
   1304                 .addHeader("Vary: Foo")
   1305                 .setBody("A"));
   1306         server.enqueue(new MockResponse().setBody("B"));
   1307         server.play();
   1308 
   1309         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1310         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1311     }
   1312 
   1313     public void testVaryMatchesAddedRequestHeaderField() throws Exception {
   1314         server.enqueue(new MockResponse()
   1315                 .addHeader("Cache-Control: max-age=60")
   1316                 .addHeader("Vary: Foo")
   1317                 .setBody("A"));
   1318         server.enqueue(new MockResponse().setBody("B"));
   1319         server.play();
   1320 
   1321         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1322         URLConnection fooConnection = server.getUrl("/").openConnection();
   1323         fooConnection.addRequestProperty("Foo", "bar");
   1324         assertEquals("B", readAscii(fooConnection));
   1325     }
   1326 
   1327     public void testVaryMatchesRemovedRequestHeaderField() throws Exception {
   1328         server.enqueue(new MockResponse()
   1329                 .addHeader("Cache-Control: max-age=60")
   1330                 .addHeader("Vary: Foo")
   1331                 .setBody("A"));
   1332         server.enqueue(new MockResponse().setBody("B"));
   1333         server.play();
   1334 
   1335         URLConnection fooConnection = server.getUrl("/").openConnection();
   1336         fooConnection.addRequestProperty("Foo", "bar");
   1337         assertEquals("A", readAscii(fooConnection));
   1338         assertEquals("B", readAscii(server.getUrl("/").openConnection()));
   1339     }
   1340 
   1341     public void testVaryFieldsAreCaseInsensitive() throws Exception {
   1342         server.enqueue(new MockResponse()
   1343                 .addHeader("Cache-Control: max-age=60")
   1344                 .addHeader("Vary: ACCEPT-LANGUAGE")
   1345                 .setBody("A"));
   1346         server.enqueue(new MockResponse().setBody("B"));
   1347         server.play();
   1348 
   1349         URL url = server.getUrl("/");
   1350         URLConnection connection1 = url.openConnection();
   1351         connection1.addRequestProperty("Accept-Language", "fr-CA");
   1352         assertEquals("A", readAscii(connection1));
   1353         URLConnection connection2 = url.openConnection();
   1354         connection2.addRequestProperty("accept-language", "fr-CA");
   1355         assertEquals("A", readAscii(connection2));
   1356     }
   1357 
   1358     public void testVaryMultipleFieldsWithMatch() throws Exception {
   1359         server.enqueue(new MockResponse()
   1360                 .addHeader("Cache-Control: max-age=60")
   1361                 .addHeader("Vary: Accept-Language, Accept-Charset")
   1362                 .addHeader("Vary: Accept-Encoding")
   1363                 .setBody("A"));
   1364         server.enqueue(new MockResponse().setBody("B"));
   1365         server.play();
   1366 
   1367         URL url = server.getUrl("/");
   1368         URLConnection connection1 = url.openConnection();
   1369         connection1.addRequestProperty("Accept-Language", "fr-CA");
   1370         connection1.addRequestProperty("Accept-Charset", "UTF-8");
   1371         connection1.addRequestProperty("Accept-Encoding", "identity");
   1372         assertEquals("A", readAscii(connection1));
   1373         URLConnection connection2 = url.openConnection();
   1374         connection2.addRequestProperty("Accept-Language", "fr-CA");
   1375         connection2.addRequestProperty("Accept-Charset", "UTF-8");
   1376         connection2.addRequestProperty("Accept-Encoding", "identity");
   1377         assertEquals("A", readAscii(connection2));
   1378     }
   1379 
   1380     public void testVaryMultipleFieldsWithNoMatch() throws Exception {
   1381         server.enqueue(new MockResponse()
   1382                 .addHeader("Cache-Control: max-age=60")
   1383                 .addHeader("Vary: Accept-Language, Accept-Charset")
   1384                 .addHeader("Vary: Accept-Encoding")
   1385                 .setBody("A"));
   1386         server.enqueue(new MockResponse().setBody("B"));
   1387         server.play();
   1388 
   1389         URL url = server.getUrl("/");
   1390         URLConnection frConnection = url.openConnection();
   1391         frConnection.addRequestProperty("Accept-Language", "fr-CA");
   1392         frConnection.addRequestProperty("Accept-Charset", "UTF-8");
   1393         frConnection.addRequestProperty("Accept-Encoding", "identity");
   1394         assertEquals("A", readAscii(frConnection));
   1395         URLConnection enConnection = url.openConnection();
   1396         enConnection.addRequestProperty("Accept-Language", "en-CA");
   1397         enConnection.addRequestProperty("Accept-Charset", "UTF-8");
   1398         enConnection.addRequestProperty("Accept-Encoding", "identity");
   1399         assertEquals("B", readAscii(enConnection));
   1400     }
   1401 
   1402     public void testVaryMultipleFieldValuesWithMatch() throws Exception {
   1403         server.enqueue(new MockResponse()
   1404                 .addHeader("Cache-Control: max-age=60")
   1405                 .addHeader("Vary: Accept-Language")
   1406                 .setBody("A"));
   1407         server.enqueue(new MockResponse().setBody("B"));
   1408         server.play();
   1409 
   1410         URL url = server.getUrl("/");
   1411         URLConnection connection1 = url.openConnection();
   1412         connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
   1413         connection1.addRequestProperty("Accept-Language", "en-US");
   1414         assertEquals("A", readAscii(connection1));
   1415 
   1416         URLConnection connection2 = url.openConnection();
   1417         connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
   1418         connection2.addRequestProperty("Accept-Language", "en-US");
   1419         assertEquals("A", readAscii(connection2));
   1420     }
   1421 
   1422     public void testVaryMultipleFieldValuesWithNoMatch() throws Exception {
   1423         server.enqueue(new MockResponse()
   1424                 .addHeader("Cache-Control: max-age=60")
   1425                 .addHeader("Vary: Accept-Language")
   1426                 .setBody("A"));
   1427         server.enqueue(new MockResponse().setBody("B"));
   1428         server.play();
   1429 
   1430         URL url = server.getUrl("/");
   1431         URLConnection connection1 = url.openConnection();
   1432         connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
   1433         connection1.addRequestProperty("Accept-Language", "en-US");
   1434         assertEquals("A", readAscii(connection1));
   1435 
   1436         URLConnection connection2 = url.openConnection();
   1437         connection2.addRequestProperty("Accept-Language", "fr-CA");
   1438         connection2.addRequestProperty("Accept-Language", "en-US");
   1439         assertEquals("B", readAscii(connection2));
   1440     }
   1441 
   1442     public void testVaryAsterisk() throws Exception {
   1443         server.enqueue(new MockResponse()
   1444                 .addHeader("Cache-Control: max-age=60")
   1445                 .addHeader("Vary: *")
   1446                 .setBody("A"));
   1447         server.enqueue(new MockResponse().setBody("B"));
   1448         server.play();
   1449 
   1450         assertEquals("A", readAscii(server.getUrl("/").openConnection()));
   1451         assertEquals("B", readAscii(server.getUrl("/").openConnection()));
   1452     }
   1453 
   1454     public void testVaryAndHttps() throws Exception {
   1455         TestSSLContext testSSLContext = TestSSLContext.create();
   1456         server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
   1457         server.enqueue(new MockResponse()
   1458                 .addHeader("Cache-Control: max-age=60")
   1459                 .addHeader("Vary: Accept-Language")
   1460                 .setBody("A"));
   1461         server.enqueue(new MockResponse().setBody("B"));
   1462         server.play();
   1463 
   1464         URL url = server.getUrl("/");
   1465         HttpsURLConnection connection1 = (HttpsURLConnection) url.openConnection();
   1466         connection1.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
   1467         connection1.addRequestProperty("Accept-Language", "en-US");
   1468         assertEquals("A", readAscii(connection1));
   1469 
   1470         HttpsURLConnection connection2 = (HttpsURLConnection) url.openConnection();
   1471         connection2.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
   1472         connection2.addRequestProperty("Accept-Language", "en-US");
   1473         assertEquals("A", readAscii(connection2));
   1474     }
   1475 
   1476     public void testDiskWriteFailureCacheDegradation() throws Exception {
   1477         Deque<InvocationHandler> writeHandlers = mockOs.getHandlers("write");
   1478         int i = 0;
   1479         boolean hasMoreScenarios = true;
   1480         while (hasMoreScenarios) {
   1481             mockOs.enqueueNormal("write", i++);
   1482             mockOs.enqueueFault("write");
   1483             exercisePossiblyFaultyCache(false);
   1484             hasMoreScenarios = writeHandlers.isEmpty();
   1485             writeHandlers.clear();
   1486         }
   1487         System.out.println("Exercising the cache performs " + (i - 1) + " writes.");
   1488     }
   1489 
   1490     public void testDiskReadFailureCacheDegradation() throws Exception {
   1491         Deque<InvocationHandler> readHandlers = mockOs.getHandlers("read");
   1492         int i = 0;
   1493         boolean hasMoreScenarios = true;
   1494         while (hasMoreScenarios) {
   1495             mockOs.enqueueNormal("read", i++);
   1496             mockOs.enqueueFault("read");
   1497             exercisePossiblyFaultyCache(true);
   1498             hasMoreScenarios = readHandlers.isEmpty();
   1499             readHandlers.clear();
   1500         }
   1501         System.out.println("Exercising the cache performs " + (i - 1) + " reads.");
   1502     }
   1503 
   1504     public void testCachePlusCookies() throws Exception {
   1505         server.enqueue(new MockResponse()
   1506                 .addHeader("Set-Cookie: a=FIRST; domain=.local;")
   1507                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1508                 .addHeader("Cache-Control: max-age=0")
   1509                 .setBody("A"));
   1510         server.enqueue(new MockResponse()
   1511                 .addHeader("Set-Cookie: a=SECOND; domain=.local;")
   1512                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1513         server.play();
   1514 
   1515         URL url = server.getUrl("/");
   1516         assertEquals("A", readAscii(url.openConnection()));
   1517         assertCookies(url, "a=FIRST");
   1518         assertEquals("A", readAscii(url.openConnection()));
   1519         assertCookies(url, "a=SECOND");
   1520     }
   1521 
   1522     public void testGetHeadersReturnsNetworkEndToEndHeaders() throws Exception {
   1523         server.enqueue(new MockResponse()
   1524                 .addHeader("Allow: GET, HEAD")
   1525                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1526                 .addHeader("Cache-Control: max-age=0")
   1527                 .setBody("A"));
   1528         server.enqueue(new MockResponse()
   1529                 .addHeader("Allow: GET, HEAD, PUT")
   1530                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1531         server.play();
   1532 
   1533         URLConnection connection1 = server.getUrl("/").openConnection();
   1534         assertEquals("A", readAscii(connection1));
   1535         assertEquals("GET, HEAD", connection1.getHeaderField("Allow"));
   1536 
   1537         URLConnection connection2 = server.getUrl("/").openConnection();
   1538         assertEquals("A", readAscii(connection2));
   1539         assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow"));
   1540     }
   1541 
   1542     public void testGetHeadersReturnsCachedHopByHopHeaders() throws Exception {
   1543         server.enqueue(new MockResponse()
   1544                 .addHeader("Transfer-Encoding: identity")
   1545                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1546                 .addHeader("Cache-Control: max-age=0")
   1547                 .setBody("A"));
   1548         server.enqueue(new MockResponse()
   1549                 .addHeader("Transfer-Encoding: none")
   1550                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1551         server.play();
   1552 
   1553         URLConnection connection1 = server.getUrl("/").openConnection();
   1554         assertEquals("A", readAscii(connection1));
   1555         assertEquals("identity", connection1.getHeaderField("Transfer-Encoding"));
   1556 
   1557         URLConnection connection2 = server.getUrl("/").openConnection();
   1558         assertEquals("A", readAscii(connection2));
   1559         assertEquals("identity", connection2.getHeaderField("Transfer-Encoding"));
   1560     }
   1561 
   1562     public void testGetHeadersDeletesCached100LevelWarnings() throws Exception {
   1563         server.enqueue(new MockResponse()
   1564                 .addHeader("Warning: 199 test danger")
   1565                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1566                 .addHeader("Cache-Control: max-age=0")
   1567                 .setBody("A"));
   1568         server.enqueue(new MockResponse()
   1569                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1570         server.play();
   1571 
   1572         URLConnection connection1 = server.getUrl("/").openConnection();
   1573         assertEquals("A", readAscii(connection1));
   1574         assertEquals("199 test danger", connection1.getHeaderField("Warning"));
   1575 
   1576         URLConnection connection2 = server.getUrl("/").openConnection();
   1577         assertEquals("A", readAscii(connection2));
   1578         assertEquals(null, connection2.getHeaderField("Warning"));
   1579     }
   1580 
   1581     public void testGetHeadersRetainsCached200LevelWarnings() throws Exception {
   1582         server.enqueue(new MockResponse()
   1583                 .addHeader("Warning: 299 test danger")
   1584                 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
   1585                 .addHeader("Cache-Control: max-age=0")
   1586                 .setBody("A"));
   1587         server.enqueue(new MockResponse()
   1588                 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1589         server.play();
   1590 
   1591         URLConnection connection1 = server.getUrl("/").openConnection();
   1592         assertEquals("A", readAscii(connection1));
   1593         assertEquals("299 test danger", connection1.getHeaderField("Warning"));
   1594 
   1595         URLConnection connection2 = server.getUrl("/").openConnection();
   1596         assertEquals("A", readAscii(connection2));
   1597         assertEquals("299 test danger", connection2.getHeaderField("Warning"));
   1598     }
   1599 
   1600     public void assertCookies(URL url, String... expectedCookies) throws Exception {
   1601         List<String> actualCookies = new ArrayList<String>();
   1602         for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
   1603             actualCookies.add(cookie.toString());
   1604         }
   1605         assertEquals(Arrays.asList(expectedCookies), actualCookies);
   1606     }
   1607 
   1608     public void testCachePlusRange() throws Exception {
   1609         assertNotCached(new MockResponse()
   1610                 .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
   1611                 .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
   1612                 .addHeader("Content-Range: bytes 100-100/200")
   1613                 .addHeader("Cache-Control: max-age=60"));
   1614     }
   1615 
   1616     /**
   1617      * @param delta the offset from the current date to use. Negative
   1618      *     values yield dates in the past; positive values yield dates in the
   1619      *     future.
   1620      */
   1621     private String formatDate(long delta, TimeUnit timeUnit) {
   1622         return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
   1623     }
   1624 
   1625     private String formatDate(Date date) {
   1626         DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
   1627         rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
   1628         return rfc1123.format(date);
   1629     }
   1630 
   1631     private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate)
   1632             throws IOException {
   1633         if (requestMethod.equals("POST") || requestMethod.equals("PUT")) {
   1634             invalidate.setDoOutput(true);
   1635             OutputStream requestBody = invalidate.getOutputStream();
   1636             requestBody.write('x');
   1637             requestBody.close();
   1638         }
   1639     }
   1640 
   1641     private void assertNotCached(MockResponse response) throws Exception {
   1642         server.enqueue(response.setBody("A"));
   1643         server.enqueue(new MockResponse().setBody("B"));
   1644         server.play();
   1645 
   1646         URL url = server.getUrl("/");
   1647         assertEquals("A", readAscii(url.openConnection()));
   1648         assertEquals("B", readAscii(url.openConnection()));
   1649     }
   1650 
   1651     private void exercisePossiblyFaultyCache(boolean permitReadBodyFailures) throws Exception {
   1652         server.shutdown();
   1653         server = new MockWebServer();
   1654         server.enqueue(new MockResponse()
   1655                 .addHeader("Cache-Control: max-age=60")
   1656                 .setBody("A"));
   1657         server.enqueue(new MockResponse().setBody("B"));
   1658         server.play();
   1659 
   1660         URL url = server.getUrl("/" + UUID.randomUUID());
   1661         assertEquals("A", readAscii(url.openConnection()));
   1662 
   1663         URLConnection connection = url.openConnection();
   1664         InputStream in = connection.getInputStream();
   1665         try {
   1666             int bodyChar = in.read();
   1667             assertTrue(bodyChar == 'A' || bodyChar == 'B');
   1668             assertEquals(-1, in.read());
   1669         } catch (IOException e) {
   1670             if (!permitReadBodyFailures) {
   1671                 throw e;
   1672             }
   1673         }
   1674     }
   1675 
   1676     /**
   1677      * @return the request with the conditional get headers.
   1678      */
   1679     private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception {
   1680         // scenario 1: condition succeeds
   1681         server.enqueue(response.setBody("A"));
   1682         server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
   1683 
   1684         // scenario 2: condition fails
   1685         server.enqueue(response.setBody("B"));
   1686         server.enqueue(new MockResponse().setBody("C"));
   1687 
   1688         server.play();
   1689 
   1690         URL valid = server.getUrl("/valid");
   1691         assertEquals("A", readAscii(valid.openConnection()));
   1692         assertEquals("A", readAscii(valid.openConnection()));
   1693 
   1694         URL invalid = server.getUrl("/invalid");
   1695         assertEquals("B", readAscii(invalid.openConnection()));
   1696         assertEquals("C", readAscii(invalid.openConnection()));
   1697 
   1698         server.takeRequest(); // regular get
   1699         return server.takeRequest(); // conditional get
   1700     }
   1701 
   1702     private void assertFullyCached(MockResponse response) throws Exception {
   1703         server.enqueue(response.setBody("A"));
   1704         server.enqueue(response.setBody("B"));
   1705         server.play();
   1706 
   1707         URL url = server.getUrl("/");
   1708         assertEquals("A", readAscii(url.openConnection()));
   1709         assertEquals("A", readAscii(url.openConnection()));
   1710     }
   1711 
   1712     /**
   1713      * Shortens the body of {@code response} but not the corresponding headers.
   1714      * Only useful to test how clients respond to the premature conclusion of
   1715      * the HTTP body.
   1716      */
   1717     private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
   1718         response.setSocketPolicy(DISCONNECT_AT_END);
   1719         List<String> headers = new ArrayList<String>(response.getHeaders());
   1720         response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
   1721         response.getHeaders().clear();
   1722         response.getHeaders().addAll(headers);
   1723         return response;
   1724     }
   1725 
   1726     /**
   1727      * Reads {@code count} characters from the stream. If the stream is
   1728      * exhausted before {@code count} characters can be read, the remaining
   1729      * characters are returned and the stream is closed.
   1730      */
   1731     private String readAscii(URLConnection connection, int count) throws IOException {
   1732         HttpURLConnection httpConnection = (HttpURLConnection) connection;
   1733         InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST
   1734                 ? connection.getInputStream()
   1735                 : httpConnection.getErrorStream();
   1736         StringBuilder result = new StringBuilder();
   1737         for (int i = 0; i < count; i++) {
   1738             int value = in.read();
   1739             if (value == -1) {
   1740                 in.close();
   1741                 break;
   1742             }
   1743             result.append((char) value);
   1744         }
   1745         return result.toString();
   1746     }
   1747 
   1748     private String readAscii(URLConnection connection) throws IOException {
   1749         return readAscii(connection, Integer.MAX_VALUE);
   1750     }
   1751 
   1752     private void reliableSkip(InputStream in, int length) throws IOException {
   1753         while (length > 0) {
   1754             length -= in.skip(length);
   1755         }
   1756     }
   1757 
   1758     private void assertBadGateway(HttpURLConnection connection) throws IOException {
   1759         try {
   1760             connection.getInputStream();
   1761             fail();
   1762         } catch (FileNotFoundException expected) {
   1763         }
   1764         assertEquals(HttpURLConnection.HTTP_BAD_GATEWAY, connection.getResponseCode());
   1765         assertEquals(-1, connection.getErrorStream().read());
   1766     }
   1767 
   1768     enum TransferKind {
   1769         CHUNKED() {
   1770             @Override void setBody(MockResponse response, byte[] content, int chunkSize)
   1771                     throws IOException {
   1772                 response.setChunkedBody(content, chunkSize);
   1773             }
   1774         },
   1775         FIXED_LENGTH() {
   1776             @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
   1777                 response.setBody(content);
   1778             }
   1779         },
   1780         END_OF_STREAM() {
   1781             @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
   1782                 response.setBody(content);
   1783                 response.setSocketPolicy(DISCONNECT_AT_END);
   1784                 for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
   1785                     if (h.next().startsWith("Content-Length:")) {
   1786                         h.remove();
   1787                         break;
   1788                     }
   1789                 }
   1790             }
   1791         };
   1792 
   1793         abstract void setBody(MockResponse response, byte[] content, int chunkSize)
   1794                 throws IOException;
   1795 
   1796         void setBody(MockResponse response, String content, int chunkSize) throws IOException {
   1797             setBody(response, content.getBytes("UTF-8"), chunkSize);
   1798         }
   1799     }
   1800 
   1801     private <T> List<T> toListOrNull(T[] arrayOrNull) {
   1802         return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null;
   1803     }
   1804 
   1805     /**
   1806      * Returns a gzipped copy of {@code bytes}.
   1807      */
   1808     public byte[] gzip(byte[] bytes) throws IOException {
   1809         ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
   1810         OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
   1811         gzippedOut.write(bytes);
   1812         gzippedOut.close();
   1813         return bytesOut.toByteArray();
   1814     }
   1815 
   1816     private class InsecureResponseCache extends ResponseCache {
   1817         @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
   1818             return cache.put(uri, connection);
   1819         }
   1820 
   1821         @Override public CacheResponse get(URI uri, String requestMethod,
   1822                 Map<String, List<String>> requestHeaders) throws IOException {
   1823             final CacheResponse response = cache.get(uri, requestMethod, requestHeaders);
   1824             if (response instanceof SecureCacheResponse) {
   1825                 return new CacheResponse() {
   1826                     @Override public InputStream getBody() throws IOException {
   1827                         return response.getBody();
   1828                     }
   1829                     @Override public Map<String, List<String>> getHeaders() throws IOException {
   1830                         return response.getHeaders();
   1831                     }
   1832                 };
   1833             }
   1834             return response;
   1835         }
   1836     }
   1837 }
   1838