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