summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/service-workers/cache-storage
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/service-workers/cache-storage')
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/META.yml3
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js81
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js368
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js164
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html75
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js212
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js437
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js244
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js411
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js64
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js35
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js245
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js239
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/common.https.window.js44
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html17
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/credentials.https.html46
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html269
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/blank.html2
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js22
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js15
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html38
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js59
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py2
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html18
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt1
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js272
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/resources/vary.py25
-rw-r--r--testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html66
28 files changed, 3474 insertions, 0 deletions
diff --git a/testing/web-platform/tests/service-workers/cache-storage/META.yml b/testing/web-platform/tests/service-workers/cache-storage/META.yml
new file mode 100644
index 0000000000..bf34474f74
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - inexorabletash
+ - wanderview
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js
new file mode 100644
index 0000000000..960d1bb1bf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-abort.https.any.js
@@ -0,0 +1,81 @@
+// META: title=Cache Storage: Abort
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+// We perform the same tests on put, add, addAll. Parameterise the tests to
+// reduce repetition.
+const methodsToTest = {
+ put: async (cache, request) => {
+ const response = await fetch(request);
+ return cache.put(request, response);
+ },
+ add: async (cache, request) => cache.add(request),
+ addAll: async (cache, request) => cache.addAll([request]),
+};
+
+for (const method in methodsToTest) {
+ const perform = methodsToTest[method];
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+ const request = new Request('../resources/simple.txt', { signal });
+ return promise_rejects_dom(test, 'AbortError', perform(cache, request),
+ `${method} should reject`);
+ }, `${method}() on an already-aborted request should reject with AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/simple.txt', { signal });
+ const promise = perform(cache, request);
+ controller.abort();
+ return promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+ }, `${method}() synchronously followed by abort should reject with ` +
+ `AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ const request = new Request(
+ `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
+ { signal });
+
+ const promise = perform(cache, request);
+
+ // Wait for the server to start sending the response body.
+ let opened = false;
+ do {
+ // Normally only one fetch to 'stash-take' is needed, but the fetches
+ // will be served in reverse order sometimes
+ // (i.e., 'stash-take' gets served before 'infinite-slow-response').
+
+ const response =
+ await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
+ const body = await response.json();
+ if (body === 'open') opened = true;
+ } while (!opened);
+
+ // Sadly the above loop cannot guarantee that the browser has started
+ // processing the response body. This delay is needed to make the test
+ // failures non-flaky in Chrome version 66. My deepest apologies.
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ controller.abort();
+
+ await promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+
+ // infinite-slow-response.py doesn't know when to stop.
+ return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
+ }, `${method}() followed by abort after headers received should reject ` +
+ `with AbortError`);
+}
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js
new file mode 100644
index 0000000000..eca516abd5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-add.https.any.js
@@ -0,0 +1,368 @@
+// META: title=Cache.add and Cache.addAll
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(),
+ 'Cache.add should throw a TypeError when no arguments are given.');
+ }, 'Cache.add called with no arguments');
+
+cache_test(function(cache) {
+ return cache.add('./resources/simple.txt')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ return cache.match('./resources/simple.txt');
+ })
+ .then(function(response) {
+ assert_class_string(response, 'Response',
+ 'Cache.add should put a resource in the cache.');
+ return response.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.add called with relative URL specified as a string');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('javascript://this-is-not-http-mmkay'),
+ 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
+ }, 'Cache.add called with non-HTTP/HTTPS URL');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called with Request object');
+
+cache_test(function(cache, test) {
+ var request = new Request('./resources/simple.txt',
+ {method: 'POST', body: 'This is a body.'});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(request),
+ 'Cache.add should throw a TypeError for non-GET requests.');
+ }, 'Cache.add called with POST request');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ })
+ .then(function() {
+ return cache.add(request);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called twice with the same Request object');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return request.text()
+ .then(function() {
+ assert_false(request.bodyUsed);
+ })
+ .then(function() {
+ return cache.add(request);
+ });
+ }, 'Cache.add with request with null body (not consumed)');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=206'),
+ 'Cache.add should reject on partial response');
+ }, 'Cache.add with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ var cross_origin_url = new URL(url, location.href);
+ cross_origin_url.hostname = REMOTE_HOST;
+ return new Request(cross_origin_url.href, { mode: 'no-cors' });
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with opaque-filtered 206 response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('this-does-not-exist-please-dont-create-it'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 404');
+
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=500'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 500');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(),
+ 'Cache.addAll with no arguments should throw TypeError.');
+ }, 'Cache.addAll with no arguments');
+
+cache_test(function(cache, test) {
+ // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
+ var urls = ['./resources/simple.txt', undefined, './resources/blank.html'];
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(urls),
+ 'Cache.addAll should throw TypeError for an undefined argument.');
+ }, 'Cache.addAll with a mix of valid and undefined arguments');
+
+cache_test(function(cache) {
+ return cache.addAll([])
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return cache.keys();
+ })
+ .then(function(result) {
+ assert_equals(result.length, 0,
+ 'There should be no entry in the cache.');
+ });
+ }, 'Cache.addAll with an empty array');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ return cache.addAll(urls)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with string URL arguments');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return cache.addAll(requests)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with Request arguments');
+
+cache_test(function(cache, test) {
+ // Assumes that ../resources/simple.txt and ../resources/blank.html exist.
+ // The second resource does not.
+ var urls = ['./resources/simple.txt',
+ 'this-resource-should-not-exist',
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails')
+ .then(function() {
+ return Promise.all(urls.map(function(url) {
+ return cache.match(url);
+ }));
+ })
+ .then(function(matches) {
+ assert_array_equals(
+ matches,
+ [undefined, undefined, undefined],
+ 'If any response fails, no response should be added to cache');
+ });
+ }, 'Cache.addAll with a mix of succeeding and failing requests');
+
+cache_test(function(cache, test) {
+ var request = new Request('../resources/simple.txt');
+ return promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll([request, request]),
+ 'Cache.addAll should throw InvalidStateError if the same request is added ' +
+ 'twice.');
+ }, 'Cache.addAll called with the same Request object specified twice');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'square' }}),
+ ];
+ let result = await cache.addAll(requests);
+ assert_equals(result, undefined, 'Cache.addAll() should succeed');
+ }, 'Cache.addAll should succeed when entries differ by vary header');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when entries are duplicate by vary header');
+ }, 'Cache.addAll should reject when entries are duplicate by vary header');
+
+// VARY header matching is asymmetric. Determining if two entries are duplicate
+// depends on which entry's response is used in the comparison. The target
+// response's VARY header determines what request headers are examined. This
+// test verifies that Cache.addAll() duplicate checking handles this asymmetric
+// behavior correctly.
+cache_test(async function(cache, test) {
+ const base_url = './resources/vary.py';
+
+ // Define a request URL that sets a VARY header in the
+ // query string to be echoed back by the server.
+ const url = base_url + '?vary=x-size';
+
+ // Set a cookie to override the VARY header of the response
+ // when the request is made with credentials. This will
+ // take precedence over the query string vary param. This
+ // is a bit confusing, but it's necessary to construct a test
+ // where the URL is the same, but the VARY headers differ.
+ //
+ // Note, the test could also pass this information in additional
+ // request headers. If the cookie approach becomes too unwieldy
+ // this test could be rewritten to use that technique.
+ await fetch(base_url + '?set-vary-value-override-cookie=x-shape');
+ test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie'));
+
+ let requests = [
+ // This request will result in a Response with a "Vary: x-shape"
+ // header. This *will not* result in a duplicate match with the
+ // other entry.
+ new Request(url, { headers: { 'x-shape': 'circle',
+ 'x-size': 'big' },
+ credentials: 'same-origin' }),
+
+ // This request will result in a Response with a "Vary: x-size"
+ // header. This *will* result in a duplicate match with the other
+ // entry.
+ new Request(url, { headers: { 'x-shape': 'square',
+ 'x-size': 'big' },
+ credentials: 'omit' }),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching an earlier entry.');
+
+ // Test the reverse order now.
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests.reverse()),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching a later entry.');
+
+ }, 'Cache.addAll should reject when one entry has a vary header ' +
+ 'matching another entry');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js
new file mode 100644
index 0000000000..3eae2b6a08
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-delete.https.any.js
@@ -0,0 +1,164 @@
+// META: title=Cache.delete
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+
+// Construct a generic Request object. The URL is |test_url|. All other fields
+// are defaults.
+function new_test_request() {
+ return new Request(test_url);
+}
+
+// Construct a generic Response object.
+function new_test_response() {
+ return new Response('Hello world!', { status: 200 });
+}
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.delete(),
+ 'Cache.delete should reject with a TypeError when called with no ' +
+ 'arguments.');
+ }, 'Cache.delete with no arguments');
+
+cache_test(function(cache) {
+ return cache.put(new_test_request(), new_test_response())
+ .then(function() {
+ return cache.delete(test_url);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.delete should remove matching entries from cache.');
+ });
+ }, 'Cache.delete called with a string URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ return cache.put(request, new_test_response())
+ .then(function() {
+ return cache.delete(request);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ });
+ }, 'Cache.delete called with a Request object');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new_test_response();
+ return cache.put(request, response)
+ .then(function() {
+ return cache.delete(new Request(test_url, {method: 'HEAD'}));
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not match a non-GET request ' +
+ 'unless ignoreMethod option is set.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.delete should leave non-matching response in the cache.');
+ return cache.delete(new Request(test_url, {method: 'HEAD'}),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should match a non-GET request ' +
+ ' if ignoreMethod is true.');
+ });
+ }, 'Cache.delete called with a HEAD request');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.delete(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not delete if vary does not ' +
+ 'match unless ignoreVary is true');
+ return cache.delete(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should ignore vary if ignoreVary is true');
+ });
+ }, 'Cache.delete supports ignoreVary');
+
+cache_test(function(cache) {
+ return cache.delete(test_url)
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should resolve with "false" if there ' +
+ 'are no matching entries.');
+ });
+ }, 'Cache.delete with a non-existent entry');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, []);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (request with search parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ // cache.delete()'s behavior should be the same if ignoreSearch is
+ // not provided or if ignoreSearch is false.
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: false });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, [ entries.a.response ]);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (when it is specified as false)');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000000..3c96348e0e
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = './resources/cache-keys-attributes-for-service-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsReloadNavigation should persist.');
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = '../resources/blank.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsHistoryNavigation should persist.');
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js
new file mode 100644
index 0000000000..232fb760d4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-keys.https.any.js
@@ -0,0 +1,212 @@
+// META: title=Cache.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+cache_test(cache => {
+ return cache.keys()
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys() called on an empty cache');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('not-present-in-the-cache')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array on failure.');
+ });
+ }, 'Cache.keys with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request.url)
+ .then(function(result) {
+ assert_request_array_equals(result, [entries.a.request],
+ 'Cache.keys should match by URL.');
+ });
+ }, 'Cache.keys with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request)
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a_with_query.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.keys(head_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched method.');
+ return cache.keys(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ request,
+ ],
+ 'Cache.keys with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.keys supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.keys(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched vary.');
+ return cache.keys(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ vary_request,
+ ],
+ 'Cache.keys with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.keys supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.cat.request,
+ ],
+ 'Cache.keys should ignore URL fragment.');
+ });
+ }, 'Cache.keys with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('http')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.keys with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(undefined)
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys with undefined request should match all entries.');
+ });
+ }, 'Cache.keys with explicitly undefined request');
+
+cache_test(cache => {
+ return cache.keys(undefined, {})
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.request,
+ entries.vary_cookie_is_good.request,
+ entries.vary_cookie_absent.request,
+ ],
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters and VARY entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should not match HEAD request unless ignoreMethod ' +
+ 'option is set.');
+ });
+ }, 'Cache.keys with a HEAD Request');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js
new file mode 100644
index 0000000000..9ca45903cb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-match.https.any.js
@@ -0,0 +1,437 @@
+// META: title=Cache.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/get-host-info.sub.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('not-present-in-the-cache')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match failures should resolve with undefined.');
+ });
+ }, 'Cache.match with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request.url)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by URL.');
+ });
+ }, 'Cache.match with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var alt_response = new Response('', {status: 201});
+
+ return self.caches.open('second_matching_cache')
+ .then(function(cache) {
+ return cache.put(entries.a.request, alt_response.clone());
+ })
+ .then(function() {
+ return cache.match(entries.a.request);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.a.response,
+ 'Cache.match should match the first cache.');
+ });
+ }, 'Cache.match with multiple cache hits');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not match HEAD Request.');
+ });
+ }, 'Cache.match with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with search parameter)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched method.');
+ return cache.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'Cache.match with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.match supports ignoreVary');
+
+cache_test(function(cache) {
+ let has_cache_name = false;
+ const opts = {
+ get cacheName() {
+ has_cache_name = true;
+ return undefined;
+ }
+ };
+ return self.caches.open('foo')
+ .then(function() {
+ return cache.match('bar', opts);
+ })
+ .then(function() {
+ assert_false(has_cache_name,
+ 'Cache.match does not support cacheName option ' +
+ 'which was removed in CacheQueryOptions.');
+ });
+ }, 'Cache.match does not support cacheName option');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_equals(result, entries.cat.response,
+ 'Cache.match should ignore URL fragment.');
+ });
+ }, 'Cache.match with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('http')
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.match with string fragment "http" as query');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.match('http://example.com/c')
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.match should honor "Vary" header.');
+ });
+ }, 'Cache.match with responses containing "Vary" header');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com');
+ var response;
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ response = fetch_result;
+ assert_equals(
+ response.url, request_url,
+ '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
+ 'Reponse.url should return the URL of the response.');
+ return cache.put(request, response.clone());
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match should return a Response object that has the same ' +
+ 'properties as the stored response.');
+ return cache.match(response.url);
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should not match cache entry based on response URL.');
+ });
+ }, 'Cache.match with Request and Response objects with different URLs');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body.');
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body each time it is called.');
+ });
+ }, 'Cache.match invoked multiple times for the same Request/Response');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.blob();
+ })
+ .then(function(blob) {
+ var sliced = blob.slice(2,8);
+
+ return new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onloadend = function(event) {
+ resolve(event.target.result);
+ };
+ reader.readAsText(sliced);
+ });
+ })
+ .then(function(text) {
+ assert_equals(text, 'simple',
+ 'A Response blob returned by Cache.match should be ' +
+ 'sliceable.' );
+ });
+ }, 'Cache.match blob should be sliceable');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var request = new Request(entries.a.request.clone(), {method: 'POST'});
+ return cache.match(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not find a match');
+ });
+ }, 'Cache.match with POST Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.non_2xx_response.response;
+ return cache.match(entries.non_2xx_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.non_2xx_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored non-2xx response.');
+ });
+ }, 'Cache.match with a non-2xx Response');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.error_response.response;
+ return cache.match(entries.error_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.error_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored network error response.');
+ });
+ }, 'Cache.match with a network error Response');
+
+cache_test(function(cache) {
+ // This test validates that we can get a Response from the Cache API,
+ // clone it, and read just one side of the clone. This was previously
+ // bugged in FF for Responses with large bodies.
+ var data = [];
+ data.length = 80 * 1024;
+ data.fill('F');
+ var response;
+ return cache.put('/', new Response(data.toString()))
+ .then(function(result) {
+ return cache.match('/');
+ })
+ .then(function(r) {
+ // Make sure the original response is not GC'd.
+ response = r;
+ // Return only the clone. We purposefully test that the other
+ // half of the clone does not need to be read here.
+ return response.clone().text();
+ })
+ .then(function(text) {
+ assert_equals(text, data.toString(), 'cloned body text can be read correctly');
+ });
+ }, 'Cache produces large Responses that can be cloned and read correctly.');
+
+cache_test(async (cache) => {
+ const url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/cache-storage/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const response = await fetch(url);
+ await cache.put(new Request(url), response);
+ const cached_response = await cache.match(url);
+
+ const headers = cached_response.headers;
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ }, 'cors-exposed header should be stored correctly.');
+
+cache_test(async (cache) => {
+ // A URL that should load a resource with a known mime type.
+ const url = '/service-workers/cache-storage/resources/blank.html';
+ const expected_mime_type = 'text/html';
+
+ // Verify we get the expected mime type from the network. Note,
+ // we cannot use an exact match here since some browsers append
+ // character encoding information to the blob.type value.
+ const net_response = await fetch(url);
+ const net_mime_type = (await net_response.blob()).type;
+ assert_true(net_mime_type.includes(expected_mime_type),
+ 'network response should include the expected mime type');
+
+ // Verify we get the exact same mime type when reading the same
+ // URL resource back out of the cache.
+ await cache.add(url);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, net_mime_type,
+ 'network and cache response mime types should match');
+ }, 'MIME type should be set from content-header correctly.');
+
+cache_test(async (cache) => {
+ const url = '/dummy';
+ const original_type = 'text/html';
+ const override_type = 'text/plain';
+ const init_with_headers = {
+ headers: {
+ 'content-type': original_type
+ }
+ }
+
+ // Verify constructing a synthetic response with a content-type header
+ // gets the correct mime type.
+ const response = new Response('hello world', init_with_headers);
+ const original_response_type = (await response.blob()).type;
+ assert_true(original_response_type.includes(original_type),
+ 'original response should include the expected mime type');
+
+ // Verify overwriting the content-type header changes the mime type.
+ const overwritten_response = new Response('hello world', init_with_headers);
+ overwritten_response.headers.set('content-type', override_type);
+ const overwritten_response_type = (await overwritten_response.blob()).type;
+ assert_equals(overwritten_response_type, override_type,
+ 'mime type can be overridden');
+
+ // Verify the Response read from Cache uses the original mime type
+ // computed when it was first constructed.
+ const tmp = new Response('hello world', init_with_headers);
+ tmp.headers.set('content-type', override_type);
+ await cache.put(url, tmp);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, override_type,
+ 'overwritten and cached response mime types should match');
+ }, 'MIME type should reflect Content-Type headers of response.');
+
+cache_test(async (cache) => {
+ const url = new URL('./resources/vary.py?vary=foo',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const original_request = new Request(url, { mode: 'no-cors',
+ headers: { 'foo': 'bar' } });
+ const fetch_response = await fetch(original_request);
+ assert_equals(fetch_response.type, 'opaque');
+
+ await cache.put(original_request, fetch_response);
+
+ const match_response_1 = await cache.match(original_request);
+ assert_not_equals(match_response_1, undefined);
+
+ // Verify that cache.match() finds the entry even if queried with a varied
+ // header that does not match the cache key. Vary headers should be ignored
+ // for opaque responses.
+ const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } });
+ const match_response_2 = await cache.match(different_request);
+ assert_not_equals(match_response_2, undefined);
+}, 'Cache.match ignores vary headers on opaque response.');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js
new file mode 100644
index 0000000000..93c5517891
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-matchAll.https.any.js
@@ -0,0 +1,244 @@
+// META: title=Cache.matchAll
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('not-present-in-the-cache')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with an empty array on failure.');
+ });
+ }, 'Cache.matchAll with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request.url)
+ .then(function(result) {
+ assert_response_array_equals(result, [entries.a.response],
+ 'Cache.matchAll should match by URL.');
+ });
+ }, 'Cache.matchAll with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request)
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}),
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should not match HEAD Request.');
+ });
+ }, 'Cache.matchAll with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.matchAll(head_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with empty array for a ' +
+ 'mismatched method.');
+ return cache.matchAll(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [response],
+ 'Cache.matchAll with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.matchAll supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.matchAll(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.matchAll(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [vary_response],
+ 'Cache.matchAll with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.matchAll supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.cat.response,
+ ],
+ 'Cache.matchAll should ignore URL fragment.');
+ });
+ }, 'Cache.matchAll with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('http')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.matchAll with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll()
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll without parameters should match all entries.');
+ });
+ }, 'Cache.matchAll without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined)
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined, {})
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the query request, but is present in the cached ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'none-of-the-above'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the cached request, but is present in the query ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [entries.vary_cookie_is_cookie.response],
+ 'Cache.matchAll should match the entire header if a vary header ' +
+ 'is present in both the query and cached requests.');
+ });
+ }, 'Cache.matchAll with responses containing "Vary" header');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c',
+ {ignoreVary: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.response,
+ entries.vary_cookie_is_good.response,
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should support multiple vary request/response ' +
+ 'pairs.');
+ });
+ }, 'Cache.matchAll with multiple vary pairs');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js
new file mode 100644
index 0000000000..dbf2650a75
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-put.https.any.js
@@ -0,0 +1,411 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.put should resolve with undefined on success.');
+ });
+ }, 'Cache.put called with simple Request and Response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/simple.txt', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put called with Request and Response from fetch()');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ assert_false(request.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Request.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(request.bodyUsed,
+ 'Cache.put should not mark empty request\'s body used');
+ });
+ }, 'Cache.put with Request without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response();
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(response.bodyUsed,
+ 'Cache.put should not mark empty response\'s body used');
+ });
+ }, 'Cache.put with Response without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response.clone())
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new Request and Response.');
+ });
+ }, 'Cache.put with a Response containing an empty URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 200,
+ headers: [['Content-Type', 'text/plain']]
+ });
+ return cache.put(request, response)
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result.status, 200, 'Cache.put should store status.');
+ assert_equals(result.headers.get('Content-Type'), 'text/plain',
+ 'Cache.put should store headers.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with an empty response body');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 206,
+ headers: [['Content-Type', 'text/plain']]
+ });
+
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, response),
+ 'Cache.put should reject 206 Responses with a TypeError.');
+ }, 'Cache.put with synthetic 206 response');
+
+cache_test(function(cache, test) {
+ var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 206,
+ 'Test framework error: The status code should be 206.');
+ response = fetch_result.clone();
+ return promise_rejects_js(test, TypeError, cache.put(request, fetch_result));
+ });
+ }, 'Cache.put with HTTP 206 response');
+
+cache_test(function(cache, test) {
+ // We need to jump through some hoops to allow the test to perform opaque
+ // response filtering, but bypass the ORB safelist check. This is
+ // done, by forcing the MIME type retrieval to fail and the
+ // validation of partial first response to succeed.
+ var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)";
+ var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href);
+ test_url.hostname = REMOTE_HOST;
+ var request = new Request(test_url.href, { mode: 'no-cors' });
+ var response;
+ return fetch(request)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.type, 'opaque',
+ 'Test framework error: The response type should be opaque.');
+ assert_equals(fetch_result.status, 0,
+ 'Test framework error: The status code should be 0 for an ' +
+ ' opaque-filtered response. This is actually HTTP 206.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_not_equals(result, undefined,
+ 'Cache.put should store an entry for the opaque response');
+ });
+ }, 'Cache.put with opaque-filtered HTTP 206 response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 500,
+ 'Test framework error: The status code should be 500.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with HTTP 500 response');
+
+cache_test(function(cache) {
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(test_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(test_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ });
+ }, 'Cache.put called twice with matching Requests and different Responses');
+
+cache_test(function(cache) {
+ var first_url = test_url;
+ var second_url = first_url + '#(O_o)';
+ var third_url = first_url + '#fragment';
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(first_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(second_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ })
+ .then(function() {
+ return cache.put(new Request(third_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.keys();
+ })
+ .then(function(results) {
+ // Should match urls (without fragments or with different ones) to the
+ // same cache key. However, result.url should be the latest url used.
+ assert_equals(results[0].url, third_url);
+ return;
+ });
+}, 'Cache.put called multiple times with request URLs that differ only by a fragment');
+
+cache_test(function(cache) {
+ var url = 'http://example.com/foo';
+ return cache.put(url, new Response('some body'))
+ .then(function() { return cache.match(url); })
+ .then(function(response) { return response.text(); })
+ .then(function(body) {
+ assert_equals(body, 'some body',
+ 'Cache.put should accept a string as request.');
+ });
+ }, 'Cache.put with a string request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), 'Hello world!'),
+ 'Cache.put should only accept a Response object as the response.');
+ }, 'Cache.put with an invalid response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request('file:///etc/passwd'),
+ new Response(test_body)),
+ 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
+ }, 'Cache.put with a non-HTTP/HTTPS request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request('relative-url'), response.clone())
+ .then(function() {
+ return cache.match(new URL('relative-url', location.href).href);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should accept a relative URL ' +
+ 'as the request.');
+ });
+ }, 'Cache.put with a relative URL');
+
+cache_test(function(cache, test) {
+ var request = new Request('http://example.com/foo', { method: 'HEAD' });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for non-GET requests.');
+ }, 'Cache.put with a non-GET request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), null),
+ 'Cache.put should throw a TypeError for a null response.');
+ }, 'Cache.put with a null response');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url, {method: 'POST', body: test_body});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for a POST request.');
+ }, 'Cache.put with a POST request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return response.text().then(function() {
+ assert_true(
+ response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
+ 'The text() method should make the body disturbed.');
+ var request = new Request(test_url);
+ return cache.put(request, response).then(() => {
+ assert_unreached('cache.put should be rejected');
+ }, () => {});
+ });
+ }, 'Cache.put with a used response body');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request(test_url), response)
+ .then(function() {
+ assert_throws_js(TypeError, () => response.body.getReader());
+ });
+ }, 'getReader() after Cache.put');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body, { headers: { VARY: '*' }})),
+ 'Cache.put should reject VARY:* Responses with a TypeError.');
+ }, 'Cache.put with a VARY:* Response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body,
+ { headers: { VARY: 'Accept-Language,*' }})),
+ 'Cache.put should reject Responses with an embedded VARY:* with a ' +
+ 'TypeError.');
+ }, 'Cache.put with an embedded VARY:* Response');
+
+cache_test(async function(cache, test) {
+ const url = new URL('./resources/vary.py?vary=*',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const request = new Request(url, { mode: 'no-cors' });
+ const response = await fetch(request);
+ assert_equals(response.type, 'opaque');
+ await cache.put(request, response);
+ }, 'Cache.put with a VARY:* opaque response should not reject');
+
+cache_test(function(cache) {
+ var url = 'foo.html';
+ var redirectURL = 'http://example.com/foo-bar.html';
+ var redirectResponse = Response.redirect(redirectURL);
+ assert_equals(redirectResponse.headers.get('Location'), redirectURL,
+ 'Response.redirect() should set Location header.');
+ return cache.put(url, redirectResponse.clone())
+ .then(function() {
+ return cache.match(url);
+ })
+ .then(function(response) {
+ assert_response_equals(response, redirectResponse,
+ 'Redirect response is reproduced by the Cache API');
+ assert_equals(response.headers.get('Location'), redirectURL,
+ 'Location header is preserved by Cache API.');
+ });
+ }, 'Cache.put should store Response.redirect() correctly');
+
+cache_test(async (cache) => {
+ var request = new Request(test_url);
+ var response = new Response(new Blob([test_body]));
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ assert_equals(await cachedResponse.text(), test_body);
+ }, 'Cache.put called with simple Request and blob Response');
+
+cache_test(async (cache) => {
+ var formData = new FormData();
+ formData.append("name", "value");
+
+ var request = new Request(test_url);
+ var response = new Response(formData);
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ var cachedResponseText = await cachedResponse.text();
+ assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1);
+}, 'Cache.put called with simple Request and form data Response');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
new file mode 100644
index 0000000000..fd59ba464d
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
@@ -0,0 +1,64 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/test-helpers.js
+// META: script=/storage/buckets/resources/util.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftsBucket = await navigator.storageBuckets.open('drafts');
+
+ const cacheName = 'attachments';
+ const cacheKey = 'receipt1.txt';
+
+ var inboxCache = await inboxBucket.caches.open(cacheName);
+ var draftsCache = await draftsBucket.caches.open(cacheName);
+
+ await inboxCache.put(cacheKey, new Response('bread x 2'))
+ await draftsCache.put(cacheKey, new Response('eggs x 1'));
+
+ return inboxCache.match(cacheKey)
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'bread x 2', 'Wrong cache contents');
+ return draftsCache.match(cacheKey);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'eggs x 1', 'Wrong cache contents');
+ });
+}, 'caches from different buckets have different contents');
+
+promise_test(async function(test) {
+ await prepareForBucketTest(test);
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftBucket = await navigator.storageBuckets.open('drafts');
+
+ var caches = inboxBucket.caches;
+ var attachments = await caches.open('attachments');
+ await attachments.put('receipt1.txt', new Response('bread x 2'));
+ var result = await attachments.match('receipt1.txt');
+ assert_equals(await result.text(), 'bread x 2');
+
+ await navigator.storageBuckets.delete('inbox');
+
+ await promise_rejects_dom(
+ test, 'UnknownError', caches.open('attachments'));
+
+ // Also test when `caches` is first accessed after the deletion.
+ await navigator.storageBuckets.delete('drafts');
+ return promise_rejects_dom(
+ test, 'UnknownError', draftBucket.caches.open('attachments'));
+}, 'cache.open promise is rejected when bucket is gone');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
new file mode 100644
index 0000000000..f19522be1b
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-keys.https.any.js
@@ -0,0 +1,35 @@
+// META: title=CacheStorage.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_cache_list =
+ ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
+
+promise_test(function(test) {
+ return self.caches.keys()
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ return Promise.all(keys.map(function(key) {
+ return self.caches.delete(key);
+ }));
+ })
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ assert_array_equals(keys,
+ test_cache_list,
+ 'CacheStorage.keys should only return ' +
+ 'existing caches.');
+ });
+ }, 'CacheStorage keys');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js
new file mode 100644
index 0000000000..0c31b72629
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage-match.https.any.js
@@ -0,0 +1,245 @@
+// META: title=CacheStorage.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+(function() {
+ var next_index = 1;
+
+ // Returns a transaction (request, response, and url) for a unique URL.
+ function create_unique_transaction(test) {
+ var uniquifier = String(next_index++);
+ var url = 'http://example.com/' + uniquifier;
+
+ return {
+ request: new Request(url),
+ response: new Response('hello'),
+ url: url
+ };
+ }
+
+ self.create_unique_transaction = create_unique_transaction;
+})();
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch with no cache name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['a', 'b', 'c'];
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch from one of many caches');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['x', 'y', 'z'];
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }))
+ .then(function() { return self.caches.open('x'); })
+ .then(function(cache) {
+ return cache.put(transaction.request.clone(),
+ transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'x'});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'y'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'Cache y should not have a response for the request.');
+ });
+}, 'CacheStorageMatch from one of many caches by name');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.url, transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch a string request');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(new Request(transaction.request.url,
+ {method: 'HEAD'}));
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'A HEAD request should not be matched');
+ });
+}, 'CacheStorageMatch a HEAD request');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.match(transaction.request)
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ });
+}, 'CacheStorageMatch with no cached entry');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.delete('foo')
+ .then(function() {
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should not exist.");
+ return self.caches.match(transaction.request, {cacheName: 'foo'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The match with bad cache name should resolve to ' +
+ 'undefined.');
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should still not exist.");
+ });
+}, 'CacheStorageMatch with no caches available but name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return self.caches.delete('')
+ .then(function() {
+ return self.caches.has('');
+ })
+ .then(function(has_cache) {
+ assert_false(has_cache, "The cache should not exist.");
+ return cache.put(transaction.request, transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ return self.caches.open('');
+ })
+ .then(function(cache) {
+ return cache.put(transaction.request, transaction.response);
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should be matched.');
+ return self.caches.delete('');
+ });
+}, 'CacheStorageMatch with empty cache name provided');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/?foo');
+ var no_query_request = new Request('http://example.com/');
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(no_query_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched query.');
+ return self.caches.match(no_query_request.clone(),
+ {ignoreSearch: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreSearch should ignore the ' +
+ 'query of the request.');
+ });
+ }, 'CacheStorageMatch supports ignoreSearch');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched method.');
+ return self.caches.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return self.caches.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ ' mismatched vary.');
+ return self.caches.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'CacheStorageMatch with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'CacheStorageMatch supports ignoreVary');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js
new file mode 100644
index 0000000000..b7d5af7b53
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cache-storage.https.any.js
@@ -0,0 +1,239 @@
+// META: title=CacheStorage
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/foo';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should return a Cache.');
+ });
+ }, 'CacheStorage.open');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/bar';
+ var first_cache = null;
+ var second_cache = null;
+ return self.caches.open(cache_name)
+ .then(function(cache) {
+ first_cache = cache;
+ return self.caches.delete(cache_name);
+ })
+ .then(function() {
+ return first_cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.keys();
+ })
+ .then(function(cache_names) {
+ assert_equals(cache_names.indexOf(cache_name), -1);
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ second_cache = cache;
+ return second_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 0);
+ return first_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 1);
+ // Clean up
+ return self.caches.delete(cache_name);
+ });
+ }, 'CacheStorage.delete dooms, but does not delete immediately');
+
+promise_test(function(t) {
+ // Note that this test may collide with other tests running in the same
+ // origin that also uses an empty cache name.
+ var cache_name = '';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should accept an empty name.');
+ });
+ }, 'CacheStorage.open with an empty name');
+
+promise_test(function(t) {
+ return promise_rejects_js(
+ t,
+ TypeError,
+ self.caches.open(),
+ 'CacheStorage.open should throw TypeError if called with no arguments.');
+ }, 'CacheStorage.open with no arguments');
+
+promise_test(function(t) {
+ var test_cases = [
+ {
+ name: 'cache-storage/lowercase',
+ should_not_match:
+ [
+ 'cache-storage/Lowercase',
+ ' cache-storage/lowercase',
+ 'cache-storage/lowercase '
+ ]
+ },
+ {
+ name: 'cache-storage/has a space',
+ should_not_match:
+ [
+ 'cache-storage/has'
+ ]
+ },
+ {
+ name: 'cache-storage/has\000_in_the_name',
+ should_not_match:
+ [
+ 'cache-storage/has',
+ 'cache-storage/has_in_the_name'
+ ]
+ }
+ ];
+ return Promise.all(test_cases.map(function(testcase) {
+ var cache_name = testcase.name;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() {
+ return self.caches.has(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.has should return true for existing ' +
+ 'cache.');
+ })
+ .then(function() {
+ return Promise.all(
+ testcase.should_not_match.map(function(cache_name) {
+ return self.caches.has(cache_name)
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should only perform ' +
+ 'exact matches on cache names.');
+ });
+ }));
+ })
+ .then(function() {
+ return self.caches.delete(cache_name);
+ });
+ }));
+ }, 'CacheStorage.has with existing cache');
+
+promise_test(function(t) {
+ return self.caches.has('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should return false for ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.has with nonexistent cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/open';
+ var cache;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ cache = result;
+ })
+ .then(function() {
+ return cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result instanceof Cache,
+ 'CacheStorage.open should return a Cache object');
+ assert_not_equals(result, cache,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object each time its called.');
+ return Promise.all([cache.keys(), result.keys()]);
+ })
+ .then(function(results) {
+ var expected_urls = results[0].map(function(r) { return r.url });
+ var actual_urls = results[1].map(function(r) { return r.url });
+ assert_array_equals(actual_urls, expected_urls,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object for the same backing store.');
+ });
+ }, 'CacheStorage.open with existing cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/delete';
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() { return self.caches.delete(cache_name); })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.delete should return true after ' +
+ 'deleting an existing cache.');
+ })
+
+ .then(function() { return self.caches.has(cache_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage.has should return false after ' +
+ 'fulfillment of CacheStorage.delete promise.');
+ });
+ }, 'CacheStorage.delete with existing cache');
+
+promise_test(function(t) {
+ return self.caches.delete('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.delete should return false for a ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.delete with nonexistent cache');
+
+promise_test(function(t) {
+ var unpaired_name = 'unpaired\uD800';
+ var converted_name = 'unpaired\uFFFD';
+
+ // The test assumes that a cache with converted_name does not
+ // exist, but if the implementation fails the test then such
+ // a cache will be created. Start off in a fresh state by
+ // deleting all caches.
+ return delete_all_caches()
+ .then(function() {
+ return self.caches.has(converted_name);
+ })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'Test setup failure: cache should not exist');
+ })
+ .then(function() { return self.caches.open(unpaired_name); })
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(keys.indexOf(unpaired_name) !== -1,
+ 'keys should include cache with bad name');
+ })
+ .then(function() { return self.caches.has(unpaired_name); })
+ .then(function(cache_exists) {
+ assert_true(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ })
+ .then(function() { return self.caches.has(converted_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ });
+ }, 'CacheStorage names are DOMStrings not USVStrings');
+
+done();
diff --git a/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js
new file mode 100644
index 0000000000..eba312c273
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/common.https.window.js
@@ -0,0 +1,44 @@
+// META: title=Cache Storage: Verify that Window and Workers see same storage
+// META: timeout=long
+
+function wait_for_message(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('message', function listener(e) {
+ resolve(e.data);
+ worker.removeEventListener('message', listener);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var cache_name = 'common-test';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ var worker = new Worker('resources/common-worker.js');
+ worker.postMessage({name: cache_name});
+ return wait_for_message(worker);
+ })
+ .then(function(message) {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ return Promise.all([
+ cache.match('https://example.com/a'),
+ cache.match('https://example.com/b'),
+ cache.match('https://example.com/c')
+ ]);
+ })
+ .then(function(responses) {
+ return Promise.all(responses.map(
+ function(response) { return response.text(); }
+ ));
+ })
+ .then(function(bodies) {
+ assert_equals(bodies[0], 'a',
+ 'Body should match response put by worker');
+ assert_equals(bodies[1], 'b',
+ 'Body should match response put by worker');
+ assert_equals(bodies[2], 'c',
+ 'Body should match response put by worker');
+ });
+}, 'Window sees cache puts by Worker');
diff --git a/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
new file mode 100644
index 0000000000..ec930a87d9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script type="module">
+ const cache = await window.caches.open('cache_name_0')
+ await cache.add("")
+ const resp1 = await cache.match("")
+ const readStream = resp1.body
+ // Cloning will open the stream via NS_AsyncCopy in Gecko
+ resp1.clone()
+ // Give a little bit of time
+ await new Promise(setTimeout)
+ // At this point the previous open operation is about to finish but not yet.
+ // It will finish after the second open operation is made, potentially causing incorrect state.
+ await readStream.getReader().read();
+ document.documentElement.classList.remove('test-wait')
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html
new file mode 100644
index 0000000000..0fe4a0a0ac
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/credentials.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cache Storage: Verify credentials are respected by Cache operations</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<style>iframe { display: none; }</style>
+<script>
+
+var worker = "./resources/credentials-worker.js";
+var scope = "./resources/credentials-iframe.html";
+promise_test(function(t) {
+ return self.caches.delete('credentials')
+ .then(function() {
+ return service_worker_unregister_and_register(t, worker, scope)
+ })
+ .then(function(reg) {
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ frame.contentWindow.postMessage([
+ {name: 'file.txt', username: 'aa', password: 'bb'},
+ {name: 'file.txt', username: 'cc', password: 'dd'},
+ {name: 'file.txt'}
+ ], '*');
+ return new Promise(function(resolve, reject) {
+ window.onmessage = t.step_func(function(e) {
+ resolve(e.data);
+ });
+ });
+ })
+ .then(function(data) {
+ assert_equals(data.length, 3, 'three entries should be present');
+ assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2,
+ 'two entries should contain credentials');
+ assert_true(data.some(function(url) { return /aa:bb@/.test(url); }),
+ 'entry with credentials aa:bb should be present');
+ assert_true(data.some(function(url) { return /cc:dd@/.test(url); }),
+ 'entry with credentials cc:dd should be present');
+ });
+}, "Cache API matching includes credentials");
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html
new file mode 100644
index 0000000000..1cfc256908
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/cross-partition.https.tentative.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const cache_exists_js = (cache_name, response_queue_name) => `
+ try {
+ const exists = await self.caches.has("${cache_name}");
+ if (exists) {
+ await send("${response_queue_name}", "true");
+ } else {
+ await send("${response_queue_name}", "false");
+ }
+ } catch {
+ await send("${response_queue_name}", "exception");
+ }
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+ // Create a same-origin iframe in a cross-site popup.
+ const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+ await send(not_same_site_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_1_uuid = await receive(response_queue_uuid);
+
+ // Create a same-origin iframe in a same-site popup.
+ const same_origin_popup_uuid = newPopup(t, same_site_origin);
+ await send(same_origin_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_2_uuid = await receive(response_queue_uuid);
+
+ return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(iframe_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site iframe");
+ }
+
+ await send(iframe_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site iframe");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition iframe");
+
+const newWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new Worker(worker_url);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newWorker = ${newWorker};
+ await send("${response_queue_uuid}", newWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a dedicated worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a dedicated worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new SharedWorker(worker_url, worker_token);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newSharedWorker = ${newSharedWorker};
+ await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a shared worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a shared worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_service_worker_path +
+ `&uuid=${worker_token}`;
+ const worker_url_path = executor_service_worker_path.substring(0,
+ executor_service_worker_path.lastIndexOf('/'));
+ const scope = worker_url_path + "/not-used/";
+ const reg = await navigator.serviceWorker.register(worker_url,
+ {'scope': scope});
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newServiceWorker = ${newServiceWorker};
+ await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a service worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_2_uuid, "self.registration.unregister();"));
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ // Create a service worker in the cross-top-level-site iframe. Note that
+ // if service workers are unpartitioned then this new service worker would
+ // replace the one created above. This is why we wait to create the second
+ // service worker until after we are done with the first one.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_1_uuid, "self.registration.unregister();"));
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition service worker");
+</script>
+</body>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html
new file mode 100644
index 0000000000..a3c3a4689a
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000000..ee574d2cb7
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ event.respondWith(Promise.resolve().then(async () => {
+ const name = params.get('name');
+ await caches.delete('foo');
+ const cache = await caches.open('foo');
+ await cache.put(event.request, new Response('hello'));
+ const keys = await cache.keys();
+
+ const original = event.request[name];
+ const stored = keys[0][name];
+ return new Response(`original: ${original}, stored: ${stored}`);
+ }));
+ });
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js
new file mode 100644
index 0000000000..d0e8544b56
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/common-worker.js
@@ -0,0 +1,15 @@
+self.onmessage = function(e) {
+ var cache_name = e.data.name;
+
+ self.caches.open(cache_name)
+ .then(function(cache) {
+ return Promise.all([
+ cache.put('https://example.com/a', new Response('a')),
+ cache.put('https://example.com/b', new Response('b')),
+ cache.put('https://example.com/c', new Response('c'))
+ ]);
+ })
+ .then(function() {
+ self.postMessage('ok');
+ });
+};
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html
new file mode 100644
index 0000000000..00702df9e5
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Controlled frame for Cache API test with credentials</title>
+<script>
+
+function xhr(url, username, password) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest(), async = true;
+ xhr.open('GET', url, async, username, password);
+ xhr.send();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== XMLHttpRequest.DONE)
+ return;
+ if (xhr.status === 200) {
+ resolve(xhr.responseText);
+ } else {
+ reject(new Error(xhr.statusText));
+ }
+ };
+ });
+}
+
+window.onmessage = function(e) {
+ Promise.all(e.data.map(function(item) {
+ return xhr(item.name, item.username, item.password);
+ }))
+ .then(function() {
+ navigator.serviceWorker.controller.postMessage('keys');
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, '*');
+ };
+ });
+};
+
+</script>
+<body>
+Hello? Yes, this is iframe.
+</body>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js
new file mode 100644
index 0000000000..43965b5fe4
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/credentials-worker.js
@@ -0,0 +1,59 @@
+var cache_name = 'credentials';
+
+function assert_equals(actual, expected, message) {
+ if (!Object.is(actual, expected))
+ throw Error(message + ': expected: ' + expected + ', actual: ' + actual);
+}
+
+self.onfetch = function(e) {
+ if (!/\.txt$/.test(e.request.url)) return;
+ var content = e.request.url;
+ var cache;
+ e.respondWith(
+ self.caches.open(cache_name)
+ .then(function(result) {
+ cache = result;
+ return cache.put(e.request, new Response(content));
+ })
+
+ .then(function() { return cache.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.match() body should match');
+ })
+
+ .then(function() { return cache.matchAll(e.request); })
+ .then(function(results) {
+ assert_equals(results.length, 1, 'Should have one response');
+ return results[0].text();
+ })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.matchAll() body should match');
+ })
+
+ .then(function() { return self.caches.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'CacheStorage.match() body should match');
+ })
+
+ .then(function() {
+ return new Response('dummy');
+ })
+ );
+};
+
+self.onmessage = function(e) {
+ if (e.data === 'keys') {
+ self.caches.open(cache_name)
+ .then(function(cache) { return cache.keys(); })
+ .then(function(requests) {
+ var urls = requests.map(function(request) { return request.url; });
+ self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage(urls);
+ });
+ });
+ });
+ }
+};
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py
new file mode 100644
index 0000000000..b7109f4787
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/fetch-status.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return int(request.GET[b"status"]), [], b""
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html
new file mode 100644
index 0000000000..a2f1e502bb
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>ok</title>
+<script>
+window.onmessage = function(e) {
+ var id = e.data.id;
+ try {
+ var name = 'checkallowed';
+ self.caches.open(name).then(function (cache) {
+ self.caches.delete(name);
+ window.parent.postMessage({id: id, result: 'allowed'}, '*');
+ }).catch(function(e) {
+ window.parent.postMessage({id: id, result: 'denied', name: e.name, message: e.message}, '*');
+ });
+ } catch (e) {
+ window.parent.postMessage({id: id, result: 'unexpecteddenied', name: e.name, message: e.message}, '*');
+ }
+};
+</script>
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt
new file mode 100644
index 0000000000..9e3cb91fb9
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js
new file mode 100644
index 0000000000..050ac0b542
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/test-helpers.js
@@ -0,0 +1,272 @@
+(function() {
+ var next_cache_index = 1;
+
+ // Returns a promise that resolves to a newly created Cache object. The
+ // returned Cache will be destroyed when |test| completes.
+ function create_temporary_cache(test) {
+ var uniquifier = String(++next_cache_index);
+ var cache_name = self.location.pathname + '/' + uniquifier;
+
+ test.add_cleanup(function() {
+ self.caches.delete(cache_name);
+ });
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ });
+ }
+
+ self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+// cache_test(function(cache) {
+// // Do something with |cache|, which is a Cache object.
+// }, "Some Cache test");
+function cache_test(test_function, description) {
+ promise_test(function(test) {
+ return create_temporary_cache(test)
+ .then(function(cache) { return test_function(cache, test); });
+ }, description);
+}
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+var simple_entries = [
+ {
+ name: 'a',
+ request: new Request('http://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'b',
+ request: new Request('http://example.com/b'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_with_query',
+ request: new Request('http://example.com/a?q=r'),
+ response: new Response('')
+ },
+
+ {
+ name: 'A',
+ request: new Request('http://example.com/A'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_https',
+ request: new Request('https://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_org',
+ request: new Request('http://example.org/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat',
+ request: new Request('http://example.com/cat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'catmandu',
+ request: new Request('http://example.com/catmandu'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_num_lives',
+ request: new Request('http://example.com/cat?lives=9'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_in_the_hat',
+ request: new Request('http://example.com/cat/in/the/hat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'non_2xx_response',
+ request: new Request('http://example.com/non2xx'),
+ response: new Response('', {status: 404, statusText: 'nope'})
+ },
+
+ {
+ name: 'error_response',
+ request: new Request('http://example.com/error'),
+ response: Response.error()
+ },
+];
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+// These contain a mix of test cases that use Vary headers.
+var vary_entries = [
+ {
+ name: 'vary_cookie_is_cookie',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_is_good',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-good-enough-for-me'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_absent',
+ request: new Request('http://example.com/c'),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ }
+];
+
+// Run |test_function| with a Cache object and a map of entries. Prior to the
+// call, the Cache is populated by cache entries from |entries|. The latter is
+// expected to be an Object mapping arbitrary keys to objects of the form
+// {request: <Request object>, response: <Response object>}. Entries are
+// serially added to the cache in the order specified.
+//
+// |test_function| should return a Promise that can be used with promise_test.
+function prepopulated_cache_test(entries, test_function, description) {
+ cache_test(function(cache) {
+ var p = Promise.resolve();
+ var hash = {};
+ entries.forEach(function(entry) {
+ hash[entry.name] = entry;
+ p = p.then(function() {
+ return cache.put(entry.request.clone(), entry.response.clone())
+ .catch(function(e) {
+ assert_unreached(
+ 'Test setup failed for entry ' + entry.name + ': ' + e
+ );
+ });
+ });
+ });
+ return p
+ .then(function() {
+ assert_equals(Object.keys(hash).length, entries.length);
+ })
+ .then(function() {
+ return test_function(cache, hash);
+ });
+ }, description);
+}
+
+// Helper for testing with Headers objects. Compares Headers instances
+// by serializing |expected| and |actual| to arrays and comparing.
+function assert_header_equals(actual, expected, description) {
+ assert_class_string(actual, "Headers", description);
+ var header;
+ var actual_headers = [];
+ var expected_headers = [];
+ for (header of actual)
+ actual_headers.push(header[0] + ": " + header[1]);
+ for (header of expected)
+ expected_headers.push(header[0] + ": " + header[1]);
+ assert_array_equals(actual_headers, expected_headers,
+ description + " Headers differ.");
+}
+
+// Helper for testing with Response objects. Compares simple
+// attributes defined on the interfaces, as well as the headers. It
+// does not compare the response bodies.
+function assert_response_equals(actual, expected, description) {
+ assert_class_string(actual, "Response", description);
+ ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals. The order
+// is not significant.
+//
+// |expected| is assumed to not contain any duplicates.
+function assert_response_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_response_in_array treats the first argument as being
+ // 'actual', and the second as being 'expected array'. We are
+ // switching them around because we want to be resilient
+ // against the |actual| array containing duplicates.
+ assert_response_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_response_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_response_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Equivalent to assert_in_array, but uses assert_response_equals.
+function assert_response_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_response_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Helper for testing with Request objects. Compares simple
+// attributes defined on the interfaces, as well as the headers.
+function assert_request_equals(actual, expected, description) {
+ assert_class_string(actual, "Request", description);
+ ["url"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Requests as determined by assert_request_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_request_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_request_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Deletes all caches, returning a promise indicating success.
+function delete_all_caches() {
+ return self.caches.keys()
+ .then(function(keys) {
+ return Promise.all(keys.map(self.caches.delete.bind(self.caches)));
+ });
+}
diff --git a/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py
new file mode 100644
index 0000000000..7fde1b1094
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/resources/vary.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ if b"clear-vary-value-override-cookie" in request.GET:
+ response.unset_cookie(b"vary-value-override")
+ return b"vary cookie cleared"
+
+ set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie",
+ default=b"")
+ if set_cookie_vary:
+ response.set_cookie(b"vary-value-override", set_cookie_vary)
+ return b"vary cookie set"
+
+ # If there is a vary-value-override cookie set, then use its value
+ # for the VARY header no matter what the query string is set to. This
+ # override is necessary to test the case when two URLs are identical
+ # (including query), but differ by VARY header.
+ cookie_vary = request.cookies.get(b"vary-value-override")
+ if cookie_vary:
+ response.headers.set(b"vary", str(cookie_vary))
+ else:
+ # If there is no cookie, then use the query string value, if present.
+ query_vary = request.GET.first(b"vary", default=b"")
+ if query_vary:
+ response.headers.set(b"vary", query_vary)
+
+ return b"vary response"
diff --git a/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html
new file mode 100644
index 0000000000..098fa89daf
--- /dev/null
+++ b/testing/web-platform/tests/service-workers/cache-storage/sandboxed-iframes.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Cache Storage: Verify access in sandboxed iframes</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function load_iframe(src, sandbox) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function() { resolve(iframe); };
+
+ iframe.sandbox = sandbox;
+ iframe.src = src;
+
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(id) {
+ return new Promise(function(resolve) {
+ self.addEventListener('message', function listener(e) {
+ if (e.data.id === id) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+var counter = 0;
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts allow-same-origin')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'allowed',
+ 'Access should be allowed if sandbox has allow-same-origin');
+ });
+}, 'Sandboxed iframe with allow-same-origin is allowed access');
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'denied',
+ 'Access should be denied if sandbox lacks allow-same-origin');
+ assert_equals(message.name, 'SecurityError',
+ 'Failure should be a SecurityError');
+ });
+}, 'Sandboxed iframe without allow-same-origin is denied access');
+
+</script>