diff options
Diffstat (limited to 'testing/web-platform/tests/service-workers/cache-storage')
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> |