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