From: <rb...@us...> - 2018-02-24 20:35:53
|
Revision: 15136 http://sourceforge.net/p/htmlunit/code/15136 Author: rbri Date: 2018-02-20 19:16:28 +0000 (Tue, 20 Feb 2018) Log Message: ----------- page cache now takes care of Cache Control headers Issue 1952 Modified Paths: -------------- trunk/htmlunit/src/changes/changes.xml trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/Cache.java trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/History.java trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/HttpHeader.java trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/CacheTest.java trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/History2Test.java Added Paths: ----------- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/util/HeaderUtils.java Modified: trunk/htmlunit/src/changes/changes.xml =================================================================== --- trunk/htmlunit/src/changes/changes.xml 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/changes/changes.xml 2018-02-20 19:16:28 UTC (rev 15136) @@ -8,6 +8,9 @@ <body> <release version="2.30" date="xx, 2018" description="Bugfixes, URLSearchParams implemented, start adding support of user defined iterators, CHROME 64"> + <action type="add" dev="rbri" issue="1952" due-to="Anton Demydenko"> + Our page cache now takes care of Cache Control headers. + </action> <action type="fix" dev="rbri" issue="1951" due-to="Hartmut Arlt"> Fix incorrect encoding of consecutive '%' characters in url's </action> Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/Cache.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/Cache.java 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/Cache.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -26,6 +26,7 @@ import org.apache.http.client.utils.DateUtils; import org.w3c.dom.css.CSSStyleSheet; +import com.gargoylesoftware.htmlunit.util.HeaderUtils; import com.gargoylesoftware.htmlunit.util.UrlUtils; /** @@ -36,6 +37,7 @@ * @author Marc Guillemot * @author Daniel Gredler * @author Ahmed Ashour + * @author Anton Demydenko */ public class Cache implements Serializable { @@ -43,7 +45,7 @@ private int maxSize_ = 40; private static final Pattern DATE_HEADER_PATTERN = Pattern.compile("-?\\d+"); - + private static final long DELAY = 10 * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_MINUTE; /** * The map which holds the cached responses. Note that when keying on URLs, we key on the string version * of the URLs, rather than on the URLs themselves. This is done for performance, because a) the @@ -61,12 +63,14 @@ private WebResponse response_; private Object value_; private long lastAccess_; + private long createdAt_; Entry(final String key, final WebResponse response, final Object value) { key_ = key; response_ = response; value_ = value; - lastAccess_ = System.currentTimeMillis(); + createdAt_ = System.currentTimeMillis(); + lastAccess_ = createdAt_; } /** @@ -178,6 +182,8 @@ } /** + * <p>Perform prior validation for 'no-store' directive in Cache-Control header.</p> + * * <p>Tries to guess if the content is dynamic or not.</p> * * <p>"Since origin servers do not always provide explicit expiration times, HTTP caches typically @@ -188,19 +194,23 @@ * <tt>Last-Modified</tt> header with a date older than 10 minutes or with an <tt>Expires</tt> header * specifying expiration in more than 10 minutes.</p> * + * @see @see <a href="https://tools.ietf.org/html/rfc7234">RFC 7234</a> * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html">RFC 2616</a> * @param response the response to examine * @return {@code true} if the response should be considered as cacheable */ protected boolean isCacheableContent(final WebResponse response) { - final Date lastModified = parseDateHeader(response, "Last-Modified"); - final Date expires = parseDateHeader(response, "Expires"); + if (HeaderUtils.containsNoStore(response)) { + return false; + } - final long delay = 10 * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_MINUTE; + final Date lastModified = parseDateHeader(response, HttpHeader.LAST_MODIFIED); + final Date expires = parseDateHeader(response, HttpHeader.EXPIRES); + final long now = getCurrentTimestamp(); - return expires != null && (expires.getTime() - now > delay) - || (expires == null && lastModified != null && now - lastModified.getTime() > delay); + return expires != null && (expires.getTime() - now > DELAY) + || (expires == null && lastModified != null && now - lastModified.getTime() > DELAY); } /** @@ -235,6 +245,9 @@ * Returns the cached response corresponding to the specified request. If there is * no corresponding cached object, this method returns {@code null}. * + * <p>Calculates and check if object still fresh(RFC 7234) otherwise returns {@code null}.</p> + * @see <a href="https://tools.ietf.org/html/rfc7234">RFC 7234</a> + * * @param request the request whose corresponding response is sought * @return the cached response corresponding to the specified request if any */ @@ -250,6 +263,9 @@ * Returns the cached object corresponding to the specified request. If there is * no corresponding cached object, this method returns {@code null}. * + * <p>Calculates and check if object still fresh(RFC 7234) otherwise returns {@code null}.</p> + * @see <a href="https://tools.ietf.org/html/rfc7234">RFC 7234</a> + * * @param request the request whose corresponding cached compiled script is sought * @return the cached object corresponding to the specified request if any */ @@ -274,13 +290,58 @@ if (cachedEntry == null) { return null; } - synchronized (entries_) { - cachedEntry.touch(); + + // check if object still fresh + if (checkFreshness(cachedEntry.response_, cachedEntry.createdAt_)) { + synchronized (entries_) { + cachedEntry.touch(); + } + return cachedEntry; } - return cachedEntry; + else { + entries_.remove(UrlUtils.normalize(url)); + } + return null; } /** + * <p>Check freshness return value if + * a) s-maxage specified + * b) max-age specified + * c) expired specified + * otherwise return {@code null}</p> + * + * @see <a href="https://tools.ietf.org/html/rfc7234">RFC 7234</a> + * + * @param response + * @param createdAt + * @return freshnessLifetime + */ + private boolean checkFreshness(final WebResponse response, final long createdAt) { + final long now = getCurrentTimestamp(); + long freshnessLifetime = 0; + if (!HeaderUtils.containsPrivate(response) && HeaderUtils.containsSMaxage(response)) { + // check s-maxage + freshnessLifetime = HeaderUtils.sMaxage(response); + } + else if (HeaderUtils.containsMaxAge(response)) { + // check max-age + freshnessLifetime = HeaderUtils.maxAge(response); + } + else if (response.getResponseHeaderValue(HttpHeader.EXPIRES) != null) { + final Date expires = parseDateHeader(response, HttpHeader.EXPIRES); + if (expires != null) { + // use the same logic as in isCacheableContent() + return expires.getTime() - now > DELAY; + } + } + else { + return true; + } + return now - createdAt < freshnessLifetime * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_SECOND; + } + + /** * Returns the cached parsed version of the specified CSS snippet. If there is no * corresponding cached stylesheet, this method returns {@code null}. * @@ -344,5 +405,4 @@ entries_.clear(); } } - } Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/History.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/History.java 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/History.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -25,6 +25,7 @@ import com.gargoylesoftware.htmlunit.javascript.host.Window; import com.gargoylesoftware.htmlunit.javascript.host.event.Event; import com.gargoylesoftware.htmlunit.javascript.host.event.PopStateEvent; +import com.gargoylesoftware.htmlunit.util.HeaderUtils; import com.gargoylesoftware.htmlunit.util.UrlUtils; /** @@ -47,8 +48,15 @@ private Object state_; private HistoryEntry(final Page page) { - page_ = new SoftReference<>(page); + // verify cache-control header values before storing + if (HeaderUtils.containsNoStore(page.getWebResponse())) { + page_ = new SoftReference<>(page); + } + else { + page_ = null; + } + final WebRequest request = page.getWebResponse().getWebRequest(); webRequest_ = new WebRequest(request.getUrl(), request.getHttpMethod()); webRequest_.setRequestParameters(request.getRequestParameters()); @@ -257,6 +265,7 @@ if (entries_.size() > cacheLimit) { entries_.get(entries_.size() - cacheLimit - 1).clearPage(); } + return entry; } Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/HttpHeader.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/HttpHeader.java 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/HttpHeader.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -18,6 +18,7 @@ * Various constants. * * @author Ronald Brill + * @author Anton Demydenko */ public final class HttpHeader { @@ -34,6 +35,12 @@ /** Cache-Control. */ public static final String CACHE_CONTROL = "Cache-Control"; + /** Last-Modified. */ + public static final String LAST_MODIFIED = "Last-Modified"; + + /** Expires. */ + public static final String EXPIRES = "Expires"; + /** Accept. */ public static final String ACCEPT = "Accept"; /** accept. */ Added: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/util/HeaderUtils.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/util/HeaderUtils.java (rev 0) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/util/HeaderUtils.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2002-2018 Gargoyle Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gargoylesoftware.htmlunit.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +import com.gargoylesoftware.htmlunit.HttpHeader; +import com.gargoylesoftware.htmlunit.WebResponse; + +/** + * @author Anton Demydenko + */ +public final class HeaderUtils { + + private static final String CACHE_CONTROL_PRIVATE = "private"; + private static final String CACHE_CONTROL_PUBLIC = "public"; + private static final String CACHE_CONTROL_NO_STORE = "no-store"; + private static final String CACHE_CONTROL_NO_CACHE = "no-cache"; + private static final String CACHE_CONTROL_MAX_AGE = "max-age"; + private static final String CACHE_CONTROL_S_MAXAGE = "s-maxage"; + private static final Pattern MAX_AGE_HEADER_PATTERN = Pattern.compile("^.*max-age=([\\d]+).*$"); + private static final Pattern S_MAXAGE_HEADER_PATTERN = Pattern.compile("^.*s-maxage=([\\d]+).*$"); + + private HeaderUtils() { + // utility class + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 'private' value + */ + public static boolean containsPrivate(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_PRIVATE); + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 'public' value + */ + public static boolean containsPublic(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_PUBLIC); + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 'no-store' value + */ + public static boolean containsNoStore(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_NO_STORE); + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 'no-cache' value@return + */ + public static boolean containsNoCache(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_NO_CACHE); + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 's-maxage' value + */ + public static boolean containsSMaxage(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_S_MAXAGE); + } + + /** + * @param response {@code WebResponse} + * @return if 'Cache-Control' header is present and contains 'max-age' value + */ + public static boolean containsMaxAge(final WebResponse response) { + return containsValue(response, CACHE_CONTROL_MAX_AGE); + } + + /** + * @param response {@code WebResponse} + * @return value of 's-maxage' directive and 0 if it is absent + */ + public static long sMaxage(final WebResponse response) { + if (containsValue(response, CACHE_CONTROL_S_MAXAGE)) { + return directiveValue(response, S_MAXAGE_HEADER_PATTERN); + } + return 0; + } + + /** + * @param response {@code WebResponse} + * @return value of 'max-age' directive and 0 if it is absent + */ + public static long maxAge(final WebResponse response) { + if (containsValue(response, CACHE_CONTROL_MAX_AGE)) { + return directiveValue(response, MAX_AGE_HEADER_PATTERN); + } + + return 0; + } + + private static long directiveValue(final WebResponse response, final Pattern pattern) { + final String value = response.getResponseHeaderValue(HttpHeader.CACHE_CONTROL); + if (value != null) { + final Matcher matcher = pattern.matcher(value); + if (matcher.matches()) { + return Long.valueOf(matcher.group(1)); + } + } + + return 0; + } + + private static boolean containsValue(final WebResponse response, final String value) { + final String cacheControl = response.getResponseHeaderValue(HttpHeader.CACHE_CONTROL); + return StringUtils.contains(cacheControl, value); + } +} Modified: trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/CacheTest.java =================================================================== --- trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/CacheTest.java 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/CacheTest.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -24,6 +24,7 @@ import java.io.InputStream; import java.net.URL; import java.nio.charset.Charset; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -46,6 +47,7 @@ * @author Marc Guillemot * @author Ahmed Ashour * @author Frank Danek + * @author Anton Demydenko */ @RunWith(BrowserRunner.class) public class CacheTest extends SimpleWebTestCase { @@ -86,6 +88,9 @@ headers.put("Expires", "-1"); assertFalse(cache.isCacheableContent(response)); + + headers.put("Cache-Control", "no-store"); + assertFalse(cache.isCacheableContent(response)); } /** @@ -359,6 +364,154 @@ } /** + * @throws Exception if the test fails + */ + @Test + public void testNoStoreCacheControl() throws Exception { + final String html = "<html><head><title>page 1</title>\n" + + "<link rel='stylesheet' type='text/css' href='foo.css' />\n" + + "</head>\n" + + "<body>x</body>\n" + + "</html>"; + + final WebClient client = getWebClient(); + + final MockWebConnection connection = new MockWebConnection(); + client.setWebConnection(connection); + + final List<NameValuePair> headers = new ArrayList<>(); + headers.add(new NameValuePair("Cache-Control", "some-other-value, no-store")); + + final URL pageUrl = new URL(URL_FIRST, "page1.html"); + connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers); + connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", JAVASCRIPT_MIME_TYPE, headers); + + client.getPage(pageUrl); + assertEquals(0, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + + client.getPage(pageUrl); + assertEquals(0, client.getCache().getSize()); + assertEquals(4, connection.getRequestCount()); + } + + /** + * @throws Exception if the test fails + */ + @Test + public void testMaxAgeCacheControl() throws Exception { + final String html = "<html><head><title>page 1</title>\n" + + "<link rel='stylesheet' type='text/css' href='foo.css' />\n" + + "</head>\n" + + "<body>x</body>\n" + + "</html>"; + + final WebClient client = getWebClient(); + + final MockWebConnection connection = new MockWebConnection(); + client.setWebConnection(connection); + + final List<NameValuePair> headers = new ArrayList<>(); + headers.add(new NameValuePair("Last-Modified", "Tue, 20 Feb 2018 10:00:00 GMT")); + headers.add(new NameValuePair("Cache-Control", "some-other-value, max-age=1")); + + final URL pageUrl = new URL(URL_FIRST, "page1.html"); + connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers); + connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", JAVASCRIPT_MIME_TYPE, headers); + + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // resources should be still in cache + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // wait for max-age seconds + 1 for recache + Thread.sleep(2 * 1000); + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(4, connection.getRequestCount()); + } + + /** + * @throws Exception if the test fails + */ + @Test + public void testSMaxageCacheControl() throws Exception { + final String html = "<html><head><title>page 1</title>\n" + + "<link rel='stylesheet' type='text/css' href='foo.css' />\n" + + "</head>\n" + + "<body>x</body>\n" + + "</html>"; + + final WebClient client = getWebClient(); + + final MockWebConnection connection = new MockWebConnection(); + client.setWebConnection(connection); + + final List<NameValuePair> headers = new ArrayList<>(); + headers.add(new NameValuePair("Last-Modified", "Tue, 20 Feb 2018 10:00:00 GMT")); + headers.add(new NameValuePair("Cache-Control", "public, s-maxage=1, some-other-value, max-age=10")); + + final URL pageUrl = new URL(URL_FIRST, "page1.html"); + connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers); + connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", JAVASCRIPT_MIME_TYPE, headers); + + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // resources should be still in cache + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // wait for s-maxage seconds + 1 for recache + Thread.sleep(2 * 1000); + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(4, connection.getRequestCount()); + } + + /** + * @throws Exception if the test fails + */ + @Test + public void testExpiresCacheControl() throws Exception { + final String html = "<html><head><title>page 1</title>\n" + + "<link rel='stylesheet' type='text/css' href='foo.css' />\n" + + "</head>\n" + + "<body>x</body>\n" + + "</html>"; + + final WebClient client = getWebClient(); + + final MockWebConnection connection = new MockWebConnection(); + client.setWebConnection(connection); + + final List<NameValuePair> headers = new ArrayList<>(); + headers.add(new NameValuePair("Last-Modified", "Tue, 20 Feb 2018 10:00:00 GMT")); + headers.add(new NameValuePair("Expires", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date( + System.currentTimeMillis() + 2 * 1000 + 10 * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_MINUTE)))); + headers.add(new NameValuePair("Cache-Control", "public, some-other-value")); + + final URL pageUrl = new URL(URL_FIRST, "page1.html"); + connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers); + connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", JAVASCRIPT_MIME_TYPE, headers); + + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // resources should be still in cache + client.getPage(pageUrl); + assertEquals(2, client.getCache().getSize()); + assertEquals(2, connection.getRequestCount()); + // wait for expires + Thread.sleep(2 * 1000); + client.getPage(pageUrl); + assertEquals(0, client.getCache().getSize()); + assertEquals(4, connection.getRequestCount()); + } + + /** * Ensures {@link WebResponse#cleanUp()} is called for overflow deleted entries. * @throws Exception if the test fails */ @@ -368,8 +521,9 @@ final WebResponse response1 = createMock(WebResponse.class); expect(response1.getWebRequest()).andReturn(request1); expectLastCall().atLeastOnce(); - expect(response1.getResponseHeaderValue("Last-Modified")).andReturn(null); - expect(response1.getResponseHeaderValue("Expires")).andReturn( + expect(response1.getResponseHeaderValue(HttpHeader.CACHE_CONTROL)).andReturn(null); + expect(response1.getResponseHeaderValue(HttpHeader.LAST_MODIFIED)).andReturn(null); + expect(response1.getResponseHeaderValue(HttpHeader.EXPIRES)).andReturn( StringUtils.formatHttpDate(DateUtils.addHours(new Date(), 1))); final WebRequest request2 = new WebRequest(URL_SECOND, HttpMethod.GET); @@ -376,8 +530,9 @@ final WebResponse response2 = createMock(WebResponse.class); expect(response2.getWebRequest()).andReturn(request2); expectLastCall().atLeastOnce(); - expect(response2.getResponseHeaderValue("Last-Modified")).andReturn(null); - expect(response2.getResponseHeaderValue("Expires")).andReturn( + expect(response2.getResponseHeaderValue(HttpHeader.CACHE_CONTROL)).andReturn(null); + expect(response2.getResponseHeaderValue(HttpHeader.LAST_MODIFIED)).andReturn(null); + expect(response2.getResponseHeaderValue(HttpHeader.EXPIRES)).andReturn( StringUtils.formatHttpDate(DateUtils.addHours(new Date(), 1))); response1.cleanUp(); @@ -402,8 +557,9 @@ final WebResponse response1 = createMock(WebResponse.class); expect(response1.getWebRequest()).andReturn(request1); expectLastCall().atLeastOnce(); - expect(response1.getResponseHeaderValue("Last-Modified")).andReturn(null); - expect(response1.getResponseHeaderValue("Expires")).andReturn( + expect(response1.getResponseHeaderValue(HttpHeader.CACHE_CONTROL)).andReturn(null); + expect(response1.getResponseHeaderValue(HttpHeader.LAST_MODIFIED)).andReturn(null); + expect(response1.getResponseHeaderValue(HttpHeader.EXPIRES)).andReturn( StringUtils.formatHttpDate(DateUtils.addHours(new Date(), 1))); response1.cleanUp(); Modified: trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/History2Test.java =================================================================== --- trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/History2Test.java 2018-02-19 20:09:12 UTC (rev 15135) +++ trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/History2Test.java 2018-02-20 19:16:28 UTC (rev 15136) @@ -14,7 +14,11 @@ */ package com.gargoylesoftware.htmlunit.javascript.host; +import static java.nio.charset.StandardCharsets.ISO_8859_1; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,6 +29,7 @@ import com.gargoylesoftware.htmlunit.BrowserRunner; import com.gargoylesoftware.htmlunit.BrowserRunner.Alerts; import com.gargoylesoftware.htmlunit.WebDriverTestCase; +import com.gargoylesoftware.htmlunit.util.NameValuePair; /** * Tests for {@link History}. @@ -34,6 +39,7 @@ * @author Ronald Brill * @author Adam Afeltowicz * @author Carsten Steul + * @author Anton Demydenko */ @RunWith(BrowserRunner.class) public class History2Test extends WebDriverTestCase { @@ -802,4 +808,42 @@ loadPageWithAlerts2(html); } + + /** + * @throws Exception if an error occurs + */ + @Test + public void testHistoryBackAndForwarWithNoStoreCacheControlHeader() throws Exception { + final String html = "<html><body>" + + "<a id='startButton' href='" + URL_SECOND + "'>Start</a>\n" + + "</body></html>"; + final String secondContent = "<!DOCTYPE html>\n" + + "<html><head></head>\n" + + "<body>\n" + + " <a id='nextButton' href='" + URL_THIRD + "'>Next</a>\n" + + " <a id='forwardButton' onclick='javascript:window.history.forward()'>Forward</a>\n" + + "</body></html>"; + final String thirdContent = "<html><body>" + + "<a id='backButton' onclick='javascript:window.history.back()'>Back</a>\n" + + "</body></html>"; + + final List<NameValuePair> headers = new ArrayList<>(); + headers.add(new NameValuePair("Cache-Control", "some-other-value, no-store")); + getMockWebConnection().setResponse(URL_SECOND, secondContent, 200, "OK", "text/html;charset=ISO-8859-1", + ISO_8859_1, headers); + getMockWebConnection().setResponse(URL_THIRD, thirdContent, 200, "OK", "text/html;charset=ISO-8859-1", + ISO_8859_1, headers); + + final WebDriver driver = loadPage2(html); + driver.findElement(By.id("startButton")).click(); + driver.findElement(By.id("nextButton")).click(); + driver.findElement(By.id("backButton")).click(); + + assertEquals(URL_SECOND.toString(), driver.getCurrentUrl()); + assertEquals(4, getMockWebConnection().getRequestCount()); + + driver.findElement(By.id("forwardButton")).click(); + assertEquals(URL_THIRD.toString(), driver.getCurrentUrl()); + assertEquals(5, getMockWebConnection().getRequestCount()); + } } |