summaryrefslogtreecommitdiffstats
path: root/test/wpt/tests/fetch/http-cache
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e /test/wpt/tests/fetch/http-cache
parentInitial commit. (diff)
downloadnode-undici-upstream.tar.xz
node-undici-upstream.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/wpt/tests/fetch/http-cache')
-rw-r--r--test/wpt/tests/fetch/http-cache/304-update.any.js146
-rw-r--r--test/wpt/tests/fetch/http-cache/README.md72
-rw-r--r--test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html6
-rw-r--r--test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html27
-rw-r--r--test/wpt/tests/fetch/http-cache/cache-mode.any.js61
-rw-r--r--test/wpt/tests/fetch/http-cache/cc-request.any.js202
-rw-r--r--test/wpt/tests/fetch/http-cache/credentials.tentative.any.js62
-rw-r--r--test/wpt/tests/fetch/http-cache/freshness.any.js215
-rw-r--r--test/wpt/tests/fetch/http-cache/heuristic.any.js93
-rw-r--r--test/wpt/tests/fetch/http-cache/http-cache.js274
-rw-r--r--test/wpt/tests/fetch/http-cache/invalidate.any.js235
-rw-r--r--test/wpt/tests/fetch/http-cache/partial.any.js208
-rw-r--r--test/wpt/tests/fetch/http-cache/post-patch.any.js46
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/http-cache.py124
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/securedimage.py19
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html34
-rw-r--r--test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html28
-rw-r--r--test/wpt/tests/fetch/http-cache/split-cache.html158
-rw-r--r--test/wpt/tests/fetch/http-cache/status.any.js60
-rw-r--r--test/wpt/tests/fetch/http-cache/vary.any.js313
20 files changed, 2383 insertions, 0 deletions
diff --git a/test/wpt/tests/fetch/http-cache/304-update.any.js b/test/wpt/tests/fetch/http-cache/304-update.any.js
new file mode 100644
index 0000000..15484f0
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/304-update.any.js
@@ -0,0 +1,146 @@
+// META: global=window,worker
+// META: title=HTTP Cache - 304 Updates
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache updates returned headers from a Last-Modified 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", -3000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "lm_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates stored headers from a Last-Modified 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["Last-Modified", -3000],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "lm_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates returned headers from a ETag 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "ABC"],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", -3000],
+ ["ETag", "ABC"],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache updates stored headers from a ETag 304",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "DEF"],
+ ["Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["ETag", "DEF"],
+ ["Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+ {
+ name: "Content-* header",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", -5000],
+ ["ETag", "GHI"],
+ ["Content-Test-Header", "A"]
+ ]
+ },
+ {
+ response_headers: [
+ ["Expires", 3000],
+ ["ETag", "GHI"],
+ ["Content-Test-Header", "B"]
+ ],
+ expected_type: "etag_validated",
+ expected_response_headers: [
+ ["Content-Test-Header", "B"]
+ ],
+ pause_after: true
+ },
+ {
+ expected_type: "cached",
+ expected_response_headers: [
+ ["Content-Test-Header", "B"]
+ ]
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/README.md b/test/wpt/tests/fetch/http-cache/README.md
new file mode 100644
index 0000000..512c422
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/README.md
@@ -0,0 +1,72 @@
+## HTTP Caching Tests
+
+These tests cover HTTP-specified behaviours for caches, primarily from
+[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html), but as seen through the
+lens of Fetch.
+
+A few notes:
+
+* By its nature, [caching is entirely optional](
+ https://www.rfc-editor.org/rfc/rfc9111.html#section-2-2);
+ some tests expecting a response to be
+ cached might fail because the client chose not to cache it, or chose to
+ race the cache with a network request.
+
+* Likewise, some tests might fail because there is a separate document-level
+ cache that's not well defined; see [this
+ issue](https://github.com/whatwg/fetch/issues/354).
+
+* [Partial content tests](partial.any.js) (a.k.a. Range requests) are not specified
+ in Fetch; tests are included here for interest only.
+
+* Some browser caches will behave differently when reloading /
+ shift-reloading, despite the `cache mode` staying the same.
+
+* [cache-tests.fyi](https://cache-tests.fyi/) is another test suite of HTTP caching
+ which also caters to server/CDN implementations.
+
+## Test Format
+
+Each test run gets its own URL and randomized content and operates independently.
+
+Each test is an an array of objects, with the following members:
+
+- `name` - The name of the test.
+- `requests` - a list of request objects (see below).
+
+Possible members of a request object:
+
+- template - A template object for the request, by name.
+- request_method - A string containing the HTTP method to be used.
+- request_headers - An array of `[header_name_string, header_value_string]` arrays to
+ emit in the request.
+- request_body - A string to use as the request body.
+- mode - The mode string to pass to `fetch()`.
+- credentials - The credentials string to pass to `fetch()`.
+- cache - The cache string to pass to `fetch()`.
+- pause_after - Boolean controlling a 3-second pause after the request completes.
+- response_status - A `[number, string]` array containing the HTTP status code
+ and phrase to return.
+- response_headers - An array of `[header_name_string, header_value_string]` arrays to
+ emit in the response. These values will also be checked like
+ expected_response_headers, unless there is a third value that is
+ `false`. See below for special handling considerations.
+- response_body - String to send as the response body. If not set, it will contain
+ the test identifier.
+- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]`
+- expected_status - A number representing a HTTP status code to check the response for.
+ If not set, the value of `response_status[0]` will be used; if that
+ is not set, 200 will be used.
+- expected_request_headers - An array of `[header_name_string, header_value_string]` representing
+ headers to check the request for.
+- expected_response_headers - An array of `[header_name_string, header_value_string]` representing
+ headers to check the response for. See also response_headers.
+- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking.
+
+Some headers in `response_headers` are treated specially:
+
+* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server.
+* For URL-carrying headers, the value will be appended as a query parameter for `target`.
+
+See the source for exact details.
+
diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html
new file mode 100644
index 0000000..905facd
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <meta charset="utf-8">
+ <img src="/images/green.png">
+ <img src="/images/green.png">
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html
new file mode 100644
index 0000000..a8979ba
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html id="doc" class="reftest-wait">
+ <meta charset="utf-8">
+ <link rel="match" href="basic-auth-cache-test-ref.html">
+
+ <img id="auth" onload="loadNoAuth()">
+ <img id="noauth" onload="removeWait()">
+
+
+ <script type="text/javascript">
+ function loadAuth() {
+ var authUrl = 'http://testuser:testpass@' + window.location.host + '/fetch/http-cache/resources/securedimage.py';
+ document.getElementById('auth').src = authUrl;
+ }
+
+ function loadNoAuth() {
+ var noAuthUrl = 'http://' + window.location.host + '/fetch/http-cache/resources/securedimage.py';
+ document.getElementById('noauth').src = noAuthUrl;
+ }
+
+ function removeWait() {
+ document.getElementById('doc').className = "";
+ }
+
+ window.onload = loadAuth;
+ </script>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/cache-mode.any.js b/test/wpt/tests/fetch/http-cache/cache-mode.any.js
new file mode 100644
index 0000000..8f406d5
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/cache-mode.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Fetch - Cache Mode
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache",
+ requests: [
+ {
+ cache: "no-cache",
+ expected_request_headers: [['cache-control', 'max-age=0']]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present",
+ requests: [
+ {
+ cache: "no-cache",
+ request_headers: [['cache-control', 'foo']],
+ expected_request_headers: [['cache-control', 'foo']]
+ }
+ ]
+ },
+ {
+ name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store",
+ requests: [
+ {
+ cache: "no-store",
+ expected_request_headers: [
+ ['cache-control', 'no-cache'],
+ ['pragma', 'no-cache']
+ ]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present",
+ requests: [
+ {
+ cache: "no-store",
+ request_headers: [['cache-control', 'foo']],
+ expected_request_headers: [['cache-control', 'foo']]
+ }
+ ]
+ },
+ {
+ name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present",
+ requests: [
+ {
+ cache: "no-store",
+ request_headers: [['pragma', 'foo']],
+ expected_request_headers: [['pragma', 'foo']]
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/cc-request.any.js b/test/wpt/tests/fetch/http-cache/cc-request.any.js
new file mode 100644
index 0000000..d556566
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/cc-request.any.js
@@ -0,0 +1,202 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Cache-Control Request Directives
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0",
+ requests: [
+ {
+ template: "fresh",
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=0"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1",
+ requests: [
+ {
+ template: "fresh",
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=1"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Age", "1800"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-age=600"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1"]
+ ],
+ pause_after: true
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-stale=1000"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"],
+ ["Age", "2000"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "max-stale=1000"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "min-fresh=2000"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1500"],
+ ["Age", "1000"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "min-fresh=1000"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Last-Modified", -10000]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "lm_validate"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["ETag", http_content("abc")]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ expected_type: "etag_validate"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-store"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached",
+ requests: [
+ {
+ request_headers: [
+ ["Cache-Control", "only-if-cached"]
+ ],
+ expected_status: 504,
+ expected_response_text: null
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js
new file mode 100644
index 0000000..3177092
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js
@@ -0,0 +1,62 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Content
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=http-cache.js
+
+// This is a tentative test.
+// Firefox behavior is used as expectations.
+//
+// whatwg/fetch issue:
+// https://github.com/whatwg/fetch/issues/1253
+//
+// Chrome design doc:
+// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U/edit#
+
+const request_cacheable = {
+ request_headers: [],
+ response_headers: [
+ ['Cache-Control', 'max-age=3600'],
+ ],
+ // TODO(arthursonzogni): The behavior is tested only for same-origin requests.
+ // It must behave similarly for cross-site and cross-origin requests. The
+ // problems is the http-cache.js infrastructure returns the
+ // "Server-Request-Count" as HTTP response headers, which aren't readable for
+ // CORS requests.
+ base_url: location.href.replace(/\/[^\/]*$/, '/'),
+};
+
+const request_credentialled = { ...request_cacheable, credentials: 'include', };
+const request_anonymous = { ...request_cacheable, credentials: 'omit', };
+
+const responseIndex = count => {
+ return {
+ expected_response_headers: [
+ ['Server-Request-Count', count.toString()],
+ ],
+ }
+};
+
+var tests = [
+ {
+ name: 'same-origin: 2xAnonymous, 2xCredentialled, 1xAnonymous',
+ requests: [
+ { ...request_anonymous , ...responseIndex(1)} ,
+ { ...request_anonymous , ...responseIndex(1)} ,
+ { ...request_credentialled , ...responseIndex(2)} ,
+ { ...request_credentialled , ...responseIndex(2)} ,
+ { ...request_anonymous , ...responseIndex(1)} ,
+ ]
+ },
+ {
+ name: 'same-origin: 2xCredentialled, 2xAnonymous, 1xCredentialled',
+ requests: [
+ { ...request_credentialled , ...responseIndex(1)} ,
+ { ...request_credentialled , ...responseIndex(1)} ,
+ { ...request_anonymous , ...responseIndex(2)} ,
+ { ...request_anonymous , ...responseIndex(2)} ,
+ { ...request_credentialled , ...responseIndex(1)} ,
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/freshness.any.js b/test/wpt/tests/fetch/http-cache/freshness.any.js
new file mode 100644
index 0000000..6b97c82
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/freshness.any.js
@@ -0,0 +1,215 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Freshness
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ // response directives
+ {
+ name: "HTTP cache reuses a response with a future Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (30 * 24 * 60 * 60)]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with a past Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (-30 * 24 * 60 * 60)]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with a present Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", 0]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with an invalid Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Expires", "0"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with Cache-Control: max-age=0",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=0"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Expires", -10000]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Expires", "0"]
+ ]
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=0"],
+ ["Expires", 10000]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=1, s-maxage=3600"]
+ ],
+ pause_after: true,
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Age", "12000"]
+ ],
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not store a response with Cache-Control: no-store",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "no-store"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=10000, no-store"],
+ ["Expires", 10000]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "no-cache"],
+ ["ETag", "abcd"]
+ ]
+ },
+ {
+ expected_type: "etag_validated"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=10000, no-cache"],
+ ["Expires", 10000],
+ ["ETag", "abcd"]
+ ]
+ },
+ {
+ expected_type: "etag_validated"
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/heuristic.any.js b/test/wpt/tests/fetch/http-cache/heuristic.any.js
new file mode 100644
index 0000000..d846131
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/heuristic.any.js
@@ -0,0 +1,93 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Heuristic Freshness
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present",
+ requests: [
+ {
+ response_status: [299, "Whatever"],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)],
+ ["Cache-Control", "public"]
+ ],
+ },
+ {
+ expected_type: "cached",
+ response_status: [299, "Whatever"]
+ }
+ ]
+ },
+ {
+ name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present",
+ requests: [
+ {
+ response_status: [299, "Whatever"],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)]
+ ],
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ }
+];
+
+function check_status(status) {
+ var succeed = status[0];
+ var code = status[1];
+ var phrase = status[2];
+ var body = status[3];
+ if (body === undefined) {
+ body = http_content(code);
+ }
+ var expected_type = "not_cached";
+ var desired = "does not use"
+ if (succeed === true) {
+ expected_type = "cached";
+ desired = "reuses";
+ }
+ tests.push(
+ {
+ name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness",
+ requests: [
+ {
+ response_status: [code, phrase],
+ response_headers: [
+ ["Last-Modified", (-3 * 100)]
+ ],
+ response_body: body
+ },
+ {
+ expected_type: expected_type,
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ }
+ )
+}
+[
+ [true, 200, "OK"],
+ [true, 203, "Non-Authoritative Information"],
+ [true, 204, "No Content", ""],
+ [true, 404, "Not Found"],
+ [true, 405, "Method Not Allowed"],
+ [true, 410, "Gone"],
+ [true, 414, "URI Too Long"],
+ [true, 501, "Not Implemented"]
+].forEach(check_status);
+[
+ [false, 201, "Created"],
+ [false, 202, "Accepted"],
+ [false, 403, "Forbidden"],
+ [false, 502, "Bad Gateway"],
+ [false, 503, "Service Unavailable"],
+ [false, 504, "Gateway Timeout"],
+].forEach(check_status);
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/http-cache.js b/test/wpt/tests/fetch/http-cache/http-cache.js
new file mode 100644
index 0000000..19f1ca9
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/http-cache.js
@@ -0,0 +1,274 @@
+/* global btoa fetch token promise_test step_timeout */
+/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */
+
+const templates = {
+ 'fresh': {
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ },
+ 'stale': {
+ 'response_headers': [
+ ['Expires', -5000],
+ ['Last-Modified', -100000]
+ ]
+ },
+ 'lcl_response': {
+ 'response_headers': [
+ ['Location', 'location_target'],
+ ['Content-Location', 'content_location_target']
+ ]
+ },
+ 'location': {
+ 'query_arg': 'location_target',
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ },
+ 'content_location': {
+ 'query_arg': 'content_location_target',
+ 'response_headers': [
+ ['Expires', 100000],
+ ['Last-Modified', 0]
+ ]
+ }
+}
+
+const noBodyStatus = new Set([204, 304])
+
+function makeTest (test) {
+ return function () {
+ var uuid = token()
+ var requests = expandTemplates(test)
+ var fetchFunctions = makeFetchFunctions(requests, uuid)
+ return runTest(fetchFunctions, requests, uuid)
+ }
+}
+
+function makeFetchFunctions(requests, uuid) {
+ var fetchFunctions = []
+ for (let i = 0; i < requests.length; ++i) {
+ fetchFunctions.push({
+ code: function (idx) {
+ var config = requests[idx]
+ var url = makeTestUrl(uuid, config)
+ var init = fetchInit(requests, config)
+ return fetch(url, init)
+ .then(makeCheckResponse(idx, config))
+ .then(makeCheckResponseBody(config, uuid), function (reason) {
+ if ('expected_type' in config && config.expected_type === 'error') {
+ assert_throws_js(TypeError, function () { throw reason })
+ } else {
+ throw reason
+ }
+ })
+ },
+ pauseAfter: 'pause_after' in requests[i]
+ })
+ }
+ return fetchFunctions
+}
+
+function runTest(fetchFunctions, requests, uuid) {
+ var idx = 0
+ function runNextStep () {
+ if (fetchFunctions.length) {
+ var nextFetchFunction = fetchFunctions.shift()
+ if (nextFetchFunction.pauseAfter === true) {
+ return nextFetchFunction.code(idx++)
+ .then(pause)
+ .then(runNextStep)
+ } else {
+ return nextFetchFunction.code(idx++)
+ .then(runNextStep)
+ }
+ } else {
+ return Promise.resolve()
+ }
+ }
+
+ return runNextStep()
+ .then(function () {
+ return getServerState(uuid)
+ }).then(function (testState) {
+ checkRequests(requests, testState)
+ return Promise.resolve()
+ })
+}
+
+function expandTemplates (test) {
+ var rawRequests = test.requests
+ var requests = []
+ for (let i = 0; i < rawRequests.length; i++) {
+ var request = rawRequests[i]
+ request.name = test.name
+ if ('template' in request) {
+ var template = templates[request['template']]
+ for (let member in template) {
+ if (!request.hasOwnProperty(member)) {
+ request[member] = template[member]
+ }
+ }
+ }
+ requests.push(request)
+ }
+ return requests
+}
+
+function fetchInit (requests, config) {
+ var init = {
+ 'headers': []
+ }
+ if ('request_method' in config) init.method = config['request_method']
+ // Note: init.headers must be a copy of config['request_headers'] array,
+ // because new elements are added later.
+ if ('request_headers' in config) init.headers = [...config['request_headers']];
+ if ('name' in config) init.headers.push(['Test-Name', config.name])
+ if ('request_body' in config) init.body = config['request_body']
+ if ('mode' in config) init.mode = config['mode']
+ if ('credentials' in config) init.credentials = config['credentials']
+ if ('cache' in config) init.cache = config['cache']
+ init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))])
+ return init
+}
+
+function makeCheckResponse (idx, config) {
+ return function checkResponse (response) {
+ var reqNum = idx + 1
+ var resNum = parseInt(response.headers.get('Server-Request-Count'))
+ if ('expected_type' in config) {
+ if (config.expected_type === 'error') {
+ assert_true(false, `Request ${reqNum} doesn't throw an error`)
+ return response.text()
+ }
+ if (config.expected_type === 'cached') {
+ assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`)
+ }
+ if (config.expected_type === 'not_cached') {
+ assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`)
+ }
+ }
+ if ('expected_status' in config) {
+ assert_equals(response.status, config.expected_status,
+ `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`)
+ } else if ('response_status' in config) {
+ assert_equals(response.status, config.response_status[0],
+ `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`)
+ } else {
+ assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`)
+ }
+ if ('response_headers' in config) {
+ config.response_headers.forEach(function (header) {
+ if (header.len < 3 || header[2] === true) {
+ assert_equals(response.headers.get(header[0]), header[1],
+ `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
+ }
+ })
+ }
+ if ('expected_response_headers' in config) {
+ config.expected_response_headers.forEach(function (header) {
+ assert_equals(response.headers.get(header[0]), header[1],
+ `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
+ })
+ }
+ return response.text()
+ }
+}
+
+function makeCheckResponseBody (config, uuid) {
+ return function checkResponseBody (resBody) {
+ var statusCode = 200
+ if ('response_status' in config) {
+ statusCode = config.response_status[0]
+ }
+ if ('expected_response_text' in config) {
+ if (config.expected_response_text !== null) {
+ assert_equals(resBody, config.expected_response_text,
+ `Response body is "${resBody}", not expected "${config.expected_response_text}"`)
+ }
+ } else if ('response_body' in config && config.response_body !== null) {
+ assert_equals(resBody, config.response_body,
+ `Response body is "${resBody}", not sent "${config.response_body}"`)
+ } else if (!noBodyStatus.has(statusCode)) {
+ assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`)
+ }
+ }
+}
+
+function checkRequests (requests, testState) {
+ var testIdx = 0
+ for (let i = 0; i < requests.length; ++i) {
+ var expectedValidatingHeaders = []
+ var config = requests[i]
+ var serverRequest = testState[testIdx]
+ var reqNum = i + 1
+ if ('expected_type' in config) {
+ if (config.expected_type === 'cached') continue // the server will not see the request
+ if (config.expected_type === 'etag_validated') {
+ expectedValidatingHeaders.push('if-none-match')
+ }
+ if (config.expected_type === 'lm_validated') {
+ expectedValidatingHeaders.push('if-modified-since')
+ }
+ }
+ testIdx++
+ expectedValidatingHeaders.forEach(vhdr => {
+ assert_own_property(serverRequest.request_headers, vhdr,
+ `request ${reqNum} doesn't have ${vhdr} header`)
+ })
+ if ('expected_request_headers' in config) {
+ config.expected_request_headers.forEach(expectedHdr => {
+ assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1],
+ `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`)
+ })
+ }
+ }
+}
+
+function pause () {
+ return new Promise(function (resolve, reject) {
+ step_timeout(function () {
+ return resolve()
+ }, 3000)
+ })
+}
+
+function makeTestUrl (uuid, config) {
+ var arg = ''
+ var base_url = ''
+ if ('base_url' in config) {
+ base_url = config.base_url
+ }
+ if ('query_arg' in config) {
+ arg = `&target=${config.query_arg}`
+ }
+ return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}`
+}
+
+function getServerState (uuid) {
+ return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`)
+ .then(function (response) {
+ return response.text()
+ }).then(function (text) {
+ return JSON.parse(text) || []
+ })
+}
+
+function run_tests (tests) {
+ tests.forEach(function (test) {
+ promise_test(makeTest(test), test.name)
+ })
+}
+
+var contentStore = {}
+function http_content (csKey) {
+ if (csKey in contentStore) {
+ return contentStore[csKey]
+ } else {
+ var content = btoa(Math.random() * Date.now())
+ contentStore[csKey] = content
+ return content
+ }
+}
diff --git a/test/wpt/tests/fetch/http-cache/invalidate.any.js b/test/wpt/tests/fetch/http-cache/invalidate.any.js
new file mode 100644
index 0000000..9f8090a
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/invalidate.any.js
@@ -0,0 +1,235 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Invalidation
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: 'HTTP cache invalidates after a successful response from a POST',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "POST",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from a PUT',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ template: "fresh",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from a DELETE',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates after a successful response from an unknown method',
+ requests: [
+ {
+ template: "fresh"
+ }, {
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+
+
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a POST',
+ requests: [
+ {
+ template: "location"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ template: "lcl_response"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ template: "location",
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a PUT',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from a DELETE',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Location URL after a successful response from an unknown method',
+ requests: [
+ {
+ template: "location"
+ }, {
+ template: "lcl_response",
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ template: "location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+
+
+
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ request_method: "POST",
+ request_body: "abc",
+ template: "lcl_response"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [500, "Internal Server Error"]
+ }, {
+ template: "content_location",
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "PUT",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "DELETE",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method',
+ requests: [
+ {
+ template: "content_location"
+ }, {
+ template: "lcl_response",
+ request_method: "FOO",
+ request_body: "abc"
+ }, {
+ template: "content_location",
+ expected_type: "not_cached"
+ }
+ ]
+ }
+
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/partial.any.js b/test/wpt/tests/fetch/http-cache/partial.any.js
new file mode 100644
index 0000000..3f23b59
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/partial.any.js
@@ -0,0 +1,208 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Partial Content
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache stores partial content and reuses it",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234",
+ expected_request_headers: [
+ ["Range", "bytes=-5"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Range", "bytes=-5"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=0-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01"
+ },
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=1-"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "1234567890"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ],
+ response_body: "0123456789A"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "A"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores complete response and serves smaller ranges from it with only-if-cached",
+ requests: [
+ {
+ response_headers: [
+ ["Cache-Control", "max-age=3600"]
+ ],
+ response_body: "01234567890"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=0-1"]
+ ],
+ mode: "same-origin",
+ cache: "only-if-cached",
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "01"
+ },
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=6-8"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ["Range", "bytes=6-"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "234"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 4-9/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ request_headers: [
+ ['Range', "bytes=-1"]
+ ],
+ expected_type: "cached",
+ expected_status: 206,
+ expected_response_text: "4"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache stores partial content and completes it",
+ requests: [
+ {
+ request_headers: [
+ ['Range', "bytes=-5"]
+ ],
+ response_status: [206, "Partial Content"],
+ response_headers: [
+ ["Cache-Control", "max-age=3600"],
+ ["Content-Range", "bytes 0-4/10"]
+ ],
+ response_body: "01234"
+ },
+ {
+ expected_request_headers: [
+ ["range", "bytes=5-"]
+ ]
+ }
+ ]
+ },
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/post-patch.any.js b/test/wpt/tests/fetch/http-cache/post-patch.any.js
new file mode 100644
index 0000000..0a69baa
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/post-patch.any.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Caching POST and PATCH responses
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header",
+ requests: [
+ {
+ request_method: "PATCH",
+ request_body: "abc",
+ response_status: [200, "OK"],
+ response_headers: [
+ ['Cache-Control', "private, max-age=1000"],
+ ['Content-Location', ""]
+ ],
+ response_body: "abc"
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header",
+ requests: [
+ {
+ request_method: "POST",
+ request_body: "abc",
+ response_status: [200, "OK"],
+ response_headers: [
+ ['Cache-Control', "private, max-age=1000"],
+ ['Content-Location', ""]
+ ],
+ response_body: "abc"
+ },
+ {
+ expected_type: "cached"
+ }
+ ]
+ }
+];
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/resources/http-cache.py b/test/wpt/tests/fetch/http-cache/resources/http-cache.py
new file mode 100644
index 0000000..3ab610d
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/http-cache.py
@@ -0,0 +1,124 @@
+import datetime
+import json
+import time
+from base64 import b64decode
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+NOTEHDRS = set([u'content-type', u'access-control-allow-origin', u'last-modified', u'etag'])
+NOBODYSTATUS = set([204, 304])
+LOCATIONHDRS = set([u'location', u'content-location'])
+DATEHDRS = set([u'date', u'expires', u'last-modified'])
+
+def main(request, response):
+ dispatch = request.GET.first(b"dispatch", None)
+ uuid = request.GET.first(b"uuid", None)
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ if request.method == u"OPTIONS":
+ return handle_preflight(uuid, request, response)
+ if not uuid:
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"UUID not found"
+ if dispatch == b'test':
+ return handle_test(uuid, request, response)
+ elif dispatch == b'state':
+ return handle_state(uuid, request, response)
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Fallthrough"
+
+def handle_preflight(uuid, request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*')
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Allow-Headers", request.headers.get(b"Access-Control-Request-Headers") or "*")
+ response.headers.set(b"Access-Control-Max-Age", b"86400")
+ return b"Preflight request"
+
+def handle_state(uuid, request, response):
+ response.headers.set(b"Content-Type", b"text/plain")
+ return json.dumps(request.server.stash.take(uuid))
+
+def handle_test(uuid, request, response):
+ server_state = request.server.stash.take(uuid) or []
+ try:
+ requests = json.loads(b64decode(request.headers.get(b'Test-Requests', b"")))
+ except:
+ response.status = (400, b"Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"No or bad Test-Requests request header"
+ config = requests[len(server_state)]
+ if not config:
+ response.status = (404, b"Not Found")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return b"Config not found"
+ noted_headers = {}
+ now = time.time()
+ for header in config.get(u'response_headers', []):
+ if header[0].lower() in LOCATIONHDRS: # magic locations
+ if (len(header[1]) > 0):
+ header[1] = u"%s&target=%s" % (request.url, header[1])
+ else:
+ header[1] = request.url
+ if header[0].lower() in DATEHDRS and isinstance(header[1], int): # magic dates
+ header[1] = http_date(now, header[1])
+ response.headers.set(isomorphic_encode(header[0]), isomorphic_encode(header[1]))
+ if header[0].lower() in NOTEHDRS:
+ noted_headers[header[0].lower()] = header[1]
+ state = {
+ u'now': now,
+ u'request_method': request.method,
+ u'request_headers': dict([[isomorphic_decode(h.lower()), isomorphic_decode(request.headers[h])] for h in request.headers]),
+ u'response_headers': noted_headers
+ }
+ server_state.append(state)
+ request.server.stash.put(uuid, server_state)
+
+ if u"access-control-allow-origin" not in noted_headers:
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+ if u"content-type" not in noted_headers:
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Server-Request-Count", len(server_state))
+
+ code, phrase = config.get(u"response_status", [200, b"OK"])
+ if config.get(u"expected_type", u"").endswith(u'validated'):
+ ref_hdrs = server_state[0][u'response_headers']
+ previous_lm = ref_hdrs.get(u'last-modified', False)
+ if previous_lm and request.headers.get(b"If-Modified-Since", False) == isomorphic_encode(previous_lm):
+ code, phrase = [304, b"Not Modified"]
+ previous_etag = ref_hdrs.get(u'etag', False)
+ if previous_etag and request.headers.get(b"If-None-Match", False) == isomorphic_encode(previous_etag):
+ code, phrase = [304, b"Not Modified"]
+ if code != 304:
+ code, phrase = [999, b'304 Not Generated']
+ response.status = (code, phrase)
+
+ content = config.get(u"response_body", uuid)
+ if code in NOBODYSTATUS:
+ return b""
+ return content
+
+
+def get_header(headers, header_name):
+ result = None
+ for header in headers:
+ if header[0].lower() == header_name.lower():
+ result = header[1]
+ return result
+
+WEEKDAYS = [u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun']
+MONTHS = [None, u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul',
+ u'Aug', u'Sep', u'Oct', u'Nov', u'Dec']
+
+def http_date(now, delta_secs=0):
+ date = datetime.datetime.utcfromtimestamp(now + delta_secs)
+ return u"%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT" % (
+ WEEKDAYS[date.weekday()],
+ date.day,
+ MONTHS[date.month],
+ date.year,
+ date.hour,
+ date.minute,
+ date.second)
diff --git a/test/wpt/tests/fetch/http-cache/resources/securedimage.py b/test/wpt/tests/fetch/http-cache/resources/securedimage.py
new file mode 100644
index 0000000..cac9cfe
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/securedimage.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ image_url = str.replace(request.url, u"fetch/http-cache/resources/securedimage.py", u"images/green.png")
+
+ if b"authorization" not in request.headers:
+ response.status = 401
+ response.headers.set(b"WWW-Authenticate", b"Basic")
+ return
+ else:
+ auth = request.headers.get(b"Authorization")
+ if auth != b"Basic dGVzdHVzZXI6dGVzdHBhc3M=":
+ response.set_error(403, u"Invalid username or password - " + isomorphic_decode(auth))
+ return
+
+ response.status = 301
+ response.headers.set(b"Location", isomorphic_encode(image_url))
diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html
new file mode 100644
index 0000000..48b1618
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - helper</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <meta name="timeout" content="normal">
+ <script src="/resources/testharness.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+<script>
+ const host = get_host_info();
+
+ // Create iframe that is same-origin to the opener.
+ var iframe = document.createElement("iframe");
+ iframe.src = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') + "split-cache-popup.html";
+ document.body.appendChild(iframe);
+
+ window.addEventListener("message", function listener(event) {
+ if (event.origin !== host.HTTP_ORIGIN) {
+ // Ignore messages not from the iframe or opener
+ return;
+ } else if (typeof(event.data) === "object") {
+ // This message came from the opener, pass it on to the iframe
+ iframe.contentWindow.postMessage(event.data, host.HTTP_ORIGIN);
+ } else if (typeof(event.data) === "string") {
+ // This message came from the iframe, pass it on to the opener
+ window.opener.postMessage(event.data, host.HTTP_ORIGIN);
+ }
+ })
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html
new file mode 100644
index 0000000..edb5794
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - helper</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <meta name="timeout" content="normal">
+ <script src="/resources/testharness.js"></script>
+ <script src="../http-cache.js"></script>
+</head>
+<body>
+<script>
+ window.addEventListener("message", function listener(event) {
+ window.removeEventListener("message", listener)
+
+ var fetchFunction = makeFetchFunctions(event.data.requests, event.data.uuid)[event.data.index]
+ fetchFunction.code(event.data.index).then(
+ function(response) {
+ event.source.postMessage("success", event.origin)
+ },
+ function(response) {
+ event.source.postMessage("error", event.origin)
+ }
+ )
+ })
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/split-cache.html b/test/wpt/tests/fetch/http-cache/split-cache.html
new file mode 100644
index 0000000..fe93d2e
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/split-cache.html
@@ -0,0 +1,158 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>HTTP Cache - Partioning by site</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="http-cache.js"></script>
+</head>
+<body>
+<script>
+const host = get_host_info();
+
+// We run this entire test four times, varying the following two booleans:
+// - is_cross_site_test, which controls whether the popup is cross-site.
+// - load_resource_in_iframe, which controls whether the popup loads the
+// resource in an iframe or the top-level frame. Note that the iframe is
+// always same-site to the opener.
+function performFullTest(is_cross_site_test, load_resource_in_iframe, name) {
+ const POPUP_HTTP_ORIGIN = is_cross_site_test ? host.HTTP_NOTSAMESITE_ORIGIN : host.HTTP_ORIGIN
+ const LOCAL_HTTP_ORIGIN = host.HTTP_ORIGIN
+
+ const popupBaseURL = POPUP_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+ const localBaseURL = LOCAL_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ;
+
+ // Note: Navigation requests are requested with credentials. Make sure the
+ // fetch requests are also requested with credentials. This ensures passing
+ // this test is not simply the consequence of discriminating anonymous and
+ // credentialled request in the HTTP cache.
+ //
+ // See https://github.com/whatwg/fetch/issues/1253
+ var test = {
+ requests: [
+ {
+ response_headers: [
+ ["Expires", (30 * 24 * 60 * 60)],
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ },
+ {
+ response_headers: [
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ },
+ {
+ request_headers: [
+ ["Cache-Control", "no-cache"]
+ ],
+ response_headers: [
+ ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN],
+ ],
+ // If the popup's request was a cache hit, we would only expect 2
+ // requests to the server. If it was a cache miss, we would expect 3.
+ // load_resource_in_iframe does not affect the expectation as, even
+ // though the iframe (if present) is same-site, we expect a cache miss
+ // when the popup's top-level frame is a different site.
+ expected_response_headers: [
+ ["server-request-count", is_cross_site_test ? "3" : "2"]
+ ],
+ base_url: localBaseURL,
+ credentials: "include",
+ }
+ ]
+ }
+
+ var uuid = token()
+ var local_requests = expandTemplates(test)
+ var fetchFns = makeFetchFunctions(local_requests, uuid)
+
+ var popup_requests = expandTemplates(test)
+
+ // Request the resource with a long cache expiry
+ function local_fetch() {
+ return fetchFns[0].code(0)
+ }
+
+ function popup_fetch() {
+ return new Promise(function(resolve, reject) {
+ var relativeUrl = load_resource_in_iframe
+ ? "resources/split-cache-popup-with-iframe.html"
+ : "resources/split-cache-popup.html";
+ var win = window.open(popupBaseURL + relativeUrl);
+
+ // Post a message to initiate the popup's request and give the necessary
+ // information. Posted repeatedly to account for dropped messages as the
+ // popup is loading.
+ function postMessage(event) {
+ var payload = {
+ index: 1,
+ requests: popup_requests,
+ uuid: uuid
+ }
+ win.postMessage(payload, POPUP_HTTP_ORIGIN)
+ }
+ var messagePoster = setInterval(postMessage, 100)
+
+ // Listen for the result
+ function messageListener(event) {
+ if (event.origin !== POPUP_HTTP_ORIGIN) {
+ reject("Unknown error")
+ } else if (event.data === "success") {
+ resolve()
+ } else if (event.data === "error") {
+ reject("Error in popup")
+ } else {
+ return; // Ignore testharness.js internal messages
+ }
+ window.removeEventListener("message", messageListener)
+ clearInterval(messagePoster)
+ win.close()
+ }
+ window.addEventListener("message", messageListener)
+ })
+ }
+
+ function local_fetch2() {
+ return fetchFns[2].code(2)
+ }
+
+ // Final checks.
+ function check_server_info() {
+ return getServerState(uuid)
+ .then(function (testState) {
+ checkRequests(local_requests, testState)
+ return Promise.resolve()
+ })
+ }
+
+ promise_test(() => {
+ return local_fetch()
+ .then(popup_fetch)
+ .then(local_fetch2)
+ .then(check_server_info)
+ }, name)
+}
+
+performFullTest(
+ false /* is_cross_site_test */, false /* load_resource_in_iframe */,
+ "HTTP cache is shared between same-site top-level frames");
+performFullTest(
+ true /* is_cross_site_test */, false /* load_resource_in_iframe */,
+ "HTTP cache is not shared between cross-site top-level frames");
+performFullTest(
+ false /* is_cross_site_test */, true /* load_resource_in_iframe */,
+ "HTTP cache is shared between same-site frames with same-site top-level frames");
+performFullTest(
+ true /* is_cross_site_test */, true /* load_resource_in_iframe */,
+ "HTTP cache is not shared between same-site frames with cross-site top-level frames");
+</script>
+</body>
+</html>
diff --git a/test/wpt/tests/fetch/http-cache/status.any.js b/test/wpt/tests/fetch/http-cache/status.any.js
new file mode 100644
index 0000000..10c83a2
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/status.any.js
@@ -0,0 +1,60 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Status Codes
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [];
+function check_status(status) {
+ var code = status[0];
+ var phrase = status[1];
+ var body = status[2];
+ if (body === undefined) {
+ body = http_content(code);
+ }
+ tests.push({
+ name: "HTTP cache goes to the network if it has a stale " + code + " response",
+ requests: [
+ {
+ template: "stale",
+ response_status: [code, phrase],
+ response_body: body
+ }, {
+ expected_type: "not_cached",
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ })
+ tests.push({
+ name: "HTTP cache avoids going to the network if it has a fresh " + code + " response",
+ requests: [
+ {
+ template: "fresh",
+ response_status: [code, phrase],
+ response_body: body
+ }, {
+ expected_type: "cached",
+ response_status: [code, phrase],
+ response_body: body
+ }
+ ]
+ })
+}
+[
+ [200, "OK"],
+ [203, "Non-Authoritative Information"],
+ [204, "No Content", null],
+ [299, "Whatever"],
+ [400, "Bad Request"],
+ [404, "Not Found"],
+ [410, "Gone"],
+ [499, "Whatever"],
+ [500, "Internal Server Error"],
+ [502, "Bad Gateway"],
+ [503, "Service Unavailable"],
+ [504, "Gateway Timeout"],
+ [599, "Whatever"]
+].forEach(check_status);
+run_tests(tests);
diff --git a/test/wpt/tests/fetch/http-cache/vary.any.js b/test/wpt/tests/fetch/http-cache/vary.any.js
new file mode 100644
index 0000000..2cfd226
--- /dev/null
+++ b/test/wpt/tests/fetch/http-cache/vary.any.js
@@ -0,0 +1,313 @@
+// META: global=window,worker
+// META: title=HTTP Cache - Vary
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=http-cache.js
+
+var tests = [
+ {
+ name: "HTTP cache reuses Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response when request omits variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't invalidate existing Vary response",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ response_body: http_content('foo_1')
+ },
+ {
+ request_headers: [
+ ["Foo", "2"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ expected_type: "not_cached",
+ response_body: http_content('foo_2'),
+ },
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_body: http_content('foo_1'),
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't pay attention to headers not listed in Vary",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Other", "2"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo"]
+ ],
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Other", "3"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses two-way Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use two-way Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"],
+ ["Bar", "abc"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use two-way Vary response when request omits variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar"]
+ ]
+ },
+ {
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache reuses three-way Vary response when request matches",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use three-way Vary response when request doesn't match",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "2"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc4"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Bar", "abc"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "Foo, Bar, Baz"]
+ ]
+ },
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ expected_type: "cached"
+ }
+ ]
+ },
+ {
+ name: "HTTP cache doesn't use Vary response with a field value of '*'",
+ requests: [
+ {
+ request_headers: [
+ ["Foo", "1"],
+ ["Baz", "789"]
+ ],
+ response_headers: [
+ ["Expires", 5000],
+ ["Last-Modified", -3000],
+ ["Vary", "*"]
+ ]
+ },
+ {
+ request_headers: [
+ ["*", "1"],
+ ["Baz", "789"]
+ ],
+ expected_type: "not_cached"
+ }
+ ]
+ }
+];
+run_tests(tests);