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