View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
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    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static org.apache.http.client.utils.DateUtils.formatDate;
18  import static org.htmlunit.HttpHeader.CACHE_CONTROL;
19  import static org.htmlunit.HttpHeader.ETAG;
20  import static org.htmlunit.HttpHeader.EXPIRES;
21  import static org.htmlunit.HttpHeader.IF_MODIFIED_SINCE;
22  import static org.htmlunit.HttpHeader.IF_NONE_MATCH;
23  import static org.htmlunit.HttpHeader.LAST_MODIFIED;
24  
25  import java.net.URL;
26  import java.text.SimpleDateFormat;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Date;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.commons.lang3.time.DateUtils;
35  import org.htmlunit.html.HtmlPage;
36  import org.htmlunit.http.HttpStatus;
37  import org.htmlunit.junit.annotation.Alerts;
38  import org.htmlunit.util.MimeType;
39  import org.htmlunit.util.NameValuePair;
40  import org.htmlunit.util.mocks.WebResponseMock;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * Tests for {@link Cache}.
45   *
46   * @author Marc Guillemot
47   * @author Ahmed Ashour
48   * @author Frank Danek
49   * @author Anton Demydenko
50   * @author Ronald Brill
51   * @author Ashley Frieze
52   * @author Lai Quang Duong
53   */
54  public class CacheTest extends SimpleWebTestCase {
55  
56      private static final long ONE_MINUTE = 60_000L;
57      private static final long ONE_HOUR = ONE_MINUTE * 60;
58  
59      private final long now_ = new Date().getTime();
60      private final String tomorrow_ = formatDate(DateUtils.addDays(new Date(), 1));
61  
62      /**
63       * Composite test of {@link Cache#isCacheableContent(WebResponse)}.
64       */
65      @Test
66      public void isCacheableContent() {
67          final Cache cache = new Cache();
68          final Map<String, String> headers = new HashMap<>();
69          final WebResponse response = new WebResponseMock(null, headers);
70  
71          assertFalse(cache.isCacheableContent(response));
72  
73          headers.put(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT");
74          assertTrue(cache.isCacheableContent(response));
75  
76          headers.put(LAST_MODIFIED, formatDate(DateUtils.addMinutes(new Date(), -5)));
77          assertTrue(cache.isCacheableContent(response));
78  
79          headers.put(LAST_MODIFIED, formatDate(new Date()));
80          assertFalse(cache.isCacheableContent(response));
81  
82          headers.put(LAST_MODIFIED, formatDate(DateUtils.addMinutes(new Date(), 10)));
83          assertFalse(cache.isCacheableContent(response));
84  
85          headers.put(EXPIRES, formatDate(DateUtils.addMinutes(new Date(), 5)));
86          assertFalse(cache.isCacheableContent(response));
87  
88          headers.put(EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
89          assertTrue(cache.isCacheableContent(response));
90  
91          headers.remove(LAST_MODIFIED);
92          assertTrue(cache.isCacheableContent(response));
93  
94          headers.put(EXPIRES, "0");
95          assertFalse(cache.isCacheableContent(response));
96  
97          headers.put(EXPIRES, "-1");
98          assertFalse(cache.isCacheableContent(response));
99  
100         headers.put(CACHE_CONTROL, "no-store");
101         assertFalse(cache.isCacheableContent(response));
102     }
103 
104     /**
105      * @throws Exception if the test fails
106      */
107     @Test
108     public void contentWithNoHeadersIsNotCached() {
109         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, null), now_, now_));
110     }
111 
112     /**
113      * @throws Exception if the test fails
114      */
115     @Test
116     public void contentWithExpiryDateIsCached() {
117         final Map<String, String> headers = new HashMap<>();
118         headers.put(EXPIRES, tomorrow_);
119 
120         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
121     }
122 
123     /**
124      * @throws Exception if the test fails
125      */
126     @Test
127     public void contentWithExpiryDateInFutureButShortMaxAgeIsNotInCacheWindow() {
128         final Map<String, String> headers = new HashMap<>();
129         headers.put(EXPIRES, tomorrow_);
130         // max age is 1 second, so will have expired after a minute
131         headers.put(CACHE_CONTROL, "some-other-value, max-age=1");
132 
133         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
134     }
135 
136     /**
137      * @throws Exception if the test fails
138      */
139     @Test
140     public void contentWithExpiryDateInFutureButShortSMaxAgeIsNotInCacheWindow() {
141         final Map<String, String> headers = new HashMap<>();
142         headers.put(EXPIRES, tomorrow_);
143         // s max age is 1 second, so will have expired after a minute
144         headers.put(CACHE_CONTROL, "some-other-value, s-maxage=1");
145 
146         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
147     }
148 
149     /**
150      * @throws Exception if the test fails
151      */
152     @Test
153     public void contentWithBothMaxAgeAndSMaxUsesSMaxAsPriority() {
154         final Map<String, String> headers = new HashMap<>();
155         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200, s-maxage=1");
156 
157         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
158     }
159 
160     /**
161      * @throws Exception if the test fails
162      */
163     @Test
164     public void contentWithMaxAgeInFutureWillBeCached() {
165         final Map<String, String> headers = new HashMap<>();
166         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200");
167 
168         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
169 
170         headers.clear();
171         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200");
172 
173         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
174     }
175 
176     /**
177      * @throws Exception if the test fails
178      */
179     @Test
180     public void contentWithLongLastModifiedTimeComparedToNowIsCachedOnDownload() {
181         final Map<String, String> headers = new HashMap<>();
182         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
183 
184         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
185     }
186 
187     /**
188      * @throws Exception if the test fails
189      */
190     @Test
191     public void contentWithLastModifiedTimeIsCachedAfterAFewPercentOfCreationAge() {
192         final Map<String, String> headers = new HashMap<>();
193         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
194 
195         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_HOUR, now_));
196     }
197 
198     /**
199      * @throws Exception if the test fails
200      */
201     @Test
202     public void contentWithLastModifiedTimeIsNotCachedAfterALongerPeriod() {
203         final Map<String, String> headers = new HashMap<>();
204         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
205 
206         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + (ONE_HOUR * 5), now_));
207     }
208 
209     /**
210      * @throws Exception if the test fails
211      */
212     @Test
213     public void usage() throws Exception {
214         final String content = DOCTYPE_HTML
215             + "<html><head><title>page 1</title>\n"
216             + "<script src='foo1.js'></script>\n"
217             + "<script src='foo2.js'></script>\n"
218             + "</head><body>\n"
219             + "<a href='page2.html'>to page 2</a>\n"
220             + "</body></html>";
221 
222         final String content2 = DOCTYPE_HTML
223             + "<html><head><title>page 2</title>\n"
224             + "<script src='foo2.js'></script>\n"
225             + "</head><body>\n"
226             + "<a href='page1.html'>to page 1</a>\n"
227             + "</body></html>";
228 
229         final String script1 = "alert('in foo1');";
230         final String script2 = "alert('in foo2');";
231 
232         final WebClient webClient = getWebClient();
233         final MockWebConnection connection = new MockWebConnection();
234         webClient.setWebConnection(connection);
235 
236         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
237         connection.setResponse(urlPage1, content);
238         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
239         connection.setResponse(urlPage2, content2);
240 
241         final List<NameValuePair> headers = new ArrayList<>();
242         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
243         connection.setResponse(new URL(URL_FIRST, "foo1.js"), script1, 200, "ok",
244                 MimeType.TEXT_JAVASCRIPT, headers);
245         connection.setResponse(new URL(URL_FIRST, "foo2.js"), script2, 200, "ok",
246                 MimeType.TEXT_JAVASCRIPT, headers);
247 
248         final List<String> collectedAlerts = new ArrayList<>();
249         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
250 
251         final HtmlPage page1 = webClient.getPage(urlPage1);
252         final String[] expectedAlerts = {"in foo1", "in foo2"};
253         assertEquals(expectedAlerts, collectedAlerts);
254 
255         collectedAlerts.clear();
256         page1.getAnchors().get(0).click();
257 
258         assertEquals(new String[] {"in foo2"}, collectedAlerts);
259         assertEquals("no request for scripts should have been performed",
260                 urlPage2, connection.getLastWebRequest().getUrl());
261     }
262 
263     /**
264      * @throws Exception if the test fails
265      */
266     @Test
267     public void jsUrlEncoded() throws Exception {
268         final String content = DOCTYPE_HTML
269             + "<html>\n"
270             + "<head>\n"
271             + "  <title>page 1</title>\n"
272             + "  <script src='foo1.js'></script>\n"
273             + "  <script src='foo2.js?foo[1]=bar/baz'></script>\n"
274             + "</head>\n"
275             + "<body>\n"
276             + "  <a href='page2.html'>to page 2</a>\n"
277             + "</body>\n"
278             + "</html>";
279 
280         final String content2 = DOCTYPE_HTML
281             + "<html>\n"
282             + "<head>\n"
283             + "  <title>page 2</title>\n"
284             + "  <script src='foo2.js?foo[1]=bar/baz'></script>\n"
285             + "</head>\n"
286             + "<body>\n"
287             + "  <a href='page1.html'>to page 1</a>\n"
288             + "</body>\n"
289             + "</html>";
290 
291         final String script1 = "alert('in foo1');";
292         final String script2 = "alert('in foo2');";
293 
294         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
295         getMockWebConnection().setResponse(urlPage1, content);
296         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
297         getMockWebConnection().setResponse(urlPage2, content2);
298 
299         final List<NameValuePair> headers = new ArrayList<>();
300         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
301         getMockWebConnection().setResponse(new URL(URL_FIRST, "foo1.js"), script1,
302                 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
303         getMockWebConnection().setDefaultResponse(script2, 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
304 
305         final WebClient webClient = getWebClientWithMockWebConnection();
306 
307         final List<String> collectedAlerts = new ArrayList<>();
308         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
309 
310         final HtmlPage page1 = webClient.getPage(urlPage1);
311         final String[] expectedAlerts = {"in foo1", "in foo2"};
312         assertEquals(expectedAlerts, collectedAlerts);
313 
314         collectedAlerts.clear();
315         page1.getAnchors().get(0).click();
316 
317         assertEquals(new String[] {"in foo2"}, collectedAlerts);
318         assertEquals("no request for scripts should have been performed",
319                 urlPage2, getMockWebConnection().getLastWebRequest().getUrl());
320     }
321 
322     /**
323      * @throws Exception if the test fails
324      */
325     @Test
326     public void cssUrlEncoded() throws Exception {
327         final String content = DOCTYPE_HTML
328             + "<html>\n"
329             + "<head>\n"
330             + "  <title>page 1</title>\n"
331             + "  <link href='foo1.css' type='text/css' rel='stylesheet'>\n"
332             + "  <link href='foo2.js?foo[1]=bar/baz' type='text/css' rel='stylesheet'>\n"
333             + "</head>\n"
334             + "<body>\n"
335             + "  <a href='page2.html'>to page 2</a>\n"
336             + "  <script>\n"
337             + "    var sheets = document.styleSheets;\n"
338             + "    alert(sheets.length);\n"
339             + "    var rules = sheets[0].cssRules || sheets[0].rules;\n"
340             + "    alert(rules.length);\n"
341             + "    rules = sheets[1].cssRules || sheets[1].rules;\n"
342             + "    alert(rules.length);\n"
343             + "  </script>\n"
344             + "</body>\n"
345             + "</html>";
346 
347         final String content2 = DOCTYPE_HTML
348             + "<html>\n"
349             + "<head>\n"
350             + "  <title>page 2</title>\n"
351             + "  <link href='foo2.js?foo[1]=bar/baz' type='text/css' rel='stylesheet'>\n"
352             + "</head>\n"
353             + "<body>\n"
354             + "  <a href='page1.html'>to page 1</a>\n"
355             + "  <script>\n"
356             + "    var sheets = document.styleSheets;\n"
357             + "    alert(sheets.length);\n"
358             + "    var rules = sheets[0].cssRules || sheets[0].rules;\n"
359             + "    alert(rules.length);\n"
360             + "  </script>\n"
361             + "</body>\n"
362             + "</html>";
363 
364         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
365         getMockWebConnection().setResponse(urlPage1, content);
366         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
367         getMockWebConnection().setResponse(urlPage2, content2);
368 
369         final List<NameValuePair> headers = new ArrayList<>();
370         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
371         getMockWebConnection().setResponse(new URL(URL_FIRST, "foo1.js"), "",
372                 200, "ok", MimeType.TEXT_CSS, headers);
373         getMockWebConnection().setDefaultResponse("", 200, "ok", MimeType.TEXT_CSS, headers);
374 
375         final WebClient webClient = getWebClientWithMockWebConnection();
376 
377         final List<String> collectedAlerts = new ArrayList<>();
378         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
379 
380         final HtmlPage page1 = webClient.getPage(urlPage1);
381         final String[] expectedAlerts = {"2", "0", "0"};
382         assertEquals(expectedAlerts, collectedAlerts);
383         assertEquals(3, getMockWebConnection().getRequestCount());
384 
385         collectedAlerts.clear();
386         page1.getAnchors().get(0).click();
387 
388         assertEquals(new String[] {"1", "0"}, collectedAlerts);
389         assertEquals(4, getMockWebConnection().getRequestCount());
390         assertEquals("no request for scripts should have been performed",
391                 urlPage2, getMockWebConnection().getLastWebRequest().getUrl());
392     }
393 
394     /**
395      * @throws Exception if the test fails
396      */
397     @Test
398     public void maxSizeMaintained() throws Exception {
399         final String html = DOCTYPE_HTML
400             + "<html><head><title>page 1</title>\n"
401             + "<script src='foo1.js' type='text/javascript'/>\n"
402             + "<script src='foo2.js' type='text/javascript'/>\n"
403             + "</head><body>abc</body></html>";
404 
405         final WebClient client = getWebClient();
406         client.getCache().setMaxSize(1);
407 
408         final MockWebConnection connection = new MockWebConnection();
409         client.setWebConnection(connection);
410 
411         final URL pageUrl = new URL(URL_FIRST, "page1.html");
412         connection.setResponse(pageUrl, html);
413 
414         final List<NameValuePair> headers =
415             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
416         connection.setResponse(new URL(URL_FIRST, "foo1.js"), ";", 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
417         connection.setResponse(new URL(URL_FIRST, "foo2.js"), ";", 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
418 
419         client.getPage(pageUrl);
420         assertEquals(1, client.getCache().getSize());
421 
422         client.getCache().clear();
423         assertEquals(0, client.getCache().getSize());
424     }
425 
426     /**
427      * TODO: improve CSS caching to cache a COPY of the object as stylesheet objects can be modified dynamically.
428      * @throws Exception if the test fails
429      */
430     @Test
431     public void cssIsCached() throws Exception {
432         final String html = DOCTYPE_HTML
433             + "<html><head><title>page 1</title>\n"
434             + "<style>.x { color: red; }</style>\n"
435             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
436             + "</head>\n"
437             + "<body onload='document.styleSheets.item(0); document.styleSheets.item(1);'>x</body>\n"
438             + "</html>";
439 
440         final WebClient client = getWebClient();
441 
442         final MockWebConnection connection = new MockWebConnection();
443         client.setWebConnection(connection);
444 
445         final URL pageUrl = new URL(URL_FIRST, "page1.html");
446         connection.setResponse(pageUrl, html);
447 
448         final List<NameValuePair> headers =
449             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
450         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_CSS, headers);
451 
452         client.getPage(pageUrl);
453         assertEquals(2, client.getCache().getSize());
454     }
455 
456     /**
457      * Check for correct caching if the css request gets redirected.
458      *
459      * @throws Exception if the test fails
460      */
461     @Test
462     public void cssIsCachedIfUrlWasRedirected() throws Exception {
463         final String html = DOCTYPE_HTML
464             + "<html><head><title>page 1</title>\n"
465             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
466             + "</head>\n"
467             + "<body onload='document.styleSheets.item(0); document.styleSheets.item(1);'>x</body>\n"
468             + "</html>";
469 
470         final String css = ".x { color: red; }";
471 
472         final WebClient client = getWebClient();
473 
474         final MockWebConnection connection = new MockWebConnection();
475         client.setWebConnection(connection);
476 
477         final URL pageUrl = new URL(URL_FIRST, "page1.html");
478         connection.setResponse(pageUrl, html);
479 
480         final URL cssUrl = new URL(URL_FIRST, "foo.css");
481         final URL redirectUrl = new URL(URL_FIRST, "fooContent.css");
482 
483         List<NameValuePair> headers = new ArrayList<>();
484         headers.add(new NameValuePair("Location", redirectUrl.toExternalForm()));
485         connection.setResponse(cssUrl, "", 301, "Redirect", null, headers);
486 
487         headers = Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
488         connection.setResponse(redirectUrl, css, 200, "OK", MimeType.TEXT_CSS, headers);
489 
490         client.getPage(pageUrl);
491         client.getPage(pageUrl);
492 
493         // page1.html - foo.css - fooContent.css - page1.html
494         assertEquals(4, connection.getRequestCount());
495         // foo.css - fooContent.css
496         assertEquals(2, client.getCache().getSize());
497     }
498 
499     /**
500      * @throws Exception if the test fails
501      */
502     @Test
503     public void cssFromCacheIsUsed() throws Exception {
504         final String html = DOCTYPE_HTML
505             + "<html><head><title>page 1</title>\n"
506             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
507             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
508             + "</head>\n"
509             + "<body>x</body>\n"
510             + "</html>";
511 
512         final String css = ".x { color: red; }";
513 
514         final WebClient client = getWebClient();
515 
516         final MockWebConnection connection = new MockWebConnection();
517         client.setWebConnection(connection);
518 
519         final URL pageUrl = new URL(URL_FIRST, "page1.html");
520         connection.setResponse(pageUrl, html);
521 
522         final URL cssUrl = new URL(URL_FIRST, "foo.css");
523         final List<NameValuePair> headers = new ArrayList<>();
524         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
525         connection.setResponse(cssUrl, css, 200, "OK", MimeType.TEXT_CSS, headers);
526 
527         client.getPage(pageUrl);
528 
529         assertEquals(2, connection.getRequestCount());
530         assertEquals(1, client.getCache().getSize());
531     }
532 
533     /**
534      * @throws Exception if the test fails
535      */
536     @Test
537     public void cssManuallyAddeToCache() throws Exception {
538         final String html = DOCTYPE_HTML
539             + "<html><head>\n"
540             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
541             + "</head>\n"
542             + "<body>\n"
543             + "abc <div class='test'>def</div>\n"
544             + "</body>\n"
545             + "</html>";
546 
547         final String css = ".test { visibility: hidden; }";
548 
549         final WebClient client = getWebClient();
550 
551         final MockWebConnection connection = new MockWebConnection();
552         client.setWebConnection(connection);
553 
554         final URL pageUrl = new URL(URL_FIRST, "page1.html");
555         connection.setResponse(pageUrl, html);
556 
557         final URL cssUrl = new URL(URL_FIRST, "foo.css");
558         final List<NameValuePair> headers = new ArrayList<>();
559         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
560         final WebRequest request = new WebRequest(cssUrl);
561         final WebResponseData data = new WebResponseData(css.getBytes("UTF-8"),
562                 HttpStatus.OK_200, HttpStatus.OK_200_MSG, headers);
563         final WebResponse response = new WebResponse(data, request, 100);
564         client.getCache().cacheIfPossible(new WebRequest(cssUrl), response, headers);
565 
566         final HtmlPage page = client.getPage(pageUrl);
567         assertEquals("abc", page.asNormalizedText());
568 
569         assertEquals(1, connection.getRequestCount());
570         assertEquals(1, client.getCache().getSize());
571     }
572 
573     /**
574      * Test that content retrieved with XHR is cached when right headers are here.
575      * @throws Exception if the test fails
576      */
577     @Test
578     @Alerts({"hello", "hello"})
579     public void xhrContentCached() throws Exception {
580         final String html = DOCTYPE_HTML
581             + "<html><head><title>page 1</title>\n"
582             + "<script>\n"
583             + "  function doTest() {\n"
584             + "    var xhr = new XMLHttpRequest();\n"
585             + "    xhr.open('GET', 'foo.txt', false);\n"
586             + "    xhr.send('');\n"
587             + "    alert(xhr.responseText);\n"
588             + "    xhr.send('');\n"
589             + "    alert(xhr.responseText);\n"
590             + "  }\n"
591             + "</script>\n"
592             + "</head>\n"
593             + "<body onload='doTest()'>x</body>\n"
594             + "</html>";
595 
596         final MockWebConnection connection = getMockWebConnection();
597 
598         final List<NameValuePair> headers =
599             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
600         connection.setResponse(new URL(URL_FIRST, "foo.txt"), "hello", 200, "OK", MimeType.TEXT_PLAIN, headers);
601 
602         loadPageWithAlerts(html);
603 
604         assertEquals(2, connection.getRequestCount());
605     }
606 
607     /**
608      * @throws Exception if the test fails
609      */
610     @Test
611     public void testNoStoreCacheControl() throws Exception {
612         final String html = DOCTYPE_HTML
613             + "<html><head><title>page 1</title>\n"
614             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
615             + "</head>\n"
616             + "<body>x</body>\n"
617             + "</html>";
618 
619         final WebClient client = getWebClient();
620 
621         final MockWebConnection connection = new MockWebConnection();
622         client.setWebConnection(connection);
623 
624         final List<NameValuePair> headers = new ArrayList<>();
625         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-store"));
626 
627         final URL pageUrl = new URL(URL_FIRST, "page1.html");
628         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
629         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
630 
631         client.getPage(pageUrl);
632         assertEquals(0, client.getCache().getSize());
633         assertEquals(2, connection.getRequestCount());
634 
635         client.getPage(pageUrl);
636         assertEquals(0, client.getCache().getSize());
637         assertEquals(4, connection.getRequestCount());
638     }
639 
640     /**
641      * @throws Exception if an error occurs
642      */
643     @Test
644     public void testNoCacheCacheControl() throws Exception {
645         final String html = DOCTYPE_HTML
646                 + "<html><head><title>page 1</title>\n"
647                 + "</head>\n"
648                 + "<body>x</body>\n"
649                 + "</html>";
650 
651         final WebClient client = getWebClient();
652 
653         final MockWebConnection connection = new MockWebConnection();
654         client.setWebConnection(connection);
655 
656         final String date = "Thu, 02 Mar 2023 02:00:00 GMT";
657         final String etag = "foo";
658         final String lastModified = "Wed, 01 Mar 2023 01:00:00 GMT";
659 
660         final List<NameValuePair> headers = new ArrayList<>();
661         headers.add(new NameValuePair("Date", date));
662         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-cache"));
663         headers.add(new NameValuePair(ETAG, etag));
664         headers.add(new NameValuePair(LAST_MODIFIED, lastModified));
665 
666         final URL pageUrl = new URL(URL_FIRST, "page1.html");
667         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
668 
669         client.getPage(pageUrl);
670         assertEquals(1, client.getCache().getSize());
671 
672         final String updatedDate = "Thu, 02 Mar 2023 02:00:10 GMT";
673         final List<NameValuePair> headers2 = new ArrayList<>();
674         headers2.add(new NameValuePair("Date", updatedDate));
675         headers2.add(new NameValuePair("Proxy-Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"));
676         headers2.add(new NameValuePair("X-Content-Type-Options", "nosniff"));
677         connection.setResponse(pageUrl, html, 304, "Not Modified", "text/html;charset=ISO-8859-1", headers2);
678 
679         client.getPage(pageUrl);
680         assertEquals(2, connection.getRequestCount());
681 
682         final WebRequest lastRequest = connection.getLastWebRequest();
683         assertEquals(etag, lastRequest.getAdditionalHeader(IF_NONE_MATCH));
684         assertEquals(lastModified, lastRequest.getAdditionalHeader(IF_MODIFIED_SINCE));
685         assertEquals(1, client.getCache().getSize());
686 
687         WebResponse cached = client.getCache().getCachedResponse(connection.getLastWebRequest());
688         assertEquals(updatedDate, cached.getResponseHeaderValue("Date"));
689         assertEquals(null, cached.getResponseHeaderValue("Proxy-Authorization"));
690         assertEquals(null, cached.getResponseHeaderValue("X-Content-Type-Options"));
691 
692         final String updatedEtag = "bar";
693         final String updatedLastModified = "Wed, 01 Mar 2023 02:00:00 GMT";
694 
695         final List<NameValuePair> headers3 = new ArrayList<>();
696         headers3.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-cache"));
697         headers3.add(new NameValuePair(ETAG, updatedEtag));
698         headers3.add(new NameValuePair(LAST_MODIFIED, updatedLastModified));
699         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers3);
700 
701         client.getPage(pageUrl);
702         assertEquals(3, connection.getRequestCount());
703         assertEquals(1, client.getCache().getSize());
704 
705         cached = client.getCache().getCachedResponse(connection.getLastWebRequest());
706         assertEquals(null, cached.getResponseHeaderValue("Date"));
707         assertEquals(updatedEtag, cached.getResponseHeaderValue(ETAG));
708         assertEquals(updatedLastModified, cached.getResponseHeaderValue(LAST_MODIFIED));
709     }
710 
711     /**
712      * @throws Exception if the test fails
713      */
714     @Test
715     public void testMaxAgeCacheControl() throws Exception {
716         final String html = DOCTYPE_HTML
717             + "<html><head><title>page 1</title>\n"
718             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
719             + "</head>\n"
720             + "<body>x</body>\n"
721             + "</html>";
722 
723         final WebClient client = getWebClient();
724 
725         final MockWebConnection connection = new MockWebConnection();
726         client.setWebConnection(connection);
727 
728         final List<NameValuePair> headers = new ArrayList<>();
729         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
730         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, max-age=1"));
731 
732         final URL pageUrl = new URL(URL_FIRST, "page1.html");
733         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
734         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
735 
736         client.getPage(pageUrl);
737         assertEquals(2, client.getCache().getSize());
738         assertEquals(2, connection.getRequestCount());
739         // resources should be still in cache
740         client.getPage(pageUrl);
741         assertEquals(2, client.getCache().getSize());
742         assertEquals(2, connection.getRequestCount());
743         // wait for max-age seconds + 1 for recache
744         Thread.sleep(2 * 1000);
745         client.getPage(pageUrl);
746         assertEquals(2, client.getCache().getSize());
747         assertEquals(4, connection.getRequestCount());
748 
749         // wait for max-age seconds + 1 for recache
750         Thread.sleep(2 * 1000);
751         client.getCache().clearOutdated();
752         assertEquals(0, client.getCache().getSize());
753         assertEquals(4, connection.getRequestCount());
754     }
755 
756     /**
757      * @throws Exception if the test fails
758      */
759     @Test
760     public void testSMaxageCacheControl() throws Exception {
761         final String html = DOCTYPE_HTML
762             + "<html><head><title>page 1</title>\n"
763             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
764             + "</head>\n"
765             + "<body>x</body>\n"
766             + "</html>";
767 
768         final WebClient client = getWebClient();
769 
770         final MockWebConnection connection = new MockWebConnection();
771         client.setWebConnection(connection);
772 
773         final List<NameValuePair> headers = new ArrayList<>();
774         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
775         headers.add(new NameValuePair(CACHE_CONTROL, "public, s-maxage=1, some-other-value, max-age=10"));
776 
777         final URL pageUrl = new URL(URL_FIRST, "page1.html");
778         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
779         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
780 
781         client.getPage(pageUrl);
782         assertEquals(2, client.getCache().getSize());
783         assertEquals(2, connection.getRequestCount());
784         // resources should be still in cache
785         client.getPage(pageUrl);
786         assertEquals(2, client.getCache().getSize());
787         assertEquals(2, connection.getRequestCount());
788         // wait for s-maxage seconds + 1 for recache
789         Thread.sleep(2 * 1000);
790         client.getPage(pageUrl);
791         assertEquals(2, client.getCache().getSize());
792         assertEquals(4, connection.getRequestCount());
793 
794         // wait for s-maxage seconds + 1 for recache
795         Thread.sleep(2 * 1000);
796         client.getCache().clearOutdated();
797         assertEquals(0, client.getCache().getSize());
798         assertEquals(4, connection.getRequestCount());
799     }
800 
801     /**
802      * @throws Exception if the test fails
803      */
804     @Test
805     public void testExpiresCacheControl() throws Exception {
806         final String html = DOCTYPE_HTML
807             + "<html><head><title>page 1</title>\n"
808             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
809             + "</head>\n"
810             + "<body>x</body>\n"
811             + "</html>";
812 
813         final WebClient client = getWebClient();
814 
815         final MockWebConnection connection = new MockWebConnection();
816         client.setWebConnection(connection);
817 
818         final List<NameValuePair> headers = new ArrayList<>();
819         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
820         final Date expi = new Date(System.currentTimeMillis() + 2 * 1000 + 10 * DateUtils.MILLIS_PER_MINUTE);
821         headers.add(new NameValuePair(EXPIRES, new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(expi)));
822         headers.add(new NameValuePair(CACHE_CONTROL, "public, some-other-value"));
823 
824         final URL pageUrl = new URL(URL_FIRST, "page1.html");
825         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
826         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
827 
828         client.getPage(pageUrl);
829         assertEquals(2, client.getCache().getSize());
830         assertEquals(2, connection.getRequestCount());
831         // resources should be still in cache
832         client.getPage(pageUrl);
833         assertEquals(2, client.getCache().getSize());
834         assertEquals(2, connection.getRequestCount());
835         // wait for expires
836         Thread.sleep(2 * 1000);
837         client.getPage(pageUrl);
838         assertEquals(0, client.getCache().getSize());
839         assertEquals(4, connection.getRequestCount());
840     }
841 
842     /**
843      * @throws Exception if the test fails
844      */
845     @Test
846     public void testMaxAgeOverrulesExpiresCacheControl() throws Exception {
847         final String html = DOCTYPE_HTML
848             + "<html><head><title>page 1</title>\n"
849             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
850             + "</head>\n"
851             + "<body>x</body>\n"
852             + "</html>";
853 
854         final WebClient client = getWebClient();
855 
856         final MockWebConnection connection = new MockWebConnection();
857         client.setWebConnection(connection);
858 
859         final List<NameValuePair> headers = new ArrayList<>();
860         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
861         headers.add(new NameValuePair(EXPIRES, "0"));
862         headers.add(new NameValuePair(CACHE_CONTROL, "max-age=20"));
863 
864         final URL pageUrl = new URL(URL_FIRST, "page1.html");
865         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
866         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
867 
868         client.getPage(pageUrl);
869         assertEquals(2, client.getCache().getSize());
870         assertEquals(2, connection.getRequestCount());
871         // resources should be still in cache
872         client.getPage(pageUrl);
873         assertEquals(2, client.getCache().getSize());
874         assertEquals(2, connection.getRequestCount());
875     }
876 
877     /**
878      * Ensures {@link WebResponse#cleanUp()} is called for overflow deleted entries.
879      * @throws Exception if the test fails
880      */
881     @Test
882     public void cleanUpOverflow() throws Exception {
883         final WebRequest request1 = new WebRequest(URL_FIRST, HttpMethod.GET);
884 
885         final Map<String, String> headers = new HashMap<>();
886         headers.put(HttpHeader.EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
887 
888         final WebResponseMock response1 = new WebResponseMock(request1, headers);
889 
890         final WebRequest request2 = new WebRequest(URL_SECOND, HttpMethod.GET);
891         final WebResponseMock response2 = new WebResponseMock(request2, headers);
892 
893         final Cache cache = new Cache();
894         cache.setMaxSize(1);
895         cache.cacheIfPossible(request1, response1, null);
896         assertEquals(0, response1.getCallCount("cleanUp"));
897         assertEquals(0, response2.getCallCount("cleanUp"));
898         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
899         assertEquals(0, response2.getCallCount("getResponseHeaderValue"));
900 
901         Thread.sleep(10);
902         cache.cacheIfPossible(request2, response2, null);
903         assertEquals(1, response1.getCallCount("cleanUp"));
904         assertEquals(0, response2.getCallCount("cleanUp"));
905         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
906         assertEquals(6, response2.getCallCount("getResponseHeaderValue"));
907     }
908 
909     /**
910      * Ensures {@link WebResponse#cleanUp()} is called on calling {@link Cache#clear()}.
911      */
912     @Test
913     public void cleanUpOnClear() {
914         final WebRequest request1 = new WebRequest(URL_FIRST, HttpMethod.GET);
915 
916         final Map<String, String> headers = new HashMap<>();
917         headers.put(HttpHeader.EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
918 
919         final WebResponseMock response1 = new WebResponseMock(request1, headers);
920 
921         final Cache cache = new Cache();
922         cache.cacheIfPossible(request1, response1, null);
923         assertEquals(0, response1.getCallCount("cleanUp"));
924         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
925 
926         cache.clear();
927 
928         assertEquals(1, response1.getCallCount("cleanUp"));
929         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
930     }
931 }