diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/fetch | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fetch')
760 files changed, 60299 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/META.yml b/testing/web-platform/tests/fetch/META.yml new file mode 100644 index 0000000000..81432ff5f5 --- /dev/null +++ b/testing/web-platform/tests/fetch/META.yml @@ -0,0 +1,7 @@ +spec: https://fetch.spec.whatwg.org/ +suggested_reviewers: + - jdm + - youennf + - annevk + - mnot + - yutakahirano diff --git a/testing/web-platform/tests/fetch/README.md b/testing/web-platform/tests/fetch/README.md new file mode 100644 index 0000000000..dcaad0219d --- /dev/null +++ b/testing/web-platform/tests/fetch/README.md @@ -0,0 +1,6 @@ +Tests for the [Fetch Standard](https://fetch.spec.whatwg.org/). + +More Fetch tests can be found in + +* /cors +* /xhr diff --git a/testing/web-platform/tests/fetch/api/abort/cache.https.any.js b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js new file mode 100644 index 0000000000..bdaf0e69e5 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js @@ -0,0 +1,47 @@ +// META: title=Request signals & the cache API +// META: global=window,worker + +promise_test(async () => { + await caches.delete('test'); + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/data.json', { signal }); + + const cache = await caches.open('test'); + await cache.put(request, new Response('')); + + const requests = await cache.keys(); + + assert_equals(requests.length, 1, 'Ensuring cleanup worked'); + + const [cachedRequest] = requests; + + controller.abort(); + + assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); + + const data = await fetch(cachedRequest).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signals are not stored in the cache API"); + +promise_test(async () => { + await caches.delete('test'); + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/data.json', { signal }); + controller.abort(); + + const cache = await caches.open('test'); + await cache.put(request, new Response('')); + + const requests = await cache.keys(); + + assert_equals(requests.length, 1, 'Ensuring cleanup worked'); + + const [cachedRequest] = requests; + + assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); + + const data = await fetch(cachedRequest).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signals are not stored in the cache API, even if they're already aborted"); diff --git a/testing/web-platform/tests/fetch/api/abort/destroyed-context.html b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html new file mode 100644 index 0000000000..161d39bd9c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// This is a regression test for crbug.com/860063. +window.controller = new AbortController(); +async_test(t => { + onmessage = t.step_func(event => { + assert_equals(event.data, 'started'); + const iframe = document.querySelector('iframe'); + document.body.removeChild(iframe); + controller.abort(); + t.done(); + }); +}, 'aborting a fetch in a destroyed context should not crash'); +</script> +<iframe srcdoc=" + <!DOCTYPE html> + <meta charset=utf-8> + <script> + fetch('../resources/infinite-slow-response.py', { signal: parent.controller.signal }).then(() => { + parent.postMessage('started', '*'); + }); + </script> + "> +</iframe> diff --git a/testing/web-platform/tests/fetch/api/abort/general.any.js b/testing/web-platform/tests/fetch/api/abort/general.any.js new file mode 100644 index 0000000000..3727bb42af --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/general.any.js @@ -0,0 +1,572 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../request/request-error.js + +const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text']; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +// This is used to close connections that weren't correctly closed during the tests, +// otherwise you can end up running out of HTTP connections. +let requestAbortKeys = []; + +function abortRequests() { + const keys = requestAbortKeys; + requestAbortKeys = []; + return Promise.all( + keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`)) + ); +} + +const hostInfo = get_host_info(); +const urlHostname = hostInfo.REMOTE_HOST; + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const fetchPromise = fetch('../resources/data.json', { signal }); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Aborting rejects with AbortError"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(error1); + + const fetchPromise = fetch('../resources/data.json', { signal }); + + await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason'); +}, "Aborting rejects with abort reason"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const url = new URL('../resources/data.json', location); + url.hostname = urlHostname; + + const fetchPromise = fetch(url, { + signal, + mode: 'no-cors' + }); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Aborting rejects with AbortError - no-cors"); + +// Test that errors thrown from the request constructor take priority over abort errors. +// badRequestArgTests is from response-error.js +for (const { args, testName } of badRequestArgTests) { + promise_test(async t => { + try { + // If this doesn't throw, we'll effectively skip the test. + // It'll fail properly in ../request/request-error.html + new Request(...args); + } + catch (err) { + const controller = new AbortController(); + controller.abort(); + + // Add signal to 2nd arg + args[1] = args[1] || {}; + args[1].signal = controller.signal; + await promise_rejects_js(t, TypeError, fetch(...args)); + } + }, `TypeError from request constructor takes priority - ${testName}`); +} + +test(() => { + const request = new Request(''); + assert_true(Boolean(request.signal), "Signal member is present & truthy"); + assert_equals(request.signal.constructor, AbortSignal); +}, "Request objects have a signal property"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + assert_true(Boolean(request.signal), "Signal member is present & truthy"); + assert_equals(request.signal.constructor, AbortSignal); + assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); + assert_true(request.signal.aborted, `Request's signal has aborted`); + + const fetchPromise = fetch(request); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(error1); + + const request = new Request('../resources/data.json', { signal }); + + assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); + assert_true(request.signal.aborted, `Request's signal has aborted`); + assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`); + + const fetchPromise = fetch(request); + + await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason"); +}, "Signal on request object should also have abort reason"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + const requestFromRequest = new Request(request); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json'); + const requestFromRequest = new Request(request, { signal }); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object, with signal on second request"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal: new AbortController().signal }); + const requestFromRequest = new Request(request, { signal }); + + const fetchPromise = fetch(requestFromRequest); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal on request object created from request object, with signal on second request overriding another"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + const fetchPromise = fetch(request, {method: 'POST'}); + + await promise_rejects_dom(t, "AbortError", fetchPromise); +}, "Signal retained after unrelated properties are overridden by fetch"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { signal }); + + const data = await fetch(request, { signal: null }).then(r => r.json()); + assert_equals(data.key, 'value', 'Fetch fully completes'); +}, "Signal removed by setting to null"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const log = []; + + await Promise.all([ + fetch('../resources/data.json', { signal }).then( + () => assert_unreached("Fetch must not resolve"), + () => log.push('fetch-reject') + ), + Promise.resolve().then(() => log.push('next-microtask')) + ]); + + assert_array_equals(log, ['fetch-reject', 'next-microtask']); +}, "Already aborted signal rejects immediately"); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('../resources/data.json', { + signal, + method: 'POST', + body: 'foo', + headers: { 'Content-Type': 'text/plain' } + }); + + await fetch(request).catch(() => {}); + + assert_true(request.bodyUsed, "Body has been used"); +}, "Request is still 'used' if signal is aborted before fetching"); + +for (const bodyMethod of BODY_METHODS) { + promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + + const log = []; + const response = await fetch('../resources/data.json', { signal }); + + controller.abort(); + + const bodyPromise = response[bodyMethod](); + + await Promise.all([ + bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)), + Promise.resolve().then(() => log.push('next-microtask')) + ]); + + await promise_rejects_dom(t, "AbortError", bodyPromise); + + assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']); + }, `response.${bodyMethod}() rejects if already aborted`); +} + +promise_test(async (t) => { + const controller = new AbortController(); + const signal = controller.signal; + + const res = await fetch('../resources/data.json', { signal }); + controller.abort(); + + await promise_rejects_dom(t, 'AbortError', res.text()); + await promise_rejects_dom(t, 'AbortError', res.text()); +}, 'Call text() twice on aborted response'); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + controller.abort(); + + await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {}); + + // I'm hoping this will give the browser enough time to (incorrectly) make the request + // above, if it intends to. + await fetch('../resources/data.json').then(r => r.json()); + + const response = await fetch(`../resources/stash-take.py?key=${stateKey}`); + const data = await response.json(); + + assert_equals(data, null, "Request hasn't been made to the server"); +}, "Already aborted signal does not make request"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const fetches = []; + + for (let i = 0; i < 3; i++) { + const abortKey = token(); + requestAbortKeys.push(abortKey); + + fetches.push( + fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) + ); + } + + for (const fetchPromise of fetches) { + await promise_rejects_dom(t, "AbortError", fetchPromise); + } +}, "Already aborted signal can be used for many fetches"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + + await fetch('../resources/data.json', { signal }).then(r => r.json()); + + controller.abort(); + + const fetches = []; + + for (let i = 0; i < 3; i++) { + const abortKey = token(); + requestAbortKeys.push(abortKey); + + fetches.push( + fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) + ); + } + + for (const fetchPromise of fetches) { + await promise_rejects_dom(t, "AbortError", fetchPromise); + } +}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + + const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + controller.abort(); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Underlying connection is closed when aborting after receiving response"); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location); + url.hostname = urlHostname; + + await fetch(url, { + signal, + mode: 'no-cors' + }); + + const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location); + stashTakeURL.hostname = urlHostname; + + const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + controller.abort(); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(stashTakeURL).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Underlying connection is closed when aborting after receiving response - no-cors"); + +for (const bodyMethod of BODY_METHODS) { + promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + + const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + assert_equals(beforeAbortResult, "open", "Connection is open"); + + const bodyPromise = response[bodyMethod](); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", bodyPromise); + + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } + }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`); +} + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + const reader = response.body.getReader(); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", reader.read()); + await promise_rejects_dom(t, "AbortError", reader.closed); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Stream errors once aborted. Underlying connection closed."); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + requestAbortKeys.push(abortKey); + + const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); + const reader = response.body.getReader(); + + await reader.read(); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", reader.read()); + await promise_rejects_dom(t, "AbortError", reader.closed); + + // The connection won't close immediately, but it should close at some point: + const start = Date.now(); + + while (true) { + // Stop spinning if 10 seconds have passed + if (Date.now() - start > 10000) throw Error('Timed out'); + + const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); + if (afterAbortResult == 'closed') break; + } +}, "Stream errors once aborted, after reading. Underlying connection closed."); + +promise_test(async t => { + await abortRequests(); + + const controller = new AbortController(); + const signal = controller.signal; + + const response = await fetch(`../resources/empty.txt`, { signal }); + + // Read whole response to ensure close signal has sent. + await response.clone().text(); + + const reader = response.body.getReader(); + + controller.abort(); + + const item = await reader.read(); + + assert_true(item.done, "Stream is done"); +}, "Stream will not error if body is empty. It's closed with an empty queue before it errors."); + +promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + let cancelReason; + + const body = new ReadableStream({ + pull(controller) { + controller.enqueue(new Uint8Array([42])); + }, + cancel(reason) { + cancelReason = reason; + } + }); + + const fetchPromise = fetch('../resources/empty.txt', { + body, signal, + method: 'POST', + duplex: 'half', + headers: { + 'Content-Type': 'text/plain' + } + }); + + assert_true(!!cancelReason, 'Cancel called sync'); + assert_equals(cancelReason.constructor, DOMException); + assert_equals(cancelReason.name, 'AbortError'); + + await promise_rejects_dom(t, "AbortError", fetchPromise); + + const fetchErr = await fetchPromise.catch(e => e); + + assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance"); +}, "Readable stream synchronously cancels with AbortError if aborted before reading"); + +test(() => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + + const request = new Request('.', { signal }); + const requestSignal = request.signal; + + const clonedRequest = request.clone(); + + assert_equals(requestSignal, request.signal, "Original request signal the same after cloning"); + assert_true(request.signal.aborted, "Original request signal aborted"); + assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal"); + assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted"); +}, "Signal state is cloned"); + +test(() => { + const controller = new AbortController(); + const signal = controller.signal; + + const request = new Request('.', { signal }); + const clonedRequest = request.clone(); + + const log = []; + + request.signal.addEventListener('abort', () => log.push('original-aborted')); + clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted')); + + controller.abort(); + + assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order"); + assert_true(request.signal.aborted, 'Signal aborted'); + assert_true(clonedRequest.signal.aborted, 'Signal aborted'); +}, "Clone aborts with original controller"); diff --git a/testing/web-platform/tests/fetch/api/abort/keepalive.html b/testing/web-platform/tests/fetch/api/abort/keepalive.html new file mode 100644 index 0000000000..db12df0d28 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/keepalive.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script> +// This controller must be on the window so it is visible to the iframe. +window.sharedController = new AbortController(); + +async function fetchJson(url) { + const response = await fetch(url); + assert_true(response.ok, 'response should be ok'); + return response.json(); +} + +promise_test(async () => { + const stateKey = token(); + const controller = new AbortController(); + await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}`, + { + signal: controller.signal, + keepalive: true + }); + const before = await fetchJson(`../resources/stash-take.py?key=${stateKey}`); + assert_equals(before, 'open', 'connection should be open'); + + controller.abort(); + + // Spin until the abort completes. + while (true) { + const after = await fetchJson(`../resources/stash-take.py?key=${stateKey}`); + if (after) { + // stateKey='open' was removed from the dictionary by the first fetch of + // stash-take.py, so we should only ever see the value 'closed' here. + assert_equals(after, 'closed', 'connection should have closed'); + break; + } + } +}, 'aborting a keepalive fetch should work'); + +promise_test(async t => { + const key = token(); + const iframeEl = document.querySelector('iframe'); + + // Tell the iframe to start the fetch, and wait until it says it has. + await new Promise(resolve => { + onmessage = t.step_func(event => { + assert_equals(event.data, 'started', 'event data should be "started"'); + resolve(); + }); + iframeEl.contentWindow.postMessage(key, '*'); + }); + + // Detach the context of the fetch. + iframeEl.remove(); + + sharedController.abort(); + + // The abort should not do anything. The connection should stay open. Wait 1 + // second to give time for the fetch to complete. + await new Promise(resolve => t.step_timeout(resolve, 1000)); + + const after = await fetchJson(`../resources/stash-take.py?key=${key}`); + assert_equals(after, 'on', 'fetch should have completed'); +}, 'aborting a detached keepalive fetch should not do anything'); +</script> + +<iframe srcdoc=" + <!DOCTYPE html> + <meta charset=utf-8> + <script> + onmessage = async event => { + const key = event.data; + await fetch( + `../resources/redirect.py?delay=500&location=` + + `../resources/stash-put.py%3fkey=${key}%26value=on`, + { + signal: parent.sharedController.signal, + keepalive: true + }); + parent.postMessage('started', '*'); + }; + </script> + "> +</iframe> diff --git a/testing/web-platform/tests/fetch/api/abort/request.any.js b/testing/web-platform/tests/fetch/api/abort/request.any.js new file mode 100644 index 0000000000..dcc7803abe --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/request.any.js @@ -0,0 +1,85 @@ +// META: timeout=long +// META: global=window,worker + +const BODY_FUNCTION_AND_DATA = { + arrayBuffer: null, + blob: null, + formData: new FormData(), + json: new Blob(["{}"]), + text: null, +}; + +for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) { + promise_test(async () => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request("../resources/data.json", { + method: "post", + signal, + body, + }); + + controller.abort(); + await request[bodyFunction](); + assert_true( + true, + `An aborted request should still be able to run ${bodyFunction}()` + ); + }, `Calling ${bodyFunction}() on an aborted request`); + + promise_test(async () => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request("../resources/data.json", { + method: "post", + signal, + body, + }); + + const p = request[bodyFunction](); + controller.abort(); + await p; + assert_true( + true, + `An aborted request should still be able to run ${bodyFunction}()` + ); + }, `Aborting a request after calling ${bodyFunction}()`); + + if (!body) { + promise_test(async () => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request("../resources/data.json", { + method: "post", + signal, + body, + }); + + // consuming happens synchronously, so don't wait + fetch(request).catch(() => {}); + + controller.abort(); + await request[bodyFunction](); + assert_true( + true, + `An aborted consumed request should still be able to run ${bodyFunction}() when empty` + ); + }, `Calling ${bodyFunction}() on an aborted consumed empty request`); + } + + promise_test(async t => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request("../resources/data.json", { + method: "post", + signal, + body: body || new Blob(["foo"]), + }); + + // consuming happens synchronously, so don't wait + fetch(request).catch(() => {}); + + controller.abort(); + await promise_rejects_js(t, TypeError, request[bodyFunction]()); + }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`); +} diff --git a/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html new file mode 100644 index 0000000000..ed9bc973e8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Aborting fetch when intercepted by a service worker</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../../../service-workers/service-worker/resources/test-helpers.sub.js"></script> +</head> +<body> +<script> + // Duplicating this resource to make service worker scoping simpler. + const SCOPE = '../resources/basic.html'; + const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text']; + + const error1 = new Error('error1'); + error1.name = 'error1'; + + async function setupRegistration(t, scope, service_worker) { + const reg = await navigator.serviceWorker.register(service_worker, { scope }); + await wait_for_state(t, reg.installing, 'activated'); + add_completion_callback(_ => reg.unregister()); + return reg; + } + + promise_test(async t => { + const suffix = "?q=aborted-not-intercepted"; + const scope = SCOPE + suffix; + await setupRegistration(t, scope, '../resources/sw-intercept.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + controller.abort(); + + const nextData = new Promise(resolve => { + w.navigator.serviceWorker.addEventListener('message', function once(event) { + // The message triggered by the iframe's document's fetch + // request cannot get dispatched by the time we add the event + // listener, so we have to guard against it. + if (!event.data.endsWith(suffix)) { + w.navigator.serviceWorker.removeEventListener('message', once); + resolve(event.data); + } + }) + }); + + const fetchPromise = w.fetch('data.json', { signal }); + + await promise_rejects_dom(t, "AbortError", w.DOMException, fetchPromise); + + await w.fetch('data.json?no-abort'); + + assert_true((await nextData).endsWith('?no-abort'), "Aborted request does not go through service worker"); + }, "Already aborted request does not land in service worker"); + + for (const bodyMethod of BODY_METHODS) { + promise_test(async t => { + const scope = SCOPE + "?q=aborted-" + bodyMethod + "-rejects"; + await setupRegistration(t, scope, '../resources/sw-intercept.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const log = []; + const response = await w.fetch('data.json', { signal }); + + controller.abort(); + + const bodyPromise = response[bodyMethod](); + + await Promise.all([ + bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)), + Promise.resolve().then(() => log.push('next-microtask')) + ]); + + await promise_rejects_dom(t, "AbortError", w.DOMException, bodyPromise); + + assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']); + }, `response.${bodyMethod}() rejects if already aborted`); + } + + promise_test(async t => { + const scope = SCOPE + "?q=aborted-stream-errors"; + await setupRegistration(t, scope, '../resources/sw-intercept.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const response = await w.fetch('data.json', { signal }); + const reader = response.body.getReader(); + + controller.abort(); + + await promise_rejects_dom(t, "AbortError", w.DOMException, reader.read()); + await promise_rejects_dom(t, "AbortError", w.DOMException, reader.closed); + }, "Stream errors once aborted."); + + promise_test(async t => { + const scope = SCOPE + "?q=aborted-with-abort-reason"; + await setupRegistration(t, scope, '../resources/sw-intercept.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const fetchPromise = w.fetch('data.json', { signal }); + + controller.abort(error1); + + await promise_rejects_exactly(t, error1, fetchPromise); + }, "fetch() rejects with abort reason"); + + + promise_test(async t => { + const scope = SCOPE + "?q=aborted-with-abort-reason-in-body"; + await setupRegistration(t, scope, '../resources/sw-intercept.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const fetchResponse = await w.fetch('data.json', { signal }); + const bodyPromise = fetchResponse.body.getReader().read(); + controller.abort(error1); + + await promise_rejects_exactly(t, error1, bodyPromise); + }, "fetch() response body has abort reason"); + + promise_test(async t => { + const scope = SCOPE + "?q=service-worker-observes-abort-reason"; + await setupRegistration(t, scope, '../resources/sw-intercept-abort.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const fetchPromise = w.fetch('data.json', { signal }); + + await new Promise(resolve => { + w.navigator.serviceWorker.addEventListener('message', t.step_func(event => { + assert_equals(event.data, "fetch event has arrived"); + resolve(); + }), {once: true}); + }); + + controller.abort(error1); + + await new Promise(resolve => { + w.navigator.serviceWorker.addEventListener('message', t.step_func(event => { + assert_equals(event.data.message, error1.message); + resolve(); + }), {once: true}); + }); + + await promise_rejects_exactly(t, error1, fetchPromise); + }, "Service Worker can observe the fetch abort and associated abort reason"); + + promise_test(async t => { + let incrementing_error = new Error('error1'); + incrementing_error.name = 'error1'; + + const scope = SCOPE + "?q=serialization-on-abort"; + await setupRegistration(t, scope, '../resources/sw-intercept-abort.js'); + const iframe = await with_iframe(scope); + add_completion_callback(_ => iframe.remove()); + const w = iframe.contentWindow; + + const controller = new w.AbortController(); + const signal = controller.signal; + + const fetchPromise = w.fetch('data.json', { signal }); + + await new Promise(resolve => { + w.navigator.serviceWorker.addEventListener('message', t.step_func(event => { + assert_equals(event.data, "fetch event has arrived"); + resolve(); + }), {once: true}); + }); + + controller.abort(incrementing_error); + + const original_error_name = incrementing_error.name; + + incrementing_error.name = 'error2'; + + await new Promise(resolve => { + w.navigator.serviceWorker.addEventListener('message', t.step_func(event => { + assert_equals(event.data.name, original_error_name); + resolve(); + }), {once: true}); + }); + + await promise_rejects_exactly(t, incrementing_error, fetchPromise); + }, "Abort reason serialization happens on abort"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/api/basic/accept-header.any.js b/testing/web-platform/tests/fetch/api/basic/accept-header.any.js new file mode 100644 index 0000000000..cd54cf2a03 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/accept-header.any.js @@ -0,0 +1,34 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'"); + }); +}, "Request through fetch should have 'accept' header with value '*/*'"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'"); + }); +}, "Request through fetch should have 'accept' header with value 'custom/*'"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_true(response.headers.has("x-request-accept-language")); + }); +}, "Request through fetch should have a 'accept-language' header"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) { + assert_equals(response.status, 200, "HTTP status is 200"); + assert_equals(response.type , "basic", "Response's type is basic"); + assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'"); + }); +}, "Request through fetch should have 'accept-language' header with value 'bzh'"); diff --git a/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html b/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html new file mode 100644 index 0000000000..afc2bbbafb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html @@ -0,0 +1,43 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Block mime type as script</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div></div> +<script> + var noop = function() {}; + + ["non-empty", "empty"].forEach(function(content) { + ["text/csv", + "audio/aiff", + "audio/midi", + "audio/whatever", + "video/avi", + "video/fli", + "video/whatever", + "image/jpeg", + "image/gif", + "image/whatever"].forEach(function(test_case) { + async_test(function(t) { + var script = document.createElement("script"); + script.onerror = t.step_func_done(noop); + script.onload = t.unreached_func("Unexpected load event"); + script.src = "../resources/script-with-header.py?content=" + content + + "&mime=" + test_case; + document.body.appendChild(script); + }, "Should fail loading " + content + " script with " + test_case + + " MIME type"); + }); + }); + + ["html", "plain"].forEach(function(test_case) { + async_test(function(t) { + var script = document.createElement("script"); + script.onerror = t.unreached_func("Unexpected error event"); + script.onload = t.step_func_done(noop); + script.src = "../resources/script-with-header.py?mime=text/" + test_case; + document.body.appendChild(script); + }, "Should load script with text/" + test_case + " MIME type"); + }); + +</script> diff --git a/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js b/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js new file mode 100644 index 0000000000..2f9fa81c02 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js @@ -0,0 +1,38 @@ +// META: title=Request ETag +// META: global=window,worker +// META: script=/common/utils.js + +promise_test(function() { + var cacheBuster = token(); // ensures first request is uncached + var url = "../resources/cache.py?v=" + cacheBuster; + var etag; + + // make the first request + return fetch(url).then(function(response) { + // ensure we're getting the regular, uncached response + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), null) + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a second request + return fetch(url); + }).then(function(response) { + // while the server responds with 304 if our browser sent the correct + // If-None-Match request header, at the JavaScript level this surfaces + // as 200 + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), "304") + + etag = response.headers.get("ETag") + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a third request, explicitly setting If-None-Match request header + var headers = { "If-None-Match": etag } + return fetch(url, { headers: headers }) + }).then(function(response) { + // 304 now surfaces thanks to the explicit If-None-Match request header + assert_equals(response.status, 304); + }); +}, "Testing conditional GET with ETags"); diff --git a/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js b/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js new file mode 100644 index 0000000000..f7114425f9 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js @@ -0,0 +1,24 @@ +// META: title=Fetch: network timeout after receiving the HTTP response headers +// META: global=window,worker +// META: timeout=long +// META: script=../resources/utils.js + +function checkReader(test, reader, promiseToTest) +{ + return reader.read().then((value) => { + validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk"); + return promise_rejects_js(test, TypeError, promiseToTest(reader)); + }); +} + +promise_test((test) => { + return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { + return checkReader(test, response.body.getReader(), reader => reader.read()); + }); +}, "Response reader read() promise should reject after a network error happening after resolving fetch promise"); + +promise_test((test) => { + return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { + return checkReader(test, response.body.getReader(), reader => reader.closed); + }); +}, "Response reader closed promise should reject after a network error happening after resolving fetch promise"); diff --git a/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js b/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js new file mode 100644 index 0000000000..bb70d87d25 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker + +[ + ["content-length", "0", "header-content-length"], + ["content-length", "0, 0", "header-content-length-twice"], + ["double-trouble", ", ", "headers-double-empty"], + ["foo-test", "1, 2, 3", "headers-basic"], + ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"], + ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"], +].forEach(testValues => { + promise_test(async t => { + const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis"); + assert_equals(response.headers.get(testValues[0]), testValues[1]); + }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]); +}); diff --git a/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js b/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js new file mode 100644 index 0000000000..741d83bf7a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js @@ -0,0 +1,5 @@ +// META: global=window,worker + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x"))); +}, "Ensure fetch() rejects null bytes in headers"); diff --git a/testing/web-platform/tests/fetch/api/basic/historical.any.js b/testing/web-platform/tests/fetch/api/basic/historical.any.js new file mode 100644 index 0000000000..c808126216 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/historical.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +test(() => { + assert_false("getAll" in new Headers()); + assert_false("getAll" in Headers.prototype); +}, "Headers object no longer has a getAll() method"); + +test(() => { + assert_false("type" in new Request("about:blank")); + assert_false("type" in Request.prototype); +}, "'type' getter should not exist on Request objects"); + +// See https://github.com/whatwg/fetch/pull/979 for the removal +test(() => { + assert_false("trailer" in new Response()); + assert_false("trailer" in Response.prototype); +}, "Response object no longer has a trailer getter"); diff --git a/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js b/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js new file mode 100644 index 0000000000..1fd312a3e9 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js @@ -0,0 +1,14 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +promise_test(async (test) => { + const resp = await fetch( + "/fetch/connection-pool/resources/network-partition-key.py?" + + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}` + + `&dispatch=check_partition&addcounter=true`); + assert_equals(resp.status, 425); + const text = await resp.text(); + assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); +}, "Fetch on 425 response should not be retried for non TLS early data."); diff --git a/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js b/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js new file mode 100644 index 0000000000..e3cfd1b2f6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js @@ -0,0 +1,87 @@ +// META: global=window,dedicatedworker,sharedworker +// META: script=../resources/utils.js + +function integrity(desc, url, integrity, initRequestMode, shouldPass) { + var fetchRequestInit = {'integrity': integrity} + if (!!initRequestMode && initRequestMode !== "") { + fetchRequestInit.mode = initRequestMode; + } + + if (shouldPass) { + promise_test(function(test) { + return fetch(url, fetchRequestInit).then(function(resp) { + if (initRequestMode !== "no-cors") { + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.status, 0, "Opaque response's status is 0"); + assert_equals(resp.type, "opaque"); + } + }); + }, desc); + } else { + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit)); + }, desc); + } +} + +const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk="; +const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL"; +const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; +const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; +const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I="; +const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg=="; + +const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +const url = path; +const corsUrl = + `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`; +const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}` + +integrity("Empty string integrity", url, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-512 integrity with missing padding", url, topSha512wrongpadding, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("SHA-512 integrity base64url encoded", url, topSha512base64url, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("SHA-512 integrity base64url encoded with missing padding", url, + topSha512base64url_nopadding, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Invalid integrity", url, invalidSha256, + /* initRequestMode */ undefined, /* shouldPass */ false); +integrity("Multiple integrities: valid stronger than invalid", url, + invalidSha256 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: invalid stronger than valid", + url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("Multiple integrities: invalid as strong as valid", url, + invalidSha512 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are valid", url, + topSha384 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are invalid", url, + invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("CORS SHA-512 integrity", corsUrl, topSha512, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("CORS invalid integrity", corsUrl, invalidSha512, + /* initRequestMode */ undefined, /* shouldPass */ false); + +integrity("Empty string integrity for opaque response", corsUrl2, "", + /* initRequestMode */ "no-cors", /* shouldPass */ true); +integrity("SHA-* integrity for opaque response", corsUrl2, topSha512, + /* initRequestMode */ "no-cors", /* shouldPass */ false); + +done(); diff --git a/testing/web-platform/tests/fetch/api/basic/keepalive.any.js b/testing/web-platform/tests/fetch/api/basic/keepalive.any.js new file mode 100644 index 0000000000..d6ec1f6792 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/keepalive.any.js @@ -0,0 +1,42 @@ +// META: global=window +// META: timeout=long +// META: title=Fetch API: keepalive handling +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +} = get_host_info(); + +/** + * In a different-site iframe, test to fetch a keepalive URL on the specified + * document event. + */ +function keepaliveSimpleRequestTest(method) { + for (const evt of ['load', 'pagehide', 'unload']) { + const desc = + `[keepalive] simple ${method} request on '${evt}' [no payload]`; + promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt}); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + if (evt != 'load') { + iframe.remove(); + } + assert_equals(await getTokenFromMessage(), token1); + + assertStashedTokenAsync(desc, token1); + }, `${desc}; setting up`); + } +} + +for (const method of ['GET', 'POST']) { + keepaliveSimpleRequestTest(method); +} diff --git a/testing/web-platform/tests/fetch/api/basic/mediasource.window.js b/testing/web-platform/tests/fetch/api/basic/mediasource.window.js new file mode 100644 index 0000000000..1f89595393 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/mediasource.window.js @@ -0,0 +1,5 @@ +promise_test(t => { + const mediaSource = new MediaSource(), + mediaSourceURL = URL.createObjectURL(mediaSource); + return promise_rejects_js(t, TypeError, fetch(mediaSourceURL)); +}, "Cannot fetch blob: URL from a MediaSource"); diff --git a/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js b/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js new file mode 100644 index 0000000000..a4abcac55f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js + +function fetchNoCors(url, isOpaqueFiltered) { + var urlQuery = "?pipe=header(x-is-filtered,value)" + promise_test(function(test) { + if (isOpaqueFiltered) + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 0, "Opaque filter: status is 0"); + assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); + assert_equals(resp.url, "", "Opaque filter: url is \"\""); + assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); + assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered"); + }); + else + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered"); + }); + }, "Fetch "+ url + " with no-cors mode"); +} + +fetchNoCors(RESOURCES_DIR + "top.txt", false); +fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false); +fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true); +fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true); + +done(); + diff --git a/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js b/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js new file mode 100644 index 0000000000..1457702f1b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js @@ -0,0 +1,28 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function fetchSameOrigin(url, shouldPass) { + promise_test(function(test) { + if (shouldPass) + return fetch(url , {"mode": "same-origin"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + }); + else + return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"})); + }, "Fetch "+ url + " with same-origin mode"); +} + +var host_info = get_host_info(); + +fetchSameOrigin(RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); diff --git a/testing/web-platform/tests/fetch/api/basic/referrer.any.js b/testing/web-platform/tests/fetch/api/basic/referrer.any.js new file mode 100644 index 0000000000..85745e692a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/referrer.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function runTest(url, init, expectedReferrer, title) { + promise_test(function(test) { + url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors"; + + return fetch(url , init).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct"); + }); + }, title); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py"; +var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py"; +var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ; +var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL"); +runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL"); +runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection"); +runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection"); + + +var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@"); +runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped"); +var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier"; +runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped"); diff --git a/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js b/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js new file mode 100644 index 0000000000..d7560f03a2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js @@ -0,0 +1,82 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function requestValidOverrideHeaders(desc, validHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": validHeaders} + var urlParameters = "?headers=" + Object.keys(validHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in validHeaders) + assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods"); + }); + }, desc); +} + +requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"}); +requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""}); + +requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""}); +requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""}); +requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"}); +requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"}); +requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"}); +requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"}); +requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"}); +requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"}); +requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"}); +requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"}); +requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"}); +requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"}); +requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"}); +requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"}); +requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"}); +requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"}); +requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"}); +requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"}); +requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"}); +requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"}); +requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"}); +requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"}); + +let forbiddenMethods = [ + "TRACE", + "TRACK", + "CONNECT", + "trace", + "track", + "connect", + "trace,", + "GET,track ", + " connect", +]; + +let overrideHeaders = [ + "x-http-method-override", + "x-http-method", + "x-method-override", + "X-HTTP-METHOD-OVERRIDE", + "X-HTTP-METHOD", + "X-METHOD-OVERRIDE", +]; + +for (forbiddenMethod of forbiddenMethods) { + for (overrideHeader of overrideHeaders) { + requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod}); + } +} + +let permittedValues = [ + "GETTRACE", + "GET", + "\",TRACE\",", +]; + +for (permittedValue of permittedValues) { + for (overrideHeader of overrideHeaders) { + requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue}); + } +} diff --git a/testing/web-platform/tests/fetch/api/basic/request-head.any.js b/testing/web-platform/tests/fetch/api/basic/request-head.any.js new file mode 100644 index 0000000000..e0b6afa079 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-head.any.js @@ -0,0 +1,6 @@ +// META: global=window,worker + +promise_test(function(test) { + var requestInit = {"method": "HEAD", "body": "test"}; + return promise_rejects_js(test, TypeError, fetch(".", requestInit)); +}, "Fetch with HEAD with body"); diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js new file mode 100644 index 0000000000..4c10e717f8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js @@ -0,0 +1,13 @@ +// META: global=window,worker + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-is-A-test: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-is-A-test first)") + +promise_test(() => { + return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => { + assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/) + }) +}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)") diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js new file mode 100644 index 0000000000..4a9a801138 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker + +// This tests characters that are not +// https://infra.spec.whatwg.org/#ascii-code-point +// but are still +// https://infra.spec.whatwg.org/#byte-value +// in request header values. +// Such request header values are valid and thus sent to servers. +// Characters outside the #byte-value range are tested e.g. in +// fetch/api/headers/headers-errors.html. + +promise_test(() => { + return fetch( + "../resources/inspect-headers.py?headers=accept|x-test", + {headers: { + "Accept": "before-æøå-after", + "X-Test": "before-ß-after" + }}) + .then(res => { + assert_equals( + res.headers.get("x-request-accept"), + "before-æøå-after", + "Accept Header"); + assert_equals( + res.headers.get("x-request-x-test"), + "before-ß-after", + "X-Test Header"); + }); +}, "Non-ascii bytes in request headers"); diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js new file mode 100644 index 0000000000..ac54256e4c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js @@ -0,0 +1,82 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkContentType(contentType, body) +{ + if (self.FormData && body instanceof self.FormData) { + assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType); + return; + } + + var expectedContentType = "text/plain;charset=UTF-8"; + if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer) + expectedContentType = null; + else if (body instanceof Blob) + expectedContentType = body.type ? body.type : null; + else if (body instanceof URLSearchParams) + expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8"; + + assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType); +} + +function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) { + var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type"; + var requestInit = {"method": method} + promise_test(function(test){ + if (typeof body === "function") + body = body(); + if (body) + requestInit["body"] = body; + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent"); + assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset"); + assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin); + if (expectedContentLength !== undefined) + assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength); + checkContentType(resp.headers.get("x-request-content-type"), body); + }); + }, desc); +} + +var url = RESOURCES_DIR + "inspect-headers.py" + +requestHeaders("Fetch with GET", url, "GET", null, null, null); +requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null); +requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin); +requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10"); +requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4"); +requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4"); +requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4"); +requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8"); +requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4"); +requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4"); +requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null); +requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14"); + +function requestOriginHeader(method, mode, needsOrigin) { + promise_test(function(test){ + return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + if(needsOrigin) + assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin); + else + assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header") + }); + }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header"); +} + +requestOriginHeader("GET", "cors", false); +requestOriginHeader("POST", "same-origin", true); +requestOriginHeader("POST", "no-cors", true); +requestOriginHeader("PUT", "same-origin", true); +requestOriginHeader("TacO", "same-origin", true); +requestOriginHeader("TacO", "cors", true); diff --git a/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js b/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js new file mode 100644 index 0000000000..9662a91c17 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js @@ -0,0 +1,18 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +requestForbiddenHeaders( + 'Access-Control-Request-Private-Network is a forbidden request header', + {'Access-Control-Request-Private-Network': ''}); + +var invalidRequestHeaders = [ + ["Access-Control-Request-Private-Network", "KO"], +]; + +invalidRequestHeaders.forEach(function(header) { + test(function() { + var request = new Request(""); + request.headers.set(header[0], header[1]); + assert_equals(request.headers.get(header[0]), null); + }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\""); +}); diff --git a/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html b/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html new file mode 100644 index 0000000000..bdea1e1853 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + let finalURL = "/fetch/api/basic/request-referrer.any.worker.js"; + let url = "/fetch/api/resources/redirect.py?location=" + + encodeURIComponent(finalURL); + fetch_tests_from_worker(new Worker(url)); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js b/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js new file mode 100644 index 0000000000..0c3357642d --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function testReferrer(referrer, expected, desc) { + promise_test(function(test) { + var url = RESOURCES_DIR + "inspect-headers.py?headers=referer" + var req = new Request(url, { referrer: referrer }); + return fetch(req).then(function(resp) { + var actual = resp.headers.get("x-request-referer"); + if (expected) { + assert_equals(actual, expected, "request's referer should be: " + expected); + return; + } + if (actual) { + assert_equals(actual, "", "request's referer should be empty"); + } + }); + }, desc); +} + +testReferrer("about:client", self.location.href, 'about:client referrer'); + +var fooURL = new URL("./foo", self.location).href; +testReferrer(fooURL, fooURL, 'url referrer'); diff --git a/testing/web-platform/tests/fetch/api/basic/request-upload.any.js b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js new file mode 100644 index 0000000000..9168aa1154 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js @@ -0,0 +1,135 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +function testUpload(desc, url, method, createBody, expectedBody) { + const requestInit = {method}; + promise_test(function(test){ + const body = createBody(); + if (body) { + requestInit["body"] = body; + requestInit.duplex = "half"; + } + return fetch(url, requestInit).then(function(resp) { + return resp.text().then((text)=> { + assert_equals(text, expectedBody); + }); + }); + }, desc); +} + +function testUploadFailure(desc, url, method, createBody) { + const requestInit = {method}; + promise_test(t => { + const body = createBody(); + if (body) { + requestInit["body"] = body; + } + return promise_rejects_js(t, TypeError, fetch(url, requestInit)); + }, desc); +} + +const url = RESOURCES_DIR + "echo-content.py" + +testUpload("Fetch with PUT with body", url, + "PUT", + () => "Request's body", + "Request's body"); +testUpload("Fetch with POST with text body", url, + "POST", + () => "Request's body", + "Request's body"); +testUpload("Fetch with POST with URLSearchParams body", url, + "POST", + () => new URLSearchParams("name=value"), + "name=value"); +testUpload("Fetch with POST with Blob body", url, + "POST", + () => new Blob(["Test"]), + "Test"); +testUpload("Fetch with POST with ArrayBuffer body", url, + "POST", + () => new ArrayBuffer(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Uint8Array body", url, + "POST", + () => new Uint8Array(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Int8Array body", url, + "POST", + () => new Int8Array(4), + "\0\0\0\0"); +testUpload("Fetch with POST with Float32Array body", url, + "POST", + () => new Float32Array(1), + "\0\0\0\0"); +testUpload("Fetch with POST with Float64Array body", url, + "POST", + () => new Float64Array(1), + "\0\0\0\0\0\0\0\0"); +testUpload("Fetch with POST with DataView body", url, + "POST", + () => new DataView(new ArrayBuffer(8), 0, 4), + "\0\0\0\0"); +testUpload("Fetch with POST with Blob body with mime type", url, + "POST", + () => new Blob(["Test"], { type: "text/maybe" }), + "Test"); + +testUploadFailure("Fetch with POST with ReadableStream containing String", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue("Test"); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing null", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(null); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing number", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(99); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(new ArrayBuffer()); + controller.close(); + }}) + }); +testUploadFailure("Fetch with POST with ReadableStream containing Blob", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.enqueue(new Blob()); + controller.close(); + }}) + }); + +promise_test(async (test) => { + const resp = await fetch( + "/fetch/connection-pool/resources/network-partition-key.py?" + + `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}` + + `&dispatch=check_partition&addcounter=true`, + {method: "POST", body: "foobar"}); + assert_equals(resp.status, 421); + const text = await resp.text(); + assert_equals(text, "ok. Request was sent 2 times. 2 connections were created."); +}, "Fetch with POST with text body on 421 response should be retried once on new connection."); + +promise_test(async (test) => { + const body = new ReadableStream({start: c => c.close()}); + await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body})); +}, "Streaming upload shouldn't work on Http/1.1."); diff --git a/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js b/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js new file mode 100644 index 0000000000..eedc2bf6a7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js @@ -0,0 +1,186 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const duplex = "half"; + +async function assertUpload(url, method, createBody, expectedBody) { + const requestInit = {method}; + const body = createBody(); + if (body) { + requestInit["body"] = body; + requestInit.duplex = "half"; + } + const resp = await fetch(url, requestInit); + const text = await resp.text(); + assert_equals(text, expectedBody); +} + +function testUpload(desc, url, method, createBody, expectedBody) { + promise_test(async () => { + await assertUpload(url, method, createBody, expectedBody); + }, desc); +} + +function createStream(chunks) { + return new ReadableStream({ + start: (controller) => { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + controller.close(); + } + }); +} + +const url = RESOURCES_DIR + "echo-content.h2.py" + +testUpload("Fetch with POST with empty ReadableStream", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.close(); + }}) + }, + ""); + +testUpload("Fetch with POST with ReadableStream", url, + "POST", + () => { + return new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}) + }, + "Test"); + +promise_test(async (test) => { + const body = new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}); + const resp = await fetch( + "/fetch/connection-pool/resources/network-partition-key.py?" + + `status=421&uuid=${token()}&partition_id=${self.origin}` + + `&dispatch=check_partition&addcounter=true`, + {method: "POST", body: body, duplex}); + assert_equals(resp.status, 421); + const text = await resp.text(); + assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); +}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry."); + +promise_test(async (test) => { + const request = new Request('', { + body: new ReadableStream(), + method: 'POST', + duplex, + }); + + assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); + + const response = await fetch('data:a/a;charset=utf-8,test', { + method: 'POST', + body: new ReadableStream(), + duplex, + }); + + assert_equals(await response.text(), 'test', `Response has correct body`); +}, "Feature detect for POST with ReadableStream"); + +promise_test(async (test) => { + const request = new Request('data:a/a;charset=utf-8,test', { + body: new ReadableStream(), + method: 'POST', + duplex, + }); + + assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); + const response = await fetch(request); + assert_equals(await response.text(), 'test', `Response has correct body`); +}, "Feature detect for POST with ReadableStream, using request object"); + +test(() => { + let duplexAccessed = false; + + const request = new Request("", { + body: new ReadableStream(), + method: "POST", + get duplex() { + duplexAccessed = true; + return "half"; + }, + }); + + assert_equals( + request.headers.get("Content-Type"), + null, + `Request should not have a content-type set` + ); + assert_true(duplexAccessed, `duplex dictionary property should be accessed`); +}, "Synchronous feature detect"); + +// The asserts the synchronousFeatureDetect isn't broken by a partial implementation. +// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request, +// but it failed when passed to fetch(). +// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts. +promise_test(async () => { + let duplexAccessed = false; + + const request = new Request("", { + body: new ReadableStream(), + method: "POST", + get duplex() { + duplexAccessed = true; + return "half"; + }, + }); + + const supported = + request.headers.get("Content-Type") === null && duplexAccessed; + + // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here) + if (!supported) return false; + + await assertUpload( + url, + "POST", + () => + new ReadableStream({ + start: (controller) => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }, + }), + "Test" + ); +}, "Synchronous feature detect fails if feature unsupported"); + +promise_test(async (t) => { + const body = createStream(["hello"]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing a String"); + +promise_test(async (t) => { + const body = createStream([null]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing null"); + +promise_test(async (t) => { + const body = createStream([33]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing a number"); + +promise_test(async (t) => { + const url = "/fetch/api/resources/authentication.py?realm=test"; + const body = createStream([]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload should fail on a 401 response"); + diff --git a/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js b/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js new file mode 100644 index 0000000000..bb05892657 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js @@ -0,0 +1,38 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +const nullBodyStatus = [204, 205, 304]; +const methods = ["GET", "POST", "OPTIONS"]; + +for (const status of nullBodyStatus) { + for (const method of methods) { + promise_test( + async () => { + const url = + `${RESOURCES_DIR}status.py?code=${status}&content=hello-world`; + const resp = await fetch(url, { method }); + assert_equals(resp.status, status); + assert_equals(resp.body, null, "the body should be null"); + const text = await resp.text(); + assert_equals(text, "", "null bodies result in empty text"); + }, + `Response.body is null for responses with status=${status} (method=${method})`, + ); + } +} + +promise_test(async () => { + const url = `${RESOURCES_DIR}status.py?code=200&content=hello-world`; + const resp = await fetch(url, { method: "HEAD" }); + assert_equals(resp.status, 200); + assert_equals(resp.body, null, "the body should be null"); + const text = await resp.text(); + assert_equals(text, "", "null bodies result in empty text"); +}, `Response.body is null for responses with method=HEAD`); + +promise_test(async (t) => { + const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE"; + const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`; + const promise = fetch(url, { method: "GET", integrity }); + promise_rejects_js(t, TypeError, promise); +}, "Null body status with subresource integrity should abort"); diff --git a/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js b/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js new file mode 100644 index 0000000000..0d123c4294 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js @@ -0,0 +1,16 @@ +function checkResponseURL(fetchedURL, expectedURL) +{ + promise_test(function() { + return fetch(fetchedURL).then(function(response) { + assert_equals(response.url, expectedURL); + }); + }, "Testing response url getter with " +fetchedURL); +} + +var baseURL = "http://{{host}}:{{ports[http][0]}}"; +checkResponseURL(baseURL + "/ada", baseURL + "/ada"); +checkResponseURL(baseURL + "/#", baseURL + "/"); +checkResponseURL(baseURL + "/#ada", baseURL + "/"); +checkResponseURL(baseURL + "#ada", baseURL + "/"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js new file mode 100644 index 0000000000..9ef44183c1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js @@ -0,0 +1,26 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkNetworkError(url, method) { + method = method || "GET"; + const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO" + promise_test(function(test) { + var promise = fetch(url, { method: method }); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +checkNetworkError("about:blank", "GET"); +checkNetworkError("about:blank", "PUT"); +checkNetworkError("about:blank", "POST"); +checkNetworkError("about:invalid.com"); +checkNetworkError("about:config"); +checkNetworkError("about:unicorn"); + +promise_test(function(test) { + var promise = fetch("about:blank", { + "method": "GET", + "Range": "bytes=1-10" + }); + return promise_rejects_js(test, TypeError, promise); +}, "Fetching about:blank with range header does not affect behavior"); diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js new file mode 100644 index 0000000000..8afdc033c9 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js @@ -0,0 +1,125 @@ +// META: script=../resources/utils.js + +function checkFetchResponse(url, data, mime, size, desc) { + promise_test(function(test) { + size = size.toString(); + return fetch(url).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); + assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length")); + return resp.text(); + }).then(function(bodyAsText) { + assert_equals(bodyAsText, data, "Response's body is " + data); + }); + }, desc); +} + +var blob = new Blob(["Blob's data"], { "type" : "text/plain" }); +checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size, + "Fetching [GET] URL.createObjectURL(blob) is OK"); + +function checkKoUrl(url, method, desc) { + promise_test(function(test) { + var promise = fetch(url, {"method": method}); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" }); +checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET", + "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO"); + +var invalidRequestMethods = [ + "POST", + "OPTIONS", + "HEAD", + "PUT", + "DELETE", + "INVALID", +]; +invalidRequestMethods.forEach(function(method) { + checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO"); +}); + +checkKoUrl("blob:not-backed-by-a-blob/", "GET", + "Fetching [GET] blob:not-backed-by-a-blob/ is KO"); + +let empty_blob = new Blob([]); +checkFetchResponse(URL.createObjectURL(empty_blob), "", "", 0, + "Fetching URL.createObjectURL(empty_blob) is OK"); + +let empty_type_blob = new Blob([], {type: ""}); +checkFetchResponse(URL.createObjectURL(empty_type_blob), "", "", 0, + "Fetching URL.createObjectURL(empty_type_blob) is OK"); + +let empty_data_blob = new Blob([], {type: "text/plain"}); +checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0, + "Fetching URL.createObjectURL(empty_data_blob) is OK"); + +let invalid_type_blob = new Blob([], {type: "invalid"}); +checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0, + "Fetching URL.createObjectURL(invalid_type_blob) is OK"); + +promise_test(function(test) { + return fetch("/images/blue.png").then(function(resp) { + return resp.arrayBuffer(); + }).then(function(image_buffer) { + let blob = new Blob([image_buffer]); + return fetch(URL.createObjectURL(blob)).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), "", "Content-Type is " + resp.headers.get("Content-Type")); + }) + }); +}, "Blob content is not sniffed for a content type [image/png]"); + +let simple_xml_string = '<?xml version="1.0" encoding="UTF-8"?><x></x>'; +let xml_blob_no_type = new Blob([simple_xml_string]); +checkFetchResponse(URL.createObjectURL(xml_blob_no_type), simple_xml_string, "", 45, + "Blob content is not sniffed for a content type [text/xml]"); + +let simple_text_string = 'Hello, World!'; +promise_test(function(test) { + let blob = new Blob([simple_text_string], {"type": "text/plain"}); + let slice = blob.slice(7, simple_text_string.length, "\0"); + return fetch(URL.createObjectURL(slice)).then(function (resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), ""); + assert_equals(resp.headers.get("Content-Length"), "6"); + return resp.text(); + }).then(function(bodyAsText) { + assert_equals(bodyAsText, "World!"); + }); +}, "Set content type to the empty string for slice with invalid content type"); + +promise_test(function(test) { + let blob = new Blob([simple_text_string], {"type": "text/plain"}); + let slice = blob.slice(7, simple_text_string.length, "\0"); + return fetch(URL.createObjectURL(slice)).then(function (resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), ""); + assert_equals(resp.headers.get("Content-Length"), "6"); + return resp.text(); + }).then(function(bodyAsText) { + assert_equals(bodyAsText, "World!"); + }); +}, "Set content type to the empty string for slice with no content type "); + +promise_test(function(test) { + let blob = new Blob([simple_xml_string]); + let slice = blob.slice(0, 38); + return fetch(URL.createObjectURL(slice)).then(function (resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), ""); + assert_equals(resp.headers.get("Content-Length"), "38"); + return resp.text(); + }).then(function(bodyAsText) { + assert_equals(bodyAsText, '<?xml version="1.0" encoding="UTF-8"?>'); + }); +}, "Blob.slice should not sniff the content for a content type"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js new file mode 100644 index 0000000000..55df43bd50 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js @@ -0,0 +1,43 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkFetchResponse(url, data, mime, fetchMode, method) { + var cut = (url.length >= 40) ? "[...]" : ""; + var desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK"; + var init = {"method": method || "GET"}; + if (fetchMode) { + init.mode = fetchMode; + desc += " (" + fetchMode + ")"; + } + promise_test(function(test) { + return fetch(url, init).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.statusText, "OK", "HTTP statusText is OK"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(body) { + assert_equals(body, data, "Response's body is correct"); + }); + }, desc); +} + +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors"); +checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain"); +checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5", + "response's body", + "image/png"); +checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST"); +checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD"); + +function checkKoUrl(url, method, desc) { + var cut = (url.length >= 40) ? "[...]" : ""; + desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO" + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, {"method": method})); + }, desc); +} + +checkKoUrl("data:notAdataUrl.com", "GET"); diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js new file mode 100644 index 0000000000..550f69c41b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js @@ -0,0 +1,31 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkKoUrl(url, desc) { + if (!desc) + desc = "Fetching " + url.substring(0, 45) + " is KO" + promise_test(function(test) { + var promise = fetch(url); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/"; +checkKoUrl("aaa" + urlWithoutScheme); +checkKoUrl("cap" + urlWithoutScheme); +checkKoUrl("cid" + urlWithoutScheme); +checkKoUrl("dav" + urlWithoutScheme); +checkKoUrl("dict" + urlWithoutScheme); +checkKoUrl("dns" + urlWithoutScheme); +checkKoUrl("geo" + urlWithoutScheme); +checkKoUrl("im" + urlWithoutScheme); +checkKoUrl("imap" + urlWithoutScheme); +checkKoUrl("ipp" + urlWithoutScheme); +checkKoUrl("ldap" + urlWithoutScheme); +checkKoUrl("mailto" + urlWithoutScheme); +checkKoUrl("nfs" + urlWithoutScheme); +checkKoUrl("pop" + urlWithoutScheme); +checkKoUrl("rtsp" + urlWithoutScheme); +checkKoUrl("snmp" + urlWithoutScheme); + +done(); diff --git a/testing/web-platform/tests/fetch/api/basic/status.h2.any.js b/testing/web-platform/tests/fetch/api/basic/status.h2.any.js new file mode 100644 index 0000000000..99fec88f50 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/status.h2.any.js @@ -0,0 +1,17 @@ +// See also /xhr/status.h2.window.js + +[ + 200, + 210, + 400, + 404, + 410, + 500, + 502 +].forEach(status => { + promise_test(async t => { + const response = await fetch("/xhr/resources/status.py?code=" + status); + assert_equals(response.status, status, "status should be " + status); + assert_equals(response.statusText, "", "statusText should be the empty string"); + }, "statusText over H2 for status " + status + " should be the empty string"); +}); diff --git a/testing/web-platform/tests/fetch/api/basic/stream-response.any.js b/testing/web-platform/tests/fetch/api/basic/stream-response.any.js new file mode 100644 index 0000000000..d964dda717 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/stream-response.any.js @@ -0,0 +1,40 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function streamBody(reader, test, count = 0) { + return reader.read().then(function(data) { + if (!data.done && count < 2) { + count += 1; + return streamBody(reader, test, count); + } else { + test.step(function() { + assert_true(count >= 2, "Retrieve body progressively"); + }); + } + }); +} + +//simulate streaming: +//count is large enough to let the UA deliver the body before it is completely retrieved +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) { + if (resp.body) + return streamBody(resp.body.getReader(), test); + else + test.step(function() { + assert_unreached( "Body does not exist in response"); + }); + }); +}, "Stream response's body when content-type is present"); + +// This test makes sure that the response body is not buffered if no content type is provided. +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=300&count=10¬ype=true").then(function(resp) { + if (resp.body) + return streamBody(resp.body.getReader(), test); + else + test.step(function() { + assert_unreached( "Body does not exist in response"); + }); + }); +}, "Stream response's body when content-type is not present"); diff --git a/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js b/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js new file mode 100644 index 0000000000..382efc1a8b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js @@ -0,0 +1,54 @@ +// META: global=window,worker + +// These tests verify that stream creation is not affected by changes to +// Object.prototype. + +const creationCases = { + fetch: async () => fetch(location.href), + request: () => new Request(location.href, {method: 'POST', body: 'hi'}), + response: () => new Response('bye'), + consumeEmptyResponse: () => new Response().text(), + consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(), + consumeEmptyRequest: () => new Request(location.href).text(), + consumeNonEmptyRequest: () => new Request(location.href, + {method: 'POST', body: 'yes'}).arrayBuffer(), +}; + +for (const creationCase of Object.keys(creationCases)) { + for (const accessorName of ['start', 'type', 'size', 'highWaterMark']) { + promise_test(async t => { + Object.defineProperty(Object.prototype, accessorName, { + get() { throw Error(`Object.prototype.${accessorName} was accessed`); }, + configurable: true + }); + t.add_cleanup(() => { + delete Object.prototype[accessorName]; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `throwing Object.prototype.${accessorName} accessor should not affect ` + + `stream creation by '${creationCase}'`); + + promise_test(async t => { + // -1 is a convenient value which is invalid, and should cause the + // constructor to throw, for all four fields. + Object.prototype[accessorName] = -1; + t.add_cleanup(() => { + delete Object.prototype[accessorName]; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `Object.prototype.${accessorName} accessor returning invalid value ` + + `should not affect stream creation by '${creationCase}'`); + } + + promise_test(async t => { + Object.prototype.start = controller => controller.error(new Error('start')); + t.add_cleanup(() => { + delete Object.prototype.start; + return Promise.resolve(); + }); + await creationCases[creationCase](); + }, `Object.prototype.start function which errors the stream should not ` + + `affect stream creation by '${creationCase}'`); +} diff --git a/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js b/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js new file mode 100644 index 0000000000..05c8c88825 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js @@ -0,0 +1,74 @@ +// META: title=Fetch: Request and Response text() should decode as UTF-8 +// META: global=window,worker +// META: script=../resources/utils.js + +function testTextDecoding(body, expectedText, urlParameter, title) +{ + var arrayBuffer = stringToArray(body); + + promise_test(function(test) { + var request = new Request("", {method: "POST", body: arrayBuffer}); + return request.text().then(function(value) { + assert_equals(value, expectedText, "Request.text() should decode data as UTF-8"); + }); + }, title + " with Request.text()"); + + promise_test(function(test) { + var response = new Response(arrayBuffer); + return response.text().then(function(value) { + assert_equals(value, expectedText, "Response.text() should decode data as UTF-8"); + }); + }, title + " with Response.text()"); + + promise_test(function(test) { + return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) { + return response.text().then(function(value) { + assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); + }); + }); + }, title + " with fetched data (UTF-8 charset)"); + + promise_test(function(test) { + return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) { + return response.text().then(function(value) { + assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); + }); + }); + }, title + " with fetched data (UTF-16 charset)"); + + promise_test(function(test) { + return new Response(body).arrayBuffer().then(function(buffer) { + assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8"); + }); + }, title + " (Response object)"); + + promise_test(function(test) { + return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) { + assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8"); + }); + }, title + " (Request object)"); + +} + +var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; +var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; +var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; +var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; +var utf8Decoded = "三村かな子"; +testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM"); +testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM"); + +var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50"; +var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50"; +var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P"; +testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8"); + +var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b"; +var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b"; +var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P["; +testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8"); + +var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00"; +var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00"; +var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000"; +testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8"); diff --git a/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html b/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html new file mode 100644 index 0000000000..fa47b29473 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html @@ -0,0 +1,33 @@ +<!-- Based on /html/infrastructure/urls/resolving-urls/query-encoding/location.sub.html --> +<!doctype html> +<meta charset={{GET[encoding]}}> <!-- ends up as <meta charset> by default which is windows-1252 --> +<meta name=variant content="?encoding=windows-1252"> +<meta name=variant content="?encoding=x-cp1251"> +<meta name=variant content="?encoding=utf8"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +function expected(encoding) { + return { + "UTF-8": "%C3%BF", + "windows-1251": "%26%23255%3B", + "windows-1252": "%FF" + }[encoding]; +} + +test(() => { + const request = new Request("?\u00FF"); + assert_equals(request.url.split("?")[1], expected("UTF-8")); +}, "Request uses the UTF-8 URL parser"); + +test(() => { + const request = new Request("about:blank", { referrer: "?\u00FF" }); + assert_equals(request.referrer.split("?")[1], expected("UTF-8")); +}, "Request's referrer uses the UTF-8 URL parser"); + +test(() => { + const response = Response.redirect("?\u00FF"); + assert_equals(response.headers.get("Location").split("?")[1], expected("UTF-8")); +}, "Response.redirect() uses the UTF-8 URL parser"); +</script> diff --git a/testing/web-platform/tests/fetch/api/body/cloned-any.js b/testing/web-platform/tests/fetch/api/body/cloned-any.js new file mode 100644 index 0000000000..2bca96c704 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/body/cloned-any.js @@ -0,0 +1,50 @@ +// Changing the body after it have been passed to Response/Request +// should not change the outcome of the consumed body + +const url = 'http://a'; +const method = 'post'; + +promise_test(async t => { + const body = new FormData(); + body.set('a', '1'); + const res = new Response(body); + const req = new Request(url, { method, body }); + body.set('a', '2'); + assert_true((await res.formData()).get('a') === '1'); + assert_true((await req.formData()).get('a') === '1'); +}, 'FormData is cloned'); + +promise_test(async t => { + const body = new URLSearchParams({a: '1'}); + const res = new Response(body); + const req = new Request(url, { method, body }); + body.set('a', '2'); + assert_true((await res.formData()).get('a') === '1'); + assert_true((await req.formData()).get('a') === '1'); +}, 'URLSearchParams is cloned'); + +promise_test(async t => { + const body = new Uint8Array([97]); // a + const res = new Response(body); + const req = new Request(url, { method, body }); + body[0] = 98; // b + assert_true(await res.text() === 'a'); + assert_true(await req.text() === 'a'); +}, 'TypedArray is cloned'); + +promise_test(async t => { + const body = new Uint8Array([97]); // a + const res = new Response(body.buffer); + const req = new Request(url, { method, body: body.buffer }); + body[0] = 98; // b + assert_true(await res.text() === 'a'); + assert_true(await req.text() === 'a'); +}, 'ArrayBuffer is cloned'); + +promise_test(async t => { + const body = new Blob(['a']); + const res = new Response(body); + const req = new Request(url, { method, body }); + assert_true(await res.blob() !== body); + assert_true(await req.blob() !== body); +}, 'Blob is cloned'); diff --git a/testing/web-platform/tests/fetch/api/body/formdata.any.js b/testing/web-platform/tests/fetch/api/body/formdata.any.js new file mode 100644 index 0000000000..e25035923c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/body/formdata.any.js @@ -0,0 +1,14 @@ +promise_test(async t => { + const res = new Response(new FormData()); + const fd = await res.formData(); + assert_true(fd instanceof FormData); +}, 'Consume empty response.formData() as FormData'); + +promise_test(async t => { + const req = new Request('about:blank', { + method: 'POST', + body: new FormData() + }); + const fd = await req.formData(); + assert_true(fd instanceof FormData); +}, 'Consume empty request.formData() as FormData'); diff --git a/testing/web-platform/tests/fetch/api/body/mime-type.any.js b/testing/web-platform/tests/fetch/api/body/mime-type.any.js new file mode 100644 index 0000000000..67c9af7da2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/body/mime-type.any.js @@ -0,0 +1,127 @@ +[ + () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }), + () => new Response("", { headers: { "Content-Type": "text/plain" } }) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + const newMIMEType = "test/test"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }), + () => new Response(new URLSearchParams()), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8"); + bodyContainer.headers.delete("Content-Type"); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + }, `${bodyContainer.constructor.name}: removing implicit Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }), + () => new Response(new ArrayBuffer()), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), null); + const newMIMEType = "test/test"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: setting missing Content-Type`); +}); + +[ + () => new Request("about:blank", { method: "POST" }), + () => new Response(), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`); +}); + +[ + () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }), + () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] }) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, 'mytext/plain'); + }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new Blob([""]), method: "POST" }), + () => new Response(new Blob([""])) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + assert_equals(bodyContainer.headers.get("Content-Type"), null); + }, `${bodyContainer.constructor.name}: MIME type for Blob`); +}); + +[ + () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }), + () => new Response(new Blob([""], { type: "Text/Plain" })) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, "text/plain"); + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`); +}); + +[ + () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }), + () => new Response(new Blob([""], { type: "Text/Plain" }, { headers: [["Content-Type", "Text/Html"]] })) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + const cloned = bodyContainer.clone(); + promise_test(async t => { + const blobs = [await bodyContainer.blob(), await cloned.blob()]; + assert_equals(blobs[0].type, "text/html"); + assert_equals(blobs[1].type, "text/html"); + assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html"); + assert_equals(cloned.headers.get("Content-Type"), "Text/Html"); + }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`); +}); + +[ + () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }), + () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/html"); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, "text/html"); + }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`); +}); + +[ + () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }), + () => new Response(new Blob([], { type: "text/plain" })), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + const newMIMEType = "text/html"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`); +}); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js b/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js new file mode 100644 index 0000000000..95de0af2d8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js @@ -0,0 +1,43 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +const { + HTTPS_ORIGIN, + HTTP_ORIGIN_WITH_DIFFERENT_PORT, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + HTTPS_REMOTE_ORIGIN, +} = get_host_info(); + +function cors(desc, origin) { + const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; + const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; + + promise_test((test) => { + return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => { + assert_equals(resp.status, 0, "Opaque filter: status is 0"); + assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); + assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); + return resp.text().then((value) => { + assert_equals(value, "", "Opaque response should have an empty body"); + }); + }); + }, `${desc} [no-cors mode]`); + + promise_test((test) => { + return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'})); + }, `${desc} [server forbid CORS]`); + + promise_test((test) => { + return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => { + assert_equals(resp.status, 200, "Fetch's response's status is 200"); + assert_equals(resp.type , "cors", "CORS response's type is cors"); + }); + }, `${desc} [cors mode]`); +} + +cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT); +cors('Same domain different protocol different port', HTTPS_ORIGIN); +cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN); +cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); +cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js new file mode 100644 index 0000000000..f5217b4246 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js @@ -0,0 +1,49 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + +var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; +urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)"; + +urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)"; +urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)"; + +urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)"; +urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)"; + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Set cookies"); + +function doTest(usePreflight) { + promise_test(async (test) => { + var url = redirectUrl; + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=301"; + urlParameters += "&location=" + encodeURIComponent(urlCheckCookies); + urlParameters += "&allow_headers=a&headers=Cookie"; + headers = []; + if (usePreflight) + headers.push(["a", "b"]); + + var requestInit = {"credentials": "include", "mode": "cors", "headers": headers}; + var response = await fetch(url + urlParameters, requestInit); + + assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)"); + }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight"); +} + +doTest(false); +doTest(true); + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Clean cookies"); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js b/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js new file mode 100644 index 0000000000..8c666e4782 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js @@ -0,0 +1,56 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) { + var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + //enable cors with credentials + var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlParameters += "|header(Access-Control-Allow-Credentials,true)"; + + var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)"; + if (cookies) { + urlParameters += "|header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters += "|header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentialsMode, "mode": "cors"}; + + promise_test(function(test){ + return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + //check cookies sent + return fetch(urlCheckCookies, requestInit); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentialsMode === "include" && baseURL1 === baseURL2) { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie"); + } + //clean cookies + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) { + throw e; + }) + }); + }, desc); +} + +var local = get_host_info().HTTP_ORIGIN; +var remote = get_host_info().HTTP_REMOTE_ORIGIN; +// FIXME: otherRemote might not be accessible on some test environments. +var otherRemote = local.replace("http://", "http://www."); + +corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]); +corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]); +corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]); +corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]); +corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]); +corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js new file mode 100644 index 0000000000..340e99ab5f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js @@ -0,0 +1,41 @@ +// META: script=../resources/utils.js + +const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt", + sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|" + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "Basic Access-Control-Expose-Headers: * support") + +promise_test(() => { + const origin = location.origin, // assuming an ASCII origin + headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)" + return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted + assert_equals(resp.headers.get("test"), null) + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* for credentialed fetches only matches literally") + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* can be one of several values") + +done(); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js new file mode 100644 index 0000000000..a26eaccf2a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js @@ -0,0 +1,69 @@ +// META: script=../resources/utils.js + +function corsFilter(corsUrl, headerName, headerValue, isFiltered) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)"; + promise_test(function(test) { + return fetch(url).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isFiltered) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, "CORS filter on " + headerName + " header"); +} + +function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" + + "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" + + "header(Access-Control-Allow-Credentials, true)" + + "header(Access-Control-Expose-Headers," + headerName + ")"; + + var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed"); + if (withCredentials) + title+= "(credentials = include)"; + promise_test(function(test) { + return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isForbidden) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, title); +} + +var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + +corsFilter(url, "Cache-Control", "no-cache", false); +corsFilter(url, "Content-Language", "fr", false); +corsFilter(url, "Content-Type", "text/html", false); +corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false); +corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false); +corsFilter(url, "Pragma", "no-cache", false); +corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top" + +corsFilter(url, "Age", "27", true); +corsFilter(url, "Server", "wptServe" , true); +corsFilter(url, "Warning", "Mind the gap" , true); +corsFilter(url, "Set-Cookie", "name=value" , true); +corsFilter(url, "Set-Cookie2", "name=value" , true); + +corsExposeFilter(url, "Age", "27", false); +corsExposeFilter(url, "Server", "wptServe" , false); +corsExposeFilter(url, "Warning", "Mind the gap" , false); + +corsExposeFilter(url, "Set-Cookie", "name=value" , true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true); +corsExposeFilter(url, "Set-Cookie", "name=value" , true, true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true, true); + +done(); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js b/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js new file mode 100644 index 0000000000..f54bf4f9b6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js @@ -0,0 +1,116 @@ +// META: global=window +// META: timeout=long +// META: title=Fetch API: keepalive handling +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js +// META: script=../resources/utils.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTPS_ORIGIN, + HTTP_ORIGIN_WITH_DIFFERENT_PORT, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + HTTPS_REMOTE_ORIGIN, +} = get_host_info(); + +/** + * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests + * to different `origin` when the initiator document is still alive. They should + * behave the same as without setting keepalive. + */ +function keepaliveCorsBasicTest(desc, origin) { + const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; + const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; + + promise_test((test) => { + return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'}) + .then((resp) => { + assert_equals(resp.status, 0, 'Opaque filter: status is 0'); + assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""'); + assert_equals( + resp.type, 'opaque', 'Opaque filter: response\'s type is opaque'); + return resp.text().then((value) => { + assert_equals( + value, '', 'Opaque response should have an empty body'); + }); + }); + }, `${desc} [no-cors mode]`); + + promise_test((test) => { + return promise_rejects_js( + test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'})); + }, `${desc} [cors mode, server forbid CORS]`); + + promise_test((test) => { + return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'}) + .then((resp) => { + assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200'); + assert_equals(resp.type, 'cors', 'CORS response\'s type is cors'); + }); + }, `${desc} [cors mode]`); +} + +keepaliveCorsBasicTest( + `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT); +keepaliveCorsBasicTest( + `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN); +keepaliveCorsBasicTest( + `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN); +keepaliveCorsBasicTest( + `[keepalive] Cross domain different port`, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); +keepaliveCorsBasicTest( + `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN); + +/** + * In a same-site iframe, and in `unload` event handler, test to fetch + * a keepalive URL that involves in different cors modes. + */ +function keepaliveCorsInUnloadTest(description, origin, method) { + const evt = 'unload'; + for (const mode of ['no-cors', 'cors']) { + for (const disallowCrossOrigin of [false, true]) { + const desc = `${description} ${method} request in ${evt} [${mode} mode` + + (disallowCrossOrigin ? ']' : ', server forbid CORS]'); + const expectTokenExist = !disallowCrossOrigin || mode === 'no-cors'; + promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveIframeUrl(token1, method, { + frameOrigin: '', + requestOrigin: origin, + sendOn: evt, + mode: mode, + disallowCrossOrigin + }); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + iframe.remove(); + assert_equals(await getTokenFromMessage(), token1); + + assertStashedTokenAsync(desc, token1, {expectTokenExist}); + }, `${desc}; setting up`); + } + } +} + +for (const method of ['GET', 'POST']) { + keepaliveCorsInUnloadTest( + '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT, + method); + keepaliveCorsInUnloadTest( + `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN, + method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain different port`, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN, + method); +} diff --git a/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js new file mode 100644 index 0000000000..b3abb92284 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function corsMultipleOrigins(originList) { + var urlParameters = "?origin=" + encodeURIComponent(originList.join(", ")); + var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters)); + }, "Listing multiple origins is illegal: " + originList); +} +/* Actual origin */ +var origin = "http://{{host}}:{{ports[http][0]}}"; + +corsMultipleOrigins(["\"\"", "http://example.com", origin]); +corsMultipleOrigins(["\"\"", "http://example.com", "*"]); +corsMultipleOrigins(["\"\"", origin, origin]); +corsMultipleOrigins(["*", "http://example.com", "*"]); +corsMultipleOrigins(["*", "http://example.com", origin]); +corsMultipleOrigins(["", "http://example.com", "https://example2.com"]); + +done(); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js new file mode 100644 index 0000000000..7a0269aae4 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js @@ -0,0 +1,41 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsNoPreflight(desc, baseURL, method, headerName, headerValue) { + + var uuid_token = token(); + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method, "headers":{}}; + if (headerName) + requestInit["headers"][headerName] = headerValue; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + }); + }); + }, desc); +} + +var host_info = get_host_info(); + +corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET"); +corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST"); +corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD"); +corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*"); +corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded"); +corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8"); +corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8"); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js b/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js new file mode 100644 index 0000000000..30a02d910f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* If origin is undefined, it is set to fetched url's origin*/ +function corsOrigin(desc, baseURL, method, origin, shouldPass) { + if (!origin) + origin = baseURL; + + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method; + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var requestInit = {"mode": "cors", "method": method}; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + if (shouldPass) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); + +} + +var host_info = get_host_info(); + +/* Actual origin */ +var origin = host_info.HTTP_ORIGIN; + +corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true); +corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false); +corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true); +corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false); +corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true); +corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false); +corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true); +corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false); +corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js new file mode 100644 index 0000000000..ce6a169d81 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js @@ -0,0 +1,46 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var cors_url = get_host_info().HTTP_REMOTE_ORIGIN + + dirname(location.pathname) + + RESOURCES_DIR + + "preflight.py"; + +promise_test((test) => { + var uuid_token = token(); + var request_url = + cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" + + "&allow_headers=x-test-header"; + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash") + .then(() => { + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test1"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }) + .then((res) => res.text()) + .then((txt) => { + assert_equals(txt, "1", "Server stash must be cleared."); + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test2"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }); +}); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js new file mode 100644 index 0000000000..b2747ccd5b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js @@ -0,0 +1,19 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +function runTests(testArray) { + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header", + corsURL, + "GET", + true, + [[headerName, headerValue]]); + }); +} diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js new file mode 100644 index 0000000000..15f7659abd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + if (redirectPreflight) + urlParameters += "&redirect_preflight"; + var requestInit = {"mode": "cors", "redirect": "follow"}; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + }); + }, desc); +} + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +for (var code of [301, 302, 303, 307, 308]) { + /* preflight should not follow the redirection */ + corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true); + /* preflight is done before redirection: preflight force redirect to error */ + corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false); +} diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js new file mode 100644 index 0000000000..5df9fcf142 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) { + var uuid_token = token(); + var url = corsUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy}; + + if (referrer) + requestInit.referrer = referrer; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct"); + assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct"); + assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value"); + }); + }); + }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default")); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +var origin = get_host_info().HTTP_ORIGIN + "/"; + +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, ""); +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", ""); + +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin); +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString()) +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString()); + +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString()); +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString()); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js new file mode 100644 index 0000000000..718e351c1d --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js @@ -0,0 +1,33 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + if (allowHeaders) + urlParameters += "," + allowHeaders; + if (allowMethods) + urlParameters += "&allow_methods="+ allowMethods; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + + return fetch(url + urlParameters).then(function(resp) { + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null); +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value"); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js new file mode 100644 index 0000000000..f9fb20469c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js @@ -0,0 +1,86 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py", + origin = location.origin // assuming an ASCII origin + +function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) { + return promise_test(t => { + let testURL = url + "?", + requestInit = {} + if (withCredentials) { + testURL += "origin=" + origin + "&" + testURL += "credentials&" + requestInit.credentials = "include" + } + if (useMethod) { + requestInit.method = useMethod + } + if (useHeader.length > 0) { + requestInit.headers = [useHeader] + } + testURL += "allow_methods=" + allowMethod + "&" + testURL += "allow_headers=" + allowHeader + "&" + + if (succeeds) { + return fetch(testURL, requestInit).then(resp => { + assert_equals(resp.headers.get("x-origin"), origin) + }) + } else { + return promise_rejects_js(t, TypeError, fetch(testURL, requestInit)) + } + }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")") +} + +// "GET" does not pass the case-sensitive method check, but in the safe list. +preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"]) +// Headers check is case-insensitive, and "*" works as any for method. +preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"]) +// "*" works as any only without credentials. +preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "", "PUT", []) +preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"]) +// Exact character match works even for "*" with credentials. +preflightTest(true, true, "*", "*", "*", ["*", "1"]) + +// The following methods are upper-cased for init["method"] by +// https://fetch.spec.whatwg.org/#concept-method-normalize +// but not in Access-Control-Allow-Methods response. +// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method, +// CORS anyway passes regardless of the cases. +for (const METHOD of ['GET', 'HEAD', 'POST']) { + const method = METHOD.toLowerCase(); + preflightTest(true, true, METHOD, "*", METHOD, []) + preflightTest(true, true, METHOD, "*", method, []) + preflightTest(true, true, method, "*", METHOD, []) + preflightTest(true, true, method, "*", method, []) +} + +// The following methods are upper-cased for init["method"] by +// https://fetch.spec.whatwg.org/#concept-method-normalize +// but not in Access-Control-Allow-Methods response. +// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method, +// Access-Control-Allow-Methods should contain upper-cased methods, +// while init["method"] can be either in upper or lower case. +for (const METHOD of ['DELETE', 'PUT']) { + const method = METHOD.toLowerCase(); + preflightTest(true, true, METHOD, "*", METHOD, []) + preflightTest(true, true, METHOD, "*", method, []) + preflightTest(false, true, method, "*", METHOD, []) + preflightTest(false, true, method, "*", method, []) +} + +// "PATCH" is NOT upper-cased in both places because it is not listed in +// https://fetch.spec.whatwg.org/#concept-method-normalize. +// So Access-Control-Allow-Methods value and init["method"] should match +// case-sensitively. +preflightTest(true, true, "PATCH", "*", "PATCH", []) +preflightTest(false, true, "PATCH", "*", "patch", []) +preflightTest(false, true, "patch", "*", "PATCH", []) +preflightTest(true, true, "patch", "*", "patch", []) + +// "Authorization" header can't be wildcarded. +preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"]) +preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"]) diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js new file mode 100644 index 0000000000..a4467a6087 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* Check preflight is ok if status is ok status (200 to 299)*/ +function corsPreflightStatus(desc, corsUrl, preflightStatus) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + urlParameters += "&preflight_status=" + preflightStatus; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + if (200 <= preflightStatus && 299 >= preflightStatus) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +for (status of [200, 201, 202, 203, 204, 205, 206, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + 400, 401, 402, 403, 404, 405, + 501, 502, 503, 504, 505]) + corsPreflightStatus("Preflight answered with status " + status, corsUrl, status); diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js new file mode 100644 index 0000000000..045422f40b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js @@ -0,0 +1,62 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true); +corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false); +corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true); +corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true); +corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false); +corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true); +corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false); +corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true); +corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false); +corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true); +corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false); +corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true); +corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false); + +corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]); +corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]); + +var headers = [ + ["x-test-header1", "allowedOrRefused"], + ["x-test-header2", "allowedOrRefused"], + ["X-test-header3", "allowedOrRefused"], + ["x-test-header-b", "allowedOrRefused"], + ["x-test-header-D", "allowedOrRefused"], + ["x-test-header-C", "allowedOrRefused"], + ["x-test-header-a", "allowedOrRefused"], + ["Content-Type", "allowedOrRefused"], +]; +var safeHeaders= [ + ["Accept", "*"], + ["Accept-Language", "bzh"], + ["Content-Language", "eu"], +]; + +corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders); +corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders); + +corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders); + +promise_test(async t => { + const url = `${corsUrl}?allow_headers=*`; + await promise_rejects_js(t, TypeError, fetch(url, { + headers: { + authorization: 'foobar' + } + })); +}, '"authorization" should not be covered by the wildcard symbol'); + +promise_test(async t => { + const url = `${corsUrl}?allow_headers=authorization`; + await fetch(url, { headers: { + authorization: 'foobar' + }}); +}, '"authorization" should be covered by "authorization"');
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js new file mode 100644 index 0000000000..2aff313406 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js @@ -0,0 +1,52 @@ +// META: timeout=long +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) { + var url = redirectUrl + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@"); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + promise_test(t => { + const result = fetch(url + urlParameters, requestInit) + if(locationCredentials === "") { + return result; + } else { + return promise_rejects_js(t, TypeError, result); + } + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; +var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, ""); + + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password"); +} diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js new file mode 100644 index 0000000000..50848170d0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js @@ -0,0 +1,46 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) { + var urlBaseParameters = "&redirect_status=" + redirectStatus; + var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c"); + var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]}; + + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc + " (preflight after redirection success case)"); + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit)); + }); + }, desc + " (preflight after redirection failure case)"); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code); +} diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js new file mode 100644 index 0000000000..cdf4097d56 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + return promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect"); + }); + }); + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null"); + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null"); +} diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html b/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html new file mode 100644 index 0000000000..217baa3c46 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body></body> +<script> + +const createDataUrlIframe = (url, cors) => { + const iframe = document.createElement("iframe"); + const fetchURL = new URL(url, location.href) + + `${cors === 'null-origin' + ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`; + const tag_name = 'script'; + iframe.src = + `data:text/html, <${tag_name}>` + + `async function test() {` + + ` let allowed = true;` + + ` try {` + + ` await fetch('${fetchURL}');` + + ` } catch (e) {` + + ` allowed = false;` + + ` }` + + ` parent.postMessage({allowed}, '*');` + + `}` + + `test(); </${tag_name}>`; + return iframe; +}; + +const fetch_from_data_url_iframe_test = + (url, cors, expectation, description) => { + promise_test(async () => { + const iframe = createDataUrlIframe(url, cors); + document.body.appendChild(iframe); + const msgEvent = await new Promise(resolve => window.onmessage = resolve); + assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation); + }, description); +}; + +fetch_from_data_url_iframe_test( + '../resources/top.txt', + 'acao-omitted', + 'rejected', + 'fetching "top.txt" without ACAO should be rejected.' +); +fetch_from_data_url_iframe_test( + '../resources/top.txt', + 'null-origin', + 'allowed', + 'fetching "top.txt" with CORS allowing null origin should be allowed.' +); +fetch_from_data_url_iframe_test( + 'data:text/plain, top', + 'acao-omitted', + 'allowed', + 'fetching data url script should be allowed.' +); + +</script> diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html b/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html new file mode 100644 index 0000000000..d69748ab26 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +const fetch_from_data_url_worker_test = + (url, cors, expectation, description) => { + promise_test(async () => { + const fetchURL = new URL(url, location.href) + + `${cors === 'null-origin' + ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`; + const scriptURL = + `data:text/javascript,` + + `async function test(port) {` + + ` let allowed = true;` + + ` try {` + + ` await fetch('${fetchURL}');` + + ` } catch (e) {` + + ` allowed = false;` + + ` }` + + ` port.postMessage({allowed});` + + `}` + + `onconnect = e => {` + + ` test(e.ports[0]);` + + `};`; + const worker = new SharedWorker(scriptURL); + const msgEvent = + await new Promise(resolve => worker.port.onmessage = resolve); + assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation); + }, description); +}; + +fetch_from_data_url_worker_test( + '../resources/top.txt', + 'acao-omitted', + 'rejected', + 'fetching "top.txt" without ACAO should be rejected.' +); +fetch_from_data_url_worker_test( + '../resources/top.txt', + 'null-origin', + 'allowed', + 'fetching "top.txt" with CORS allowing null origin should be allowed.' +); +fetch_from_data_url_worker_test( + 'data:text/plain, top', + 'acao-omitted', + 'allowed', + 'fetching data url script should be allowed.' +); + +</script> diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-worker.html b/testing/web-platform/tests/fetch/api/cors/data-url-worker.html new file mode 100644 index 0000000000..13113e6262 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/data-url-worker.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +const fetch_from_data_url_shared_worker_test = + (url, cors, expectation, description) => { + promise_test(async () => { + const fetchURL = new URL(url, location.href) + + `${cors === 'null-origin' + ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`; + const scriptURL = + `data:text/javascript,` + + `async function test() {` + + ` let allowed = true;` + + ` try {` + + ` await fetch('${fetchURL}');` + + ` } catch (e) {` + + ` allowed = false;` + + ` }` + + ` postMessage({allowed});` + + `}` + + `test();`; + const worker = new Worker(scriptURL); + const msgEvent = await new Promise(resolve => worker.onmessage = resolve); + assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation); + }, description); +}; + +fetch_from_data_url_shared_worker_test( + '../resources/top.txt', + 'acao-omitted', + 'rejected', + 'fetching "top.txt" without ACAO should be rejected.' +); +fetch_from_data_url_shared_worker_test( + '../resources/top.txt', + 'null-origin', + 'allowed', + 'fetching "top.txt" with CORS allowing null origin should be allowed.' +); +fetch_from_data_url_shared_worker_test( + 'data:text/plain, top', + 'acao-omitted', + 'allowed', + 'fetching data url script should be allowed.' +); + +</script> diff --git a/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js b/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js new file mode 100644 index 0000000000..18b8f6dfa2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js @@ -0,0 +1,58 @@ +function headerNames(headers) { + let names = []; + for (let header of headers) { + names.push(header[0].toLowerCase()); + } + return names; +} + +/* + Check preflight is done + Control if server allows method and headers and check accordingly + Check control access headers added by UA (for method and headers) +*/ +function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) { + return promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) { + var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&"); + var urlParameters = "token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method}; + var requestHeaders = []; + if (headers) + requestHeaders.push.apply(requestHeaders, headers); + if (safeHeaders) + requestHeaders.push.apply(requestHeaders, safeHeaders); + requestInit["headers"] = requestHeaders; + + if (allowed) { + urlParameters += "&allow_methods=" + method + "&control_request_headers"; + if (headers) { + //Make the server allow the headers + urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C"); + } + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + if (headers) { + var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(","); + for (var i in actualHeaders) + actualHeaders[i] = actualHeaders[i].trim(); + for (var header of headers) + assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header); + + let accessControlAllowHeaders = headerNames(headers).sort().join(","); + assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value"); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + } else { + assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted") + } + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){ + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + }); + } + }); + }, desc); +} diff --git a/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json b/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json new file mode 100644 index 0000000000..945dc0f93b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json @@ -0,0 +1,13 @@ +[ + ["accept", "\""], + ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"], + ["accept-language", "\u0001"], + ["accept-language", "@"], + ["authorization", "basics"], + ["content-language", "\u0001"], + ["content-language", "@"], + ["content-type", "text/html"], + ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"], + ["range", "bytes 0-"], + ["test", "hi"] +] diff --git a/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html b/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html new file mode 100644 index 0000000000..feb9f1f2e5 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<iframe sandbox="allow-scripts" src="../resources/sandboxed-iframe.html"></iframe> +<script> +promise_test(async (t) => { + const message = await new Promise((resolve) => { + window.addEventListener('message', e => resolve(e.data)); + }); + assert_equals(message, 'PASS'); +}, 'CORS with sandboxed iframe'); +</script> +</html> diff --git a/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html b/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html new file mode 100644 index 0000000000..fa1ad1717f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script type="module"> + const abort = new AbortController(); + const resp = await fetch("5401a7dfd80adbd578b3e91b86fdc6966a752de7.vtt", { + signal: abort.signal, + }); + abort.abort(); + await resp.body.closed; + const cache = await caches.open("cache_name_0"); + await cache.put("bb4ea079adb4fe423f1d6cec18bc1caf78ac4cd6.ico", resp); +</script> diff --git a/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html b/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html new file mode 100644 index 0000000000..646d3c5f8c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<iframe srcdoc=' + <script> + let a = new Blob(["a", "ð¢¾"], {}) + let b = new Response(a) + try { let _ = b.body } catch (e) { } + frameElement.remove() + b.json().catch(() => {}) + </script> +'></iframe> diff --git a/testing/web-platform/tests/fetch/api/crashtests/request.html b/testing/web-platform/tests/fetch/api/crashtests/request.html new file mode 100644 index 0000000000..2d21930c3b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/crashtests/request.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/common/utils.js"></script> +<script> + // Cycle collection test for a case where the Request object is alive and accessible globally. + var req = new Request(`/`); + fetch(req) +</script> diff --git a/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js b/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js new file mode 100644 index 0000000000..31ccc38697 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +function basicAuth(desc, user, pass, mode, status) { + promise_test(function(test) { + var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)}; + var requestInit = {"credentials": mode, "headers": headers}; + return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) { + assert_equals(resp.status, status, "HTTP status is " + status); + assert_equals(resp.type , "basic", "Response's type is basic"); + }); + }, desc); +} + +basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200); +basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200); +basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200); +basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401); diff --git a/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js b/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js new file mode 100644 index 0000000000..16656b5435 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const authorizationValue = "Basic " + btoa("user:pass"); +async function getAuthorizationHeaderValue(url) +{ + const headers = { "Authorization": authorizationValue}; + const requestInit = {"headers": headers}; + const response = await fetch(url, requestInit); + return response.text(); +} + +promise_test(async test => { + const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py"); + assert_equals(result, authorizationValue); +}, "getAuthorizationHeaderValue - no redirection"); + +promise_test(async test => { + result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py")); + assert_equals(result, authorizationValue); + + result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/dump-authorization-header.py")); + assert_equals(result, authorizationValue); +}, "getAuthorizationHeaderValue - same origin redirection"); + +promise_test(async (test) => { + const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py")); + assert_equals(result, "none"); +}, "getAuthorizationHeaderValue - cross origin redirection"); diff --git a/testing/web-platform/tests/fetch/api/credentials/cookies.any.js b/testing/web-platform/tests/fetch/api/credentials/cookies.any.js new file mode 100644 index 0000000000..de30e47765 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/credentials/cookies.any.js @@ -0,0 +1,49 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function cookies(desc, credentials1, credentials2 ,cookies) { + var url = RESOURCES_DIR + "top.txt" + var urlParameters = ""; + var urlCleanParameters = ""; + if (cookies) { + urlParameters +="?pipe=header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters +="?pipe=header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentials1} + promise_test(function(test){ + var requestInit = {"credentials": credentials1} + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + //check cookies sent + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2}); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentials1 != "omit" && credentials2 != "omit") { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)"); + } + //clean cookies + return fetch(url + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() { + return Promise.reject(e); + }); + }); + }, desc); +} + +cookies("Include mode: 1 cookie", "include", "include", ["a=1"]); +cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]); +cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]); +cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]); +cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]); +cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]); +cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]); diff --git a/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js b/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js new file mode 100644 index 0000000000..cafb780c2c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js @@ -0,0 +1,266 @@ +// META: title=Headers set-cookie special cases +// META: global=window,worker + +const headerList = [ + ["set-cookie", "foo=bar"], + ["Set-Cookie", "fizz=buzz; domain=example.com"], +]; + +const setCookie2HeaderList = [ + ["set-cookie2", "foo2=bar2"], + ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"], +]; + +function assert_nested_array_equals(actual, expected) { + assert_equals(actual.length, expected.length, "Array length is not equal"); + for (let i = 0; i < expected.length; i++) { + assert_array_equals(actual[i], expected[i]); + } +} + +test(function () { + const headers = new Headers(headerList); + assert_equals( + headers.get("set-cookie"), + "foo=bar, fizz=buzz; domain=example.com", + ); +}, "Headers.prototype.get combines set-cookie headers in order"); + +test(function () { + const headers = new Headers(headerList); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz; domain=example.com"], + ]); +}, "Headers iterator does not combine set-cookie headers"); + +test(function () { + const headers = new Headers(setCookie2HeaderList); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers iterator does not special case set-cookie2 headers"); + +test(function () { + const headers = new Headers([...headerList, ...setCookie2HeaderList]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz; domain=example.com"], + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers iterator does not combine set-cookie & set-cookie2 headers"); + +test(function () { + // Values are in non alphabetic order, and the iterator should yield in the + // headers in the exact order of the input. + const headers = new Headers([ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); +}, "Headers iterator preserves set-cookie ordering"); + +test( + function () { + const headers = new Headers([ + ["xylophone-header", "1"], + ["best-header", "2"], + ["set-cookie", "3"], + ["a-cool-header", "4"], + ["set-cookie", "5"], + ["a-cool-header", "6"], + ["best-header", "7"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["a-cool-header", "4, 6"], + ["best-header", "2, 7"], + ["set-cookie", "3"], + ["set-cookie", "5"], + ["xylophone-header", "1"], + ]); + }, + "Headers iterator preserves per header ordering, but sorts keys alphabetically", +); + +test( + function () { + const headers = new Headers([ + ["xylophone-header", "7"], + ["best-header", "6"], + ["set-cookie", "5"], + ["a-cool-header", "4"], + ["set-cookie", "3"], + ["a-cool-header", "2"], + ["best-header", "1"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["a-cool-header", "4, 2"], + ["best-header", "6, 1"], + ["set-cookie", "5"], + ["set-cookie", "3"], + ["xylophone-header", "7"], + ]); + }, + "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)", +); + +test(function () { + const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]); + const iterator = headers[Symbol.iterator](); + assert_array_equals(iterator.next().value, ["fizz", "buzz"]); + headers.append("Set-Cookie", "a=b"); + assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); + headers.append("Accept", "text/html"); + assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); + assert_array_equals(iterator.next().value, ["x-header", "test"]); + headers.append("set-cookie", "c=d"); + assert_array_equals(iterator.next().value, ["x-header", "test"]); + assert_true(iterator.next().done); +}, "Headers iterator is correctly updated with set-cookie changes"); + +test(function () { + const headers = new Headers([ + ["set-cookie", "a"], + ["set-cookie", "b"], + ["set-cookie", "c"] + ]); + const iterator = headers[Symbol.iterator](); + assert_array_equals(iterator.next().value, ["set-cookie", "a"]); + headers.delete("set-cookie"); + headers.append("set-cookie", "d"); + headers.append("set-cookie", "e"); + headers.append("set-cookie", "f"); + assert_array_equals(iterator.next().value, ["set-cookie", "e"]); + assert_array_equals(iterator.next().value, ["set-cookie", "f"]); + assert_true(iterator.next().done); +}, "Headers iterator is correctly updated with set-cookie changes #2"); + +test(function () { + const headers = new Headers(headerList); + assert_true(headers.has("sEt-cOoKiE")); +}, "Headers.prototype.has works for set-cookie"); + +test(function () { + const headers = new Headers(setCookie2HeaderList); + headers.append("set-Cookie", "foo=bar"); + headers.append("sEt-cOoKiE", "fizz=buzz"); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz"], + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers.prototype.append works for set-cookie"); + +test(function () { + const headers = new Headers(headerList); + headers.set("set-cookie", "foo2=bar2"); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo2=bar2"], + ]); +}, "Headers.prototype.set works for set-cookie"); + +test(function () { + const headers = new Headers(headerList); + headers.delete("set-Cookie"); + const list = [...headers]; + assert_nested_array_equals(list, []); +}, "Headers.prototype.delete works for set-cookie"); + +test(function () { + const headers = new Headers(); + assert_array_equals(headers.getSetCookie(), []); +}, "Headers.prototype.getSetCookie with no headers present"); + +test(function () { + const headers = new Headers([headerList[0]]); + assert_array_equals(headers.getSetCookie(), ["foo=bar"]); +}, "Headers.prototype.getSetCookie with one header"); + +test(function () { + const headers = new Headers({ "Set-Cookie": "foo=bar" }); + assert_array_equals(headers.getSetCookie(), ["foo=bar"]); +}, "Headers.prototype.getSetCookie with one header created from an object"); + +test(function () { + const headers = new Headers(headerList); + assert_array_equals(headers.getSetCookie(), [ + "foo=bar", + "fizz=buzz; domain=example.com", + ]); +}, "Headers.prototype.getSetCookie with multiple headers"); + +test(function () { + const headers = new Headers([["set-cookie", ""]]); + assert_array_equals(headers.getSetCookie(), [""]); +}, "Headers.prototype.getSetCookie with an empty header"); + +test(function () { + const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]); + assert_array_equals(headers.getSetCookie(), ["x", "x"]); +}, "Headers.prototype.getSetCookie with two equal headers"); + +test(function () { + const headers = new Headers([ + ["set-cookie2", "x"], + ["set-cookie", "y"], + ["set-cookie2", "z"], + ]); + assert_array_equals(headers.getSetCookie(), ["y"]); +}, "Headers.prototype.getSetCookie ignores set-cookie2 headers"); + +test(function () { + // Values are in non alphabetic order, and the iterator should yield in the + // headers in the exact order of the input. + const headers = new Headers([ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); + assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]); +}, "Headers.prototype.getSetCookie preserves header ordering"); + +test(function () { + const headers = new Headers({"Set-Cookie": " a=b\n"}); + headers.append("set-cookie", "\n\rc=d "); + assert_nested_array_equals([...headers], [ + ["set-cookie", "a=b"], + ["set-cookie", "c=d"] + ]); + headers.set("set-cookie", "\te=f "); + assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]); +}, "Adding Set-Cookie headers normalizes their value"); + +test(function () { + assert_throws_js(TypeError, () => { + new Headers({"set-cookie": "\0"}); + }); + + const headers = new Headers(); + assert_throws_js(TypeError, () => { + headers.append("Set-Cookie", "a\nb"); + }); + assert_throws_js(TypeError, () => { + headers.set("Set-Cookie", "a\rb"); + }); +}, "Adding invalid Set-Cookie headers throws"); + +test(function () { + const response = new Response(); + response.headers.append("Set-Cookie", "foo=bar"); + assert_array_equals(response.headers.getSetCookie(), []); + response.headers.append("sEt-cOokIe", "bar=baz"); + assert_array_equals(response.headers.getSetCookie(), []); +}, "Set-Cookie is a forbidden response header"); diff --git a/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js b/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js new file mode 100644 index 0000000000..5710554ada --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js @@ -0,0 +1,72 @@ +// META: title=Header value normalizing test +// META: global=window,worker +// META: timeout=long + +"use strict"; + +for(let i = 0; i < 0x21; i++) { + let fail = false, + strip = false + + // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed + if(i === 0x0B || i === 0x0C) + continue + + if(i === 0) { + fail = true + } + + if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) { + strip = true + } + + let url = "../resources/inspect-headers.py?headers=val1|val2|val3", + val = String.fromCharCode(i), + expectedVal = strip ? "" : val, + val1 = val, + expectedVal1 = expectedVal, + val2 = "x" + val, + expectedVal2 = "x" + expectedVal, + val3 = val + "x", + expectedVal3 = expectedVal + "x" + + // XMLHttpRequest is not available in service workers + if (!self.GLOBAL.isWorker()) { + async_test((t) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", url) + if(fail) { + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1)) + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2)) + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3)) + t.done() + } else { + xhr.setRequestHeader("val1", val1) + xhr.setRequestHeader("val2", val2) + xhr.setRequestHeader("val3", val3) + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1) + assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2) + assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3) + }) + xhr.send() + } + }, "XMLHttpRequest with value " + encodeURI(val)) + } + + promise_test((t) => { + if(fail) { + return Promise.all([ + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })), + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })), + promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} })) + ]) + } else { + return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => { + assert_equals(res.headers.get("x-request-val1"), expectedVal1) + assert_equals(res.headers.get("x-request-val2"), expectedVal2) + assert_equals(res.headers.get("x-request-val3"), expectedVal3) + }) + } + }, "fetch() with value " + encodeURI(val)) +} diff --git a/testing/web-platform/tests/fetch/api/headers/header-values.any.js b/testing/web-platform/tests/fetch/api/headers/header-values.any.js new file mode 100644 index 0000000000..bb7570c5a3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/header-values.any.js @@ -0,0 +1,63 @@ +// META: title=Header value test +// META: global=window,worker +// META: timeout=long + +"use strict"; + +// Invalid values +[0, 0x0A, 0x0D].forEach(val => { + val = "x" + String.fromCharCode(val) + "x" + + // XMLHttpRequest is not available in service workers + if (!self.GLOBAL.isWorker()) { + test(() => { + let xhr = new XMLHttpRequest() + xhr.open("POST", "/") + assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val)) + }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw") + } + + promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw") +}) + +// Valid values +let headerValues =[] +for(let i = 0; i < 0x100; i++) { + if(i === 0 || i === 0x0A || i === 0x0D) { + continue + } + headerValues.push("x" + String.fromCharCode(i) + "x") +} +var url = "../resources/inspect-headers.py?headers=" +headerValues.forEach((_, i) => { + url += "val" + i + "|" +}) + +// XMLHttpRequest is not available in service workers +if (!self.GLOBAL.isWorker()) { + async_test((t) => { + let xhr = new XMLHttpRequest() + xhr.open("POST", url) + headerValues.forEach((val, i) => { + xhr.setRequestHeader("val" + i, val) + }) + xhr.onload = t.step_func_done(() => { + headerValues.forEach((val, i) => { + assert_equals(xhr.getResponseHeader("x-request-val" + i), val) + }) + }) + xhr.send() + }, "XMLHttpRequest with all valid values") +} + +promise_test((t) => { + const headers = new Headers + headerValues.forEach((val, i) => { + headers.append("val" + i, val) + }) + return fetch(url, { headers }).then((res) => { + headerValues.forEach((val, i) => { + assert_equals(res.headers.get("x-request-val" + i), val) + }) + }) +}, "fetch() with all valid values") diff --git a/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js b/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js new file mode 100644 index 0000000000..ead1047645 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js @@ -0,0 +1,275 @@ +// META: title=Headers structure +// META: global=window,worker + +"use strict"; + +test(function() { + new Headers(); +}, "Create headers from no parameter"); + +test(function() { + new Headers(undefined); +}, "Create headers from undefined parameter"); + +test(function() { + new Headers({}); +}, "Create headers from empty object"); + +var parameters = [null, 1]; +parameters.forEach(function(parameter) { + test(function() { + assert_throws_js(TypeError, function() { new Headers(parameter) }); + }, "Create headers with " + parameter + " should throw"); +}); + +var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3", + "name4": null, + "name5": undefined, + "name6": 1, + "Content-Type": "value4" +}; + +var headerSeq = []; +for (var name in headerDict) + headerSeq.push([name, headerDict[name]]); + +test(function() { + var headers = new Headers(headerSeq); + for (name in headerDict) { + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } + assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary"); +}, "Create headers with sequence"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) { + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Create headers with record"); + +test(function() { + var headers = new Headers(headerDict); + var headers2 = new Headers(headers); + for (name in headerDict) { + assert_equals(headers2.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Create headers with existing headers"); + +test(function() { + var headers = new Headers() + headers[Symbol.iterator] = function *() { + yield ["test", "test"] + } + var headers2 = new Headers(headers) + assert_equals(headers2.get("test"), "test") +}, "Create headers with existing headers with custom iterator"); + +test(function() { + var headers = new Headers(); + for (name in headerDict) { + headers.append(name, headerDict[name]); + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Check append method"); + +test(function() { + var headers = new Headers(); + for (name in headerDict) { + headers.set(name, headerDict[name]); + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + } +}, "Check set method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) + assert_true(headers.has(name),"headers has name " + name); + + assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders"); +}, "Check has method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) { + assert_true(headers.has(name),"headers have a header: " + name); + headers.delete(name) + assert_true(!headers.has(name),"headers do not have anymore a header: " + name); + } +}, "Check delete method"); + +test(function() { + var headers = new Headers(headerDict); + for (name in headerDict) + assert_equals(headers.get(name), String(headerDict[name]), + "name: " + name + " has value: " + headerDict[name]); + + assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value"); +}, "Check get method"); + +var headerEntriesDict = {"name1": "value1", + "Name2": "value2", + "name": "value3", + "content-Type": "value4", + "Content-Typ": "value5", + "Content-Types": "value6" +}; +var sortedHeaderDict = {}; +var headerValues = []; +var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) { + sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value]; + headerValues.push(headerEntriesDict[value]); + return value.toLowerCase(); +}).sort(); + +var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())); +function checkIteratorProperties(iterator) { + var prototype = Object.getPrototypeOf(iterator); + assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype); + + var descriptor = Object.getOwnPropertyDescriptor(prototype, "next"); + assert_true(descriptor.configurable, "configurable"); + assert_true(descriptor.enumerable, "enumerable"); + assert_true(descriptor.writable, "writable"); +} + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.keys(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + const entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value, key); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (const key of headers.keys()) + assert_true(sortedHeaderKeys.indexOf(key) != -1); +}, "Check keys method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.values(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + const entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value, sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (const value of headers.values()) + assert_true(headerValues.indexOf(value) != -1); +}, "Check values method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers.entries(); + checkIteratorProperties(actual); + + sortedHeaderKeys.forEach(function(key) { + const entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value[0], key); + assert_equals(entry.value[1], sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); + + for (const entry of headers.entries()) + assert_equals(entry[1], sortedHeaderDict[entry[0]]); +}, "Check entries method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var actual = headers[Symbol.iterator](); + + sortedHeaderKeys.forEach(function(key) { + const entry = actual.next(); + assert_false(entry.done); + assert_equals(entry.value[0], key); + assert_equals(entry.value[1], sortedHeaderDict[key]); + }); + assert_true(actual.next().done); + assert_true(actual.next().done); +}, "Check Symbol.iterator method"); + +test(function() { + var headers = new Headers(headerEntriesDict); + var reference = sortedHeaderKeys[Symbol.iterator](); + headers.forEach(function(value, key, container) { + assert_equals(headers, container); + const entry = reference.next(); + assert_false(entry.done); + assert_equals(key, entry.value); + assert_equals(value, sortedHeaderDict[entry.value]); + }); + assert_true(reference.next().done); +}, "Check forEach method"); + +test(() => { + const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"}); + const actualKeys = []; + const actualValues = []; + for (const [header, value] of headers) { + actualKeys.push(header); + actualValues.push(value); + headers.delete("foo"); + } + assert_array_equals(actualKeys, ["bar", "baz"]); + assert_array_equals(actualValues, ["0", "1"]); +}, "Iteration skips elements removed while iterating"); + +test(() => { + const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); + const actualKeys = []; + const actualValues = []; + for (const [header, value] of headers) { + actualKeys.push(header); + actualValues.push(value); + if (header === "baz") + headers.delete("bar"); + } + assert_array_equals(actualKeys, ["bar", "baz", "quux"]); + assert_array_equals(actualValues, ["0", "1", "3"]); +}, "Removing elements already iterated over causes an element to be skipped during iteration"); + +test(() => { + const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); + const actualKeys = []; + const actualValues = []; + for (const [header, value] of headers) { + actualKeys.push(header); + actualValues.push(value); + if (header === "baz") + headers.append("X-yZ", "4"); + } + assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]); + assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]); +}, "Appending a value pair during iteration causes it to be reached during iteration"); + +test(() => { + const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); + const actualKeys = []; + const actualValues = []; + for (const [header, value] of headers) { + actualKeys.push(header); + actualValues.push(value); + if (header === "baz") + headers.append("abc", "-1"); + } + assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]); + assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]); +}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time"); diff --git a/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js b/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js new file mode 100644 index 0000000000..20b8a9d375 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js @@ -0,0 +1,54 @@ +// META: title=Headers case management +// META: global=window,worker + +"use strict"; + +var headerDictCase = {"UPPERCASE": "value1", + "lowercase": "value2", + "mixedCase": "value3", + "Content-TYPE": "value4" + }; + +function checkHeadersCase(originalName, headersToCheck, expectedDict) { + var lowCaseName = originalName.toLowerCase(); + var upCaseName = originalName.toUpperCase(); + var expectedValue = expectedDict[originalName]; + assert_equals(headersToCheck.get(originalName), expectedValue, + "name: " + originalName + " has value: " + expectedValue); + assert_equals(headersToCheck.get(lowCaseName), expectedValue, + "name: " + lowCaseName + " has value: " + expectedValue); + assert_equals(headersToCheck.get(upCaseName), expectedValue, + "name: " + upCaseName + " has value: " + expectedValue); +} + +test(function() { + var headers = new Headers(headerDictCase); + for (const name in headerDictCase) + checkHeadersCase(name, headers, headerDictCase) +}, "Create headers, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (const name in headerDictCase) { + headers.append(name, headerDictCase[name]); + checkHeadersCase(name, headers, headerDictCase); + } +}, "Check append method, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (const name in headerDictCase) { + headers.set(name, headerDictCase[name]); + checkHeadersCase(name, headers, headerDictCase); + } +}, "Check set method, names use characters with different case"); + +test(function() { + var headers = new Headers(); + for (const name in headerDictCase) + headers.set(name, headerDictCase[name]); + for (const name in headerDictCase) + headers.delete(name.toLowerCase()); + for (const name in headerDictCase) + assert_false(headers.has(name), "header " + name + " should have been deleted"); +}, "Check delete method, names use characters with different case"); diff --git a/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js b/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js new file mode 100644 index 0000000000..4f3b6d11df --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js @@ -0,0 +1,66 @@ +// META: title=Headers have combined (and sorted) values +// META: global=window,worker + +"use strict"; + +var headerSeqCombine = [["single", "singleValue"], + ["double", "doubleValue1"], + ["double", "doubleValue2"], + ["triple", "tripleValue1"], + ["triple", "tripleValue2"], + ["triple", "tripleValue3"] +]; +var expectedDict = {"single": "singleValue", + "double": "doubleValue1, doubleValue2", + "triple": "tripleValue1, tripleValue2, tripleValue3" +}; + +test(function() { + var headers = new Headers(headerSeqCombine); + for (const name in expectedDict) + assert_equals(headers.get(name), expectedDict[name]); +}, "Create headers using same name for different values"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (const name in expectedDict) { + assert_true(headers.has(name), "name: " + name + " has value(s)"); + headers.delete(name); + assert_false(headers.has(name), "name: " + name + " has no value(s) anymore"); + } +}, "Check delete and has methods when using same name for different values"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (const name in expectedDict) { + headers.set(name,"newSingleValue"); + assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue"); + } +}, "Check set methods when called with already used name"); + +test(function() { + var headers = new Headers(headerSeqCombine); + for (const name in expectedDict) { + var value = headers.get(name); + headers.append(name,"newSingleValue"); + assert_equals(headers.get(name), (value + ", " + "newSingleValue")); + } +}, "Check append methods when called with already used name"); + +test(() => { + const headers = new Headers([["1", "a"],["1", "b"]]); + for(let header of headers) { + assert_array_equals(header, ["1", "a, b"]); + } +}, "Iterate combined values"); + +test(() => { + const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]), + expected = [["1", "b"], ["2", "a, b"]]; + let i = 0; + for(let header of headers) { + assert_array_equals(header, expected[i]); + i++; + } + assert_equals(i, 2); +}, "Iterate combined values in sorted order") diff --git a/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js b/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js new file mode 100644 index 0000000000..82dadd8234 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js @@ -0,0 +1,96 @@ +// META: title=Headers errors +// META: global=window,worker + +"use strict"; + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["name"]]); }); +}, "Create headers giving an array having one string as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); }); +}, "Create headers giving an array having three strings as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); }); +}, "Create headers giving bad header name as init argument"); + +test(function() { + assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); }); +}, "Create headers giving bad header value as init argument"); + +var badNames = ["invalidĀ", {}]; +var badValues = ["invalidĀ"]; + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.get(name); }); + }, "Check headers get with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.delete(name); }); + }, "Check headers delete with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.has(name); }); + }, "Check headers has with an invalid name " + name); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.set(name, "Value1"); }); + }, "Check headers set with an invalid name " + name); +}); + +badValues.forEach(function(value) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.set("name", value); }); + }, "Check headers set with an invalid value " + value); +}); + +badNames.forEach(function(name) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); }); + }, "Check headers append with an invalid name " + name); +}); + +badValues.forEach(function(value) { + test(function() { + var headers = new Headers(); + assert_throws_js(TypeError, function() { headers.append("name", value); }); + }, "Check headers append with an invalid value " + value); +}); + +test(function() { + var headers = new Headers([["name", "value"]]); + assert_throws_js(TypeError, function() { headers.forEach(); }); + assert_throws_js(TypeError, function() { headers.forEach(undefined); }); + assert_throws_js(TypeError, function() { headers.forEach(1); }); +}, "Headers forEach throws if argument is not callable"); + +test(function() { + var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]); + var counter = 0; + try { + headers.forEach(function(value, name) { + counter++; + if (name == "name2") + throw "error"; + }); + } catch (e) { + assert_equals(counter, 2); + assert_equals(e, "error"); + return; + } + assert_unreached(); +}, "Headers forEach loop should stop if callback is throwing exception"); diff --git a/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js b/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js new file mode 100644 index 0000000000..60dbb9ef67 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker + +"use strict"; + +promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +const longValue = "s".repeat(127); + +[ + { + "headers": ["accept", "accept-language", "content-language"], + "values": [longValue, "", longValue] + }, + { + "headers": ["accept", "accept-language", "content-language"], + "values": ["", longValue] + }, + { + "headers": ["content-type"], + "values": ["text/plain;" + "s".repeat(116), "text/plain"] + } +].forEach(testItem => { + testItem.headers.forEach(header => { + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + testItem.values.forEach((value) => { + noCorsHeaders.append(header, value); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '1'); + }); + noCorsHeaders.set(header, testItem.values.join(", ")); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '2'); + noCorsHeaders.delete(header); + assert_false(noCorsHeaders.has(header)); + }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", ")); + }); +}); + +function runTests(testArray) { + testArray = testArray.concat([ + ["dpr", "2"], + ["rtt", "1.0"], + ["downlink", "-1.0"], + ["ect", "6g"], + ["save-data", "on"], + ["viewport-width", "100"], + ["width", "100"], + ["unknown", "doesitmatter"] + ]); + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + noCorsHeaders.append(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + noCorsHeaders.set(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header"); + }); +} diff --git a/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js b/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js new file mode 100644 index 0000000000..68cf5b85f3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js @@ -0,0 +1,56 @@ +// META: title=Headers normalize values +// META: global=window,worker + +"use strict"; + +const expectations = { + "name1": [" space ", "space"], + "name2": ["\ttab\t", "tab"], + "name3": [" spaceAndTab\t", "spaceAndTab"], + "name4": ["\r\n newLine", "newLine"], //obs-fold cases + "name5": ["newLine\r\n ", "newLine"], + "name6": ["\r\n\tnewLine", "newLine"], + "name7": ["\t\f\tnewLine\n", "\f\tnewLine"], + "name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space +}; + +test(function () { + const headerDict = Object.fromEntries( + Object.entries(expectations).map(([name, [actual]]) => [name, actual]), + ); + var headers = new Headers(headerDict); + for (const name in expectations) { + const expected = expectations[name][1]; + assert_equals( + headers.get(name), + expected, + "name: " + name + " has normalized value: " + expected, + ); + } +}, "Create headers with not normalized values"); + +test(function () { + var headers = new Headers(); + for (const name in expectations) { + headers.append(name, expectations[name][0]); + const expected = expectations[name][1]; + assert_equals( + headers.get(name), + expected, + "name: " + name + " has value: " + expected, + ); + } +}, "Check append method with not normalized values"); + +test(function () { + var headers = new Headers(); + for (const name in expectations) { + headers.set(name, expectations[name][0]); + const expected = expectations[name][1]; + assert_equals( + headers.get(name), + expected, + "name: " + name + " has value: " + expected, + ); + } +}, "Check set method with not normalized values"); diff --git a/testing/web-platform/tests/fetch/api/headers/headers-record.any.js b/testing/web-platform/tests/fetch/api/headers/headers-record.any.js new file mode 100644 index 0000000000..fa853914f4 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-record.any.js @@ -0,0 +1,357 @@ +// META: global=window,worker + +"use strict"; + +var log = []; +function clearLog() { + log = []; +} +function addLogEntry(name, args) { + log.push([ name, ...args ]); +} + +var loggingHandler = { +}; + +setup(function() { + for (let prop of Object.getOwnPropertyNames(Reflect)) { + loggingHandler[prop] = function(...args) { + addLogEntry(prop, args); + return Reflect[prop](...args); + } + } +}); + +test(function() { + var h = new Headers(); + assert_equals([...h].length, 0); +}, "Passing nothing to Headers constructor"); + +test(function() { + var h = new Headers(undefined); + assert_equals([...h].length, 0); +}, "Passing undefined to Headers constructor"); + +test(function() { + assert_throws_js(TypeError, function() { + var h = new Headers(null); + }); +}, "Passing null to Headers constructor"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b" }; + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["a"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); +}, "Basic operation with one property"); + +test(function() { + this.add_cleanup(clearLog); + var recordProto = { c: "d" }; + var record = Object.create(recordProto, { a: { value: "b", enumerable: true } }); + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["a"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); +}, "Basic operation with one property and a proto"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b", c: "d" }; + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[5], ["get", record, "c", proxy]); + + // Check the results. + assert_equals([...h].length, 2); + assert_array_equals([...h.keys()], ["a", "c"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with two properties"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "b", "\uFFFF": "d" }; + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, function() { + var h = new Headers(proxy); + }); + + assert_equals(log.length, 5); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]); + // The second [[Get]] never happens, because we convert the invalid name to a + // ByteString first and throw. +}, "Correct operation ordering with two properties one of which has an invalid name"); + +test(function() { + this.add_cleanup(clearLog); + var record = { a: "\uFFFF", c: "d" } + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, function() { + var h = new Headers(proxy); + }); + + assert_equals(log.length, 4); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Nothing else after this, because converting the result of that [[Get]] to a + // ByteString throws. +}, "Correct operation ordering with two properties one of which has an invalid value"); + +test(function() { + this.add_cleanup(clearLog); + var record = {}; + Object.defineProperty(record, "a", { value: "b", enumerable: false }); + Object.defineProperty(record, "c", { value: "d", enumerable: true }); + Object.defineProperty(record, "e", { value: "f", enumerable: false }); + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // No [[Get]] because not enumerable + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[4], ["get", record, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]); + // No [[Get]] because not enumerable + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["c"]); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with non-enumerable properties"); + +test(function() { + this.add_cleanup(clearLog); + var record = {a: "b", c: "d", e: "f"}; + var lyingHandler = { + getOwnPropertyDescriptor: function(target, name) { + if (name == "a" || name == "e") { + return undefined; + } + return Reflect.getOwnPropertyDescriptor(target, name); + } + }; + var lyingProxy = new Proxy(record, lyingHandler); + var proxy = new Proxy(lyingProxy, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 6); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", lyingProxy]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]); + // No [[Get]] because no descriptor + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]); + // No [[Get]] because no descriptor + + // Check the results. + assert_equals([...h].length, 1); + assert_array_equals([...h.keys()], ["c"]); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Correct operation ordering with undefined descriptors"); + +test(function() { + this.add_cleanup(clearLog); + var record = {a: "b", c: "d"}; + var lyingHandler = { + ownKeys: function() { + return [ "a", "c", "a", "c" ]; + }, + }; + var lyingProxy = new Proxy(record, lyingHandler); + var proxy = new Proxy(lyingProxy, loggingHandler); + + // Returning duplicate keys from ownKeys() throws a TypeError. + assert_throws_js(TypeError, + function() { var h = new Headers(proxy); }); + + assert_equals(log.length, 2); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", lyingProxy]); +}, "Correct operation ordering with repeated keys"); + +test(function() { + this.add_cleanup(clearLog); + var record = { + a: "b", + [Symbol.toStringTag]: { + // Make sure the ToString conversion of the value happens + // after the ToString conversion of the key. + toString: function () { addLogEntry("toString", [this]); return "nope"; } + }, + c: "d" }; + var proxy = new Proxy(record, loggingHandler); + assert_throws_js(TypeError, + function() { var h = new Headers(proxy); }); + + assert_equals(log.length, 7); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[5], ["get", record, "c", proxy]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[6], ["getOwnPropertyDescriptor", record, + Symbol.toStringTag]); + // Then we throw an exception converting the Symbol to a string, before we do + // the third [[Get]]. +}, "Basic operation with Symbol keys"); + +test(function() { + this.add_cleanup(clearLog); + var record = { + a: { + toString: function() { addLogEntry("toString", [this]); return "b"; } + }, + [Symbol.toStringTag]: { + toString: function () { addLogEntry("toString", [this]); return "nope"; } + }, + c: { + toString: function() { addLogEntry("toString", [this]); return "d"; } + } + }; + // Now make that Symbol-named property not enumerable. + Object.defineProperty(record, Symbol.toStringTag, { enumerable: false }); + assert_array_equals(Reflect.ownKeys(record), + ["a", "c", Symbol.toStringTag]); + + var proxy = new Proxy(record, loggingHandler); + var h = new Headers(proxy); + + assert_equals(log.length, 9); + // The first thing is the [[Get]] of Symbol.iterator to figure out whether + // we're a sequence, during overload resolution. + assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); + // Then we have the [[OwnPropertyKeys]] from + // https://webidl.spec.whatwg.org/#es-to-record step 4. + assert_array_equals(log[1], ["ownKeys", record]); + // Then the [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); + // Then the [[Get]] from step 5.2. + assert_array_equals(log[3], ["get", record, "a", proxy]); + // Then the ToString on the value. + assert_array_equals(log[4], ["toString", record.a]); + // Then the second [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]); + // Then the second [[Get]] from step 5.2. + assert_array_equals(log[6], ["get", record, "c", proxy]); + // Then the ToString on the value. + assert_array_equals(log[7], ["toString", record.c]); + // Then the third [[GetOwnProperty]] from step 5.1. + assert_array_equals(log[8], ["getOwnPropertyDescriptor", record, + Symbol.toStringTag]); + // No [[Get]] because not enumerable. + + // Check the results. + assert_equals([...h].length, 2); + assert_array_equals([...h.keys()], ["a", "c"]); + assert_true(h.has("a")); + assert_equals(h.get("a"), "b"); + assert_true(h.has("c")); + assert_equals(h.get("c"), "d"); +}, "Operation with non-enumerable Symbol keys"); diff --git a/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js b/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js new file mode 100644 index 0000000000..d826bcab2a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js @@ -0,0 +1,20 @@ +// META: title=Headers basic +// META: global=window,worker + +"use strict"; + +var headers = new Headers(); +var methods = ["append", + "delete", + "get", + "has", + "set", + //Headers is iterable + "entries", + "keys", + "values" + ]; +for (var idx in methods) + test(function() { + assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method"); + }, "Headers has " + methods[idx] + " method"); diff --git a/testing/web-platform/tests/fetch/api/idlharness.any.js b/testing/web-platform/tests/fetch/api/idlharness.any.js new file mode 100644 index 0000000000..7b3c694e16 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/idlharness.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +idl_test( + ['fetch'], + ['referrer-policy', 'html', 'dom'], + idl_array => { + idl_array.add_objects({ + Headers: ["new Headers()"], + Request: ["new Request('about:blank')"], + Response: ["new Response()"], + }); + if (self.GLOBAL.isWindow()) { + idl_array.add_objects({ Window: ['window'] }); + } else if (self.GLOBAL.isWorker()) { + idl_array.add_objects({ WorkerGlobalScope: ['self'] }); + } + } +); diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html b/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html new file mode 100644 index 0000000000..e8660dffa9 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: blocked by CSP</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + fetch_tests_from_worker(new Worker("csp-blocked.js")); + </script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.html b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html new file mode 100644 index 0000000000..99e90dfcd8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch: blocked by CSP</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script src="csp-blocked.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers new file mode 100644 index 0000000000..c8c1e9ffbd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none';
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.js b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js new file mode 100644 index 0000000000..28653fff85 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js @@ -0,0 +1,13 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +//Content-Security-Policy: connect-src 'none'; cf .headers file +cspViolationUrl = RESOURCES_DIR + "top.txt"; + +promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(cspViolationUrl)); +}, "Fetch is blocked by CSP, got a TypeError"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers new file mode 100644 index 0000000000..c8c1e9ffbd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none';
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/nested-policy.js b/testing/web-platform/tests/fetch/api/policies/nested-policy.js new file mode 100644 index 0000000000..b0d17696c3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/nested-policy.js @@ -0,0 +1 @@ +// empty, but referrer-policy set on this file diff --git a/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers b/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers new file mode 100644 index 0000000000..7ffbf17d6b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html new file mode 100644 index 0000000000..af898aa29f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in service worker: referrer with no-referrer policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + </head> + <body> + <script> + service_worker_test("referrer-no-referrer.js"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html new file mode 100644 index 0000000000..dbef9bb658 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer with no-referrer policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + fetch_tests_from_worker(new Worker("referrer-no-referrer.js")); + </script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html new file mode 100644 index 0000000000..22a6f34c52 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch: referrer with no-referrer policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script src="referrer-no-referrer.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers new file mode 100644 index 0000000000..7ffbf17d6b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js new file mode 100644 index 0000000000..60600bf081 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js @@ -0,0 +1,19 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + var referrer = resp.headers.get("x-request-referer"); + //Either no referrer header is sent or it is empty + if (referrer) + assert_equals(referrer, "", "request's referrer is empty"); + }); +}, "Request's referrer is empty"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers new file mode 100644 index 0000000000..7ffbf17d6b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html new file mode 100644 index 0000000000..4018b83781 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in service worker: referrer with no-referrer policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + </head> + <body> + <script> + service_worker_test("referrer-origin.js?pipe=sub"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html new file mode 100644 index 0000000000..d87192e227 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in service worker: referrer with origin-when-cross-origin policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + </head> + <body> + <script> + service_worker_test("referrer-origin-when-cross-origin.js?pipe=sub"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html new file mode 100644 index 0000000000..f95ae8cf08 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer with origin-when-cross-origin policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + fetch_tests_from_worker(new Worker("referrer-origin-when-cross-origin.js?pipe=sub")); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html new file mode 100644 index 0000000000..5cd79e4b53 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch: referrer with origin-when-cross-origin policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script src="referrer-origin-when-cross-origin.js?pipe=sub"></script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers new file mode 100644 index 0000000000..ad768e6329 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js new file mode 100644 index 0000000000..0adadbc550 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + importScripts("/common/get-host-info.sub.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = location.origin + '/'; +var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers new file mode 100644 index 0000000000..ad768e6329 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html new file mode 100644 index 0000000000..bb80dd54fb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer with origin policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + fetch_tests_from_worker(new Worker("referrer-origin.js?pipe=sub")); + </script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html new file mode 100644 index 0000000000..b164afe01d --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch: referrer with origin policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script src="referrer-origin.js?pipe=sub"></script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers new file mode 100644 index 0000000000..5b29739bbd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.js b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js new file mode 100644 index 0000000000..918f8f207c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js @@ -0,0 +1,30 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = (new URL("/", location.href)).href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +promise_test(function(test) { + var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/"; + return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Cross-origin referrer is overridden by client origin"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers new file mode 100644 index 0000000000..5b29739bbd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html new file mode 100644 index 0000000000..634877edae --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer with unsafe-url policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + </head> + <body> + <script> + service_worker_test("referrer-unsafe-url.js"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html new file mode 100644 index 0000000000..42045776b1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch in worker: referrer with unsafe-url policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + fetch_tests_from_worker(new Worker("referrer-unsafe-url.js")); + </script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html new file mode 100644 index 0000000000..10dd79e3d3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Fetch: referrer with unsafe-url policy</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch"> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script src="referrer-unsafe-url.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers new file mode 100644 index 0000000000..8e23770bd6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js new file mode 100644 index 0000000000..4d61172613 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerUrl = location.href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl); + }); +}, "Request's referrer is the full url of current document/worker"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers new file mode 100644 index 0000000000..8e23770bd6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js new file mode 100644 index 0000000000..74d731f242 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js @@ -0,0 +1,38 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const BASE = location.href; +const IS_HTTPS = new URL(BASE).protocol === 'https:'; +const REMOTE_HOST = get_host_info()['REMOTE_HOST']; +const REMOTE_PORT = + IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT']; + +const REMOTE_ORIGIN = + new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin; +const DESTINATION = new URL('../resources/cors-top.txt', BASE); + +function CreateURL(url, BASE, params) { + const u = new URL(url, BASE); + for (const {name, value} of params) { + u.searchParams.append(name, value); + } + return u; +} + +const redirect = + CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN, + [{name: 'redirect_status', value: 303}, + {name: 'location', value: DESTINATION.href}]); + +promise_test(async (test) => { + const res = await fetch(redirect.href, {mode: 'no-cors'}); + // This is discussed at https://github.com/whatwg/fetch/issues/737. + assert_equals(res.type, 'opaque'); +}, 'original => remote => original with mode: "no-cors"'); + +promise_test(async (test) => { + const res = await fetch(redirect.href, {mode: 'cors'}); + assert_equals(res.type, 'cors'); +}, 'original => remote => original with mode: "cors"'); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js new file mode 100644 index 0000000000..420f9c0dfc --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js @@ -0,0 +1,51 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: timeout=long + +/** + * Fetches a target that returns response with HTTP status code `statusCode` to + * redirect `maxCount` times. + */ +function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) { + const desc = `Redirect ${statusCode} ${maxCount} times`; + + const fromUrl = `${RESOURCES_DIR}redirect.py`; + const toUrl = fromUrl; + const token1 = token(); + const url = `${fromUrl}?token=${token1}` + + `&max_age=0` + + `&redirect_status=${statusCode}` + + `&max_count=${maxCount}` + + `&location=${encodeURIComponent(toUrl)}`; + + const requestInit = {'redirect': 'follow'}; + + promise_test((test) => { + return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) + .then((resp) => { + assert_equals( + resp.status, 200, 'Clean stash response\'s status is 200'); + + if (!shouldPass) + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + + return fetch(url, requestInit) + .then((resp) => { + assert_equals(resp.status, 200, 'Response\'s status is 200'); + return resp.text(); + }) + .then((body) => { + assert_equals( + body, maxCount.toString(), `Redirected ${maxCount} times`); + }); + }); + }, desc); +} + +for (const statusCode of [301, 302, 303, 307, 308]) { + redirectCountTest(20, {statusCode}); + redirectCountTest(21, {statusCode, shouldPass: false}); +} + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js new file mode 100644 index 0000000000..487f4d42e9 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// Tests receiving a redirect response with a Location header with an empty +// value. + +const url = RESOURCES_DIR + 'redirect-empty-location.py'; + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'})); +}, 'redirect response with empty Location, follow mode'); + +promise_test(t => { + return fetch(url, {redirect:'manual'}) + .then(resp => { + assert_equals(resp.type, 'opaqueredirect'); + assert_equals(resp.status, 0); + }); +}, 'redirect response with empty Location, manual mode'); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js new file mode 100644 index 0000000000..c9ac13f3db --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js @@ -0,0 +1,35 @@ +// META: global=window +// META: timeout=long +// META: title=Fetch API: keepalive handling +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +} = get_host_info(); + + +keepaliveRedirectInUnloadTest('same-origin redirect'); +keepaliveRedirectInUnloadTest( + 'same-origin redirect + preflight', {withPreflight: true}); +keepaliveRedirectInUnloadTest('cross-origin redirect', { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +}); +keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + withPreflight: true +}); +keepaliveRedirectInUnloadTest( + 'redirect to file URL', + {url2: 'file://tmp/bar.txt', expectFetchSucceed: false}); +keepaliveRedirectInUnloadTest('redirect to data URL', { + url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5', + expectFetchSucceed: false +}); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js new file mode 100644 index 0000000000..54e4bc31fa --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js @@ -0,0 +1,18 @@ +// META: global=window +// META: title=Fetch API: keepalive handling +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +keepaliveRedirectTest(`mixed content redirect`, { + origin1: HTTPS_NOTSAMESITE_ORIGIN, + origin2: HTTP_NOTSAMESITE_ORIGIN, + expectFetchSucceed: false +}); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js new file mode 100644 index 0000000000..779ad70579 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by +// this test. As of writing, the Fetch spec has not been updated to cover these. + +// redirectLocation tests that a Location header of |locationHeader| is resolved +// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted +// as a byte sequence via isomorphic encode, as described in [INFRA]. This +// allows the caller to specify byte sequences which are not valid UTF-8. +// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its +// UTF-8 encoding, not "\u2603". +// +// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode +function redirectLocation( + desc, redirectUrl, locationHeader, expectedUrlSuffix) { + promise_test(function(test) { + // Note we use escape() instead of encodeURIComponent(), so that characters + // are escaped as bytes in the isomorphic encoding. + var url = redirectUrl + '?simple=1&location=' + escape(locationHeader); + + return fetch(url, {'redirect': 'follow'}).then(function(resp) { + assert_true( + resp.url.endsWith(expectedUrlSuffix), + resp.url + ' ends with ' + expectedUrlSuffix); + }); + }, desc); +} + +var redirUrl = RESOURCES_DIR + 'redirect.py'; +redirectLocation( + 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83', + 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83', + 'top.txt?%E2%98%83'); +redirectLocation( + 'Redirect to escaped and unescaped UTF-8', redirUrl, + 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83', + 'top.txt?%%E2%98%83'); +redirectLocation( + 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF'); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js new file mode 100644 index 0000000000..3d483bdcd4 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js @@ -0,0 +1,73 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +const VALID_URL = 'top.txt'; +const INVALID_URL = 'invalidurl:'; +const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5'; + +/** + * A test to fetch a URL that returns response redirecting to `toUrl` with + * `status` as its HTTP status code. `expectStatus` can be set to test the + * status code in fetch's Promise response. + */ +function redirectLocationTest(toUrlDesc, { + toUrl = undefined, + status, + expectStatus = undefined, + mode, + shouldPass = true +} = {}) { + toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`; + const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`; + const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` + + (toUrl ? `&location=${encodeURIComponent(toUrl)}` : ''); + const requestInit = {'redirect': mode}; + if (!expectStatus) + expectStatus = status; + + promise_test((test) => { + if (mode === 'error' || !shouldPass) + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + if (mode === 'manual') + return fetch(url, requestInit).then((resp) => { + assert_equals(resp.status, 0, "Response's status is 0"); + assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); + assert_equals(resp.statusText, '', `Response's statusText is ""`); + assert_true(resp.headers.entries().next().done, "Headers should be empty"); + }); + + if (mode === 'follow') + return fetch(url, requestInit).then((resp) => { + assert_equals( + resp.status, expectStatus, `Response's status is ${expectStatus}`); + }); + assert_unreached(`${mode} is not a valid redirect mode`); + }, desc); +} + +// FIXME: We may want to mix redirect-mode and cors-mode. +for (const status of [301, 302, 303, 307, 308]) { + redirectLocationTest('without location', {status, mode: 'follow'}); + redirectLocationTest('without location', {status, mode: 'manual'}); + // FIXME: Add tests for "error" redirect-mode without location. + + // When succeeded, `follow` mode should have followed all redirects. + redirectLocationTest( + 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'}); + redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'}); + redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'}); + + redirectLocationTest( + 'invalid', + {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false}); + redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'}); + redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'}); + + redirectLocationTest( + 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false}); + // FIXME: Should this pass? + redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'}); + redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'}); +} + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js new file mode 100644 index 0000000000..9fe086a9db --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js @@ -0,0 +1,112 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// Creates a promise_test that fetches a URL that returns a redirect response. +// +// |opts| has additional options: +// |opts.body|: the request body as a string or blob (default is empty body) +// |opts.expectedBodyAsString|: the expected response body as a string. The +// server is expected to echo the request body. The default is the empty string +// if the request after redirection isn't POST; otherwise it's |opts.body|. +// |opts.expectedRequestContentType|: the expected Content-Type of redirected +// request. +function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) { + let url = redirectUrl; + let urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + let requestHeaders = { + "Content-Encoding": "Identity", + "Content-Language": "en-US", + "Content-Location": "foo", + }; + let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders}; + opts = opts || {}; + if (opts.body) { + requestInit.body = opts.body; + } + + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + let expectedRequestContentType = "NO"; + if (opts.expectedRequestContentType) { + expectedRequestContentType = opts.expectedRequestContentType; + } + + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.type, "basic", "Response's type basic"); + assert_equals( + resp.headers.get("x-request-method"), + expectedMethod, + "Request method after redirection is " + expectedMethod); + let hasRequestBodyHeader = true; + if (opts.expectedStripRequestBodyHeader) { + hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader; + } + assert_equals( + resp.headers.get("x-request-content-type"), + expectedRequestContentType, + "Request Content-Type after redirection is " + expectedRequestContentType); + [ + "Content-Encoding", + "Content-Language", + "Content-Location" + ].forEach(header => { + let xHeader = "x-request-" + header.toLowerCase(); + let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO"; + assert_equals( + resp.headers.get(xHeader), + expectedValue, + "Request " + header + " after redirection is " + expectedValue); + }); + assert_true(resp.redirected); + return resp.text().then(function(text) { + let expectedBody = ""; + if (expectedMethod == "POST") { + expectedBody = opts.expectedBodyAsString || requestInit.body; + } + let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO"; + assert_equals(text, expectedBody, "request body"); + assert_equals( + resp.headers.get("x-request-content-length"), + expectedContentLength, + "Request Content-Length after redirection is " + expectedContentLength); + }); + }); + }, desc); +} + +promise_test(function(test) { + assert_false(new Response().redirected); + return fetch(RESOURCES_DIR + "method.py").then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_false(resp.redirected); + }); +}, "Response.redirected should be false on not-redirected responses"); + +var redirUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = "method.py"; + +const stringBody = "this is my body"; +const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]); +const blobBodyAsString = "it's me the blob! and more blob!"; + +redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET"); +redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD"); + +redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET"); +redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD"); + +redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET"); +redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); +redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD"); +redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true }); + +redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET"); +redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"}); +redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString }); +redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js new file mode 100644 index 0000000000..9f1ff98c65 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js @@ -0,0 +1,59 @@ +// META: script=/common/get-host-info.sub.js + +var redirectLocation = "cors-top.txt"; +const { ORIGIN, REMOTE_ORIGIN } = get_host_info(); + +function testRedirect(origin, redirectStatus, redirectMode, corsMode) { + var url = new URL("../resources/redirect.py", self.location); + if (origin === "cross-origin") { + url.host = get_host_info().REMOTE_HOST; + url.port = get_host_info().HTTP_PORT; + } + + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {redirect: redirectMode, mode: corsMode}; + + promise_test(function(test) { + if (redirectMode === "error" || + (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin")) + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + if (redirectMode === "manual") + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 0, "Response's status is 0"); + assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); + assert_equals(resp.statusText, "", "Response's statusText is \"\""); + assert_equals(resp.url, url + urlParameters, "Response URL should be the original one"); + }); + if (redirectMode === "follow") + return fetch(url + urlParameters, requestInit).then(function(resp) { + if (corsMode !== "no-cors" || origin === "same-origin") { + assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one"); + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.type, "opaque", "Response is opaque"); + } + }); + assert_unreached(redirectMode + " is no a valid redirect mode"); + }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode"); +} + +for (var origin of ["same-origin", "cross-origin"]) { + for (var statusCode of [301, 302, 303, 307, 308]) { + for (var redirect of ["error", "manual", "follow"]) { + for (var mode of ["cors", "no-cors"]) + testRedirect(origin, statusCode, redirect, mode); + } + } +} + +promise_test(async (t) => { + const destination = `${ORIGIN}/common/blank.html`; + // We use /common/redirect.py intentionally, as we want a CORS error. + const url = + `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`; + await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" })); +}, "manual redirect with a CORS error should be rejected"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js new file mode 100644 index 0000000000..6001c509b1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js @@ -0,0 +1,68 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +const { + HTTP_ORIGIN, + HTTP_REMOTE_ORIGIN, +} = get_host_info(); + +/** + * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to + * redirect to `toUrl`. + */ +function testOriginAfterRedirection( + desc, method, fromUrl, toUrl, statusCode, expectedOrigin) { + desc = `[${method}] Redirect ${statusCode} ${desc}`; + const token1 = token(); + const url = `${fromUrl}?token=${token1}&max_age=0` + + `&redirect_status=${statusCode}` + + `&location=${encodeURIComponent(toUrl)}`; + + const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'}; + + promise_test(function(test) { + return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) + .then((cleanResponse) => { + assert_equals( + cleanResponse.status, 200, + `Clean stash response's status is 200`); + return fetch(url, requestInit).then((redirectResponse) => { + assert_equals( + redirectResponse.status, 200, + `Inspect header response's status is 200`); + assert_equals( + redirectResponse.headers.get('x-request-origin'), + expectedOrigin, 'Check origin header'); + }); + }); + }, desc); +} + +const FROM_URL = `${RESOURCES_DIR}redirect.py`; +const CORS_FROM_URL = + `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`; +const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${ + RESOURCES_DIR}inspect-headers.py?headers=origin`; +const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${ + RESOURCES_DIR}inspect-headers.py?cors&headers=origin`; + +for (const statusCode of [301, 302, 303, 307, 308]) { + for (const method of ['GET', 'POST']) { + testOriginAfterRedirection( + 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode, + null); + testOriginAfterRedirection( + 'Same origin to other origin', method, FROM_URL, CORS_TO_URL, + statusCode, HTTP_ORIGIN); + testOriginAfterRedirection( + 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL, + statusCode, HTTP_ORIGIN); + // TODO(crbug.com/1432059): Fix broken tests. + testOriginAfterRedirection( + 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`, + statusCode, 'null'); + } +} + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js new file mode 100644 index 0000000000..56e55d79e1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js @@ -0,0 +1,104 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function getExpectation(expectations, init, initScenario, redirectPolicy, redirectScenario) { + let policies = [ + expectations[initPolicy][initScenario], + expectations[redirectPolicy][redirectScenario] + ]; + + if (policies.includes("omitted")) { + return null; + } else if (policies.includes("origin")) { + return referrerOrigin; + } else { + // "stripped-referrer" + return referrerUrl; + } +} + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header "; + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(response) { + assert_equals(response.status, 200, "Inspect header response's status is 200"); + assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, description); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +var expectations = { + "no-referrer": { + "same-origin": "omitted", + "cross-origin": "omitted" + }, + "no-referrer-when-downgrade": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + }, + "origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin", + }, + "same-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "omitted" + }, + "strict-origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "strict-origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin" + }, + "unsafe-url": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + } +}; + +for (var initPolicy in expectations) { + for (var redirectPolicy in expectations) { + + // Redirect to same-origin URL + testReferrerAfterRedirection( + "Same origin redirection", + redirectUrl, + locationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin")); + + // Redirect to cross-origin URL + testReferrerAfterRedirection( + "Cross origin redirection", + redirectUrl, + crossLocationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin")); + } +} + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js new file mode 100644 index 0000000000..99fda42e69 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js @@ -0,0 +1,66 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + + promise_test(function(test) { + return fetch(url + urlParameters, requestInit).then(function(response) { + assert_equals(response.status, 200, "Inspect header response's status is 200"); + assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, desc); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl); + +testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl); + +testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin); + +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js new file mode 100644 index 0000000000..31ec124fd6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js @@ -0,0 +1,19 @@ +// META: title=Fetch: handling different schemes in redirects +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +// All non-HTTP(S) schemes cannot survive redirects +var url = "../resources/redirect.py?location="; +var tests = [ + url + "mailto:a@a.com", + url + "data:,HI", + url + "facetime:a@a.org", + url + "about:blank", + url + "about:unicorn", + url + "blob:djfksfjs" +]; +tests.forEach(function(url) { + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url)) + }) +}) diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js new file mode 100644 index 0000000000..9d0f147349 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js @@ -0,0 +1,28 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5"; +var body = "response's body"; +var contentType = "text/plain"; + +function redirectDataURL(desc, redirectUrl, mode) { + var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL); + + var requestInit = {"mode": mode}; + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + }, desc); +} + +var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py"; +var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py"; + +redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors"); +redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors"); +redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin"); + +redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors"); +redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors"); + +done(); diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js new file mode 100644 index 0000000000..521bd3adc2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js @@ -0,0 +1,33 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const redirectUrl = RESOURCES_DIR + "redirect.h2.py"; +const redirectLocation = "top.txt"; + +async function fetchStreamRedirect(statusCode) { + const url = RESOURCES_DIR + "redirect.h2.py" + + `?redirect_status=${statusCode}&location=${redirectLocation}`; + const requestInit = {method: "POST"}; + requestInit["body"] = new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}); + requestInit.duplex = "half"; + return fetch(url, requestInit); +} + +promise_test(async () => { + const resp = await fetchStreamRedirect(303); + assert_equals(resp.status, 200); + assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), + "Response's url should be the redirected one"); +}, "Fetch upload streaming should be accepted on 303"); + +for (const statusCode of [301, 302, 307, 308]) { + promise_test(t => { + return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode)); + }, `Fetch upload streaming should fail on ${statusCode}`); +} diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html new file mode 100644 index 0000000000..f3f9f7856d --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Fetch destination tests for resources with no load event</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; +const kScope = 'resources/dummy.html?dest=frame'; + +// Set up the service worker and the frame. +promise_test(t => { + const kScript = 'resources/fetch-destination-worker-frame.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }); + }, 'Initialize global state'); + +var waitOnMessageFromSW = async t => { + await new Promise((resolve, reject) => { + navigator.serviceWorker.onmessage = t.step_func(event => { + if (event.data == "PASS") { + resolve(); + } else { + reject(); + } + }); + }).catch(() => {; + assert_unreached("Wrong destination."); + }); + t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; }); +} + +// Document destination +/////////////////////// +promise_test(async t => { + var f = document.createElement('frame'); + frame = f; + f.className = 'test-frame'; + f.src = kScope; + document.body.appendChild(f); + await waitOnMessageFromSW(t); + add_completion_callback(() => { f.remove(); }); +}, 'frame fetches with a "frame" Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html new file mode 100644 index 0000000000..1aa5a5613b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Fetch destination tests for resources with no load event</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; +const kScope = 'resources/dummy.html?dest=iframe'; + +// Set up the service worker and the frame. +promise_test(t => { + const kScript = 'resources/fetch-destination-worker-iframe.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }); + }, 'Initialize global state'); + +var waitOnMessageFromSW = async t => { + await new Promise((resolve, reject) => { + navigator.serviceWorker.onmessage = t.step_func(event => { + if (event.data == "PASS") { + resolve(); + } else { + reject(); + } + }); + }).catch(() => {; + assert_unreached("Wrong destination."); + }); + t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; }); +} + +// Document destination +/////////////////////// +promise_test(async t => { + var f = document.createElement('iframe'); + frame = f; + f.className = 'test-iframe'; + f.src = kScope; + document.body.appendChild(f); + await waitOnMessageFromSW(t); + add_completion_callback(() => { f.remove(); }); +}, 'iframe fetches with a "iframe" Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html new file mode 100644 index 0000000000..1778bf2581 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<title>Fetch destination tests for resources with no load event</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; + +// Set up the service worker and the frame. +promise_test(t => { + const kScope = 'resources/'; + const kFrame = 'resources/empty.https.html'; + const kScript = 'resources/fetch-destination-worker-no-load-event.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kFrame); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'Initialize global state'); + +var waitOnMessageFromSW = async t => { + await new Promise((resolve, reject) => { + frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => { + if (event.data == "PASS") { + resolve(); + } else { + reject(); + } + }); + }).catch(() => {; + assert_unreached("Wrong destination."); + }); + t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; }); +} +// Actual tests + +// Image destination +//////////////////// + +// CSS background image - image destination +promise_test(async t => { + let node = frame.contentWindow.document.createElement("div"); + node.style = "background-image: url(dummy.png?t=bg2&dest=image)"; + frame.contentWindow.document.body.appendChild(node); + + await waitOnMessageFromSW(t); +}, 'Background image fetches with an "image" Request.destination'); + +// Font destination +/////////////////// + +// Font loading API - font destination +promise_test(async t => { + let font = new frame.contentWindow.FontFace("foo", "url(dummy.ttf?t=api&dest=font)"); + font.load(); + + await waitOnMessageFromSW(t); +}, 'Font loading API fetches with an "font" Request.destination'); + +// CSS font - font destination +promise_test(async t => { + let style = frame.contentWindow.document.createElement("style"); + style.innerHTML = "@font-face { font-family: foo; src: url(dummy.ttf?t=css&dest=font); }"; + style.innerHTML += "div {font-family: foo; }"; + let div = frame.contentWindow.document.createElement("div"); + div.innerHTML = "bar"; + frame.contentWindow.document.body.appendChild(style); + frame.contentWindow.document.body.appendChild(div); + + await waitOnMessageFromSW(t); +}, 'CSS font fetches with an "font" Request.destination'); + +// Empty string destination +/////////////////////////// + +// sendBeacon() - empty string destination +promise_test(async t => { + frame.contentWindow.navigator.sendBeacon("dummy?t=beacon&dest=", "foobar"); + + await waitOnMessageFromSW(t); +}, 'sendBeacon() fetches with an empty string Request.destination'); + +// Cache.add() - empty string destination +promise_test(async t => { + frame.contentWindow.caches.open("foo").then(cache => { + cache.add("dummy?t=cache&dest="); + }); + + await waitOnMessageFromSW(t); +}, 'Cache.add() fetches with an empty string Request.destination'); + +// script destination +///////////////////// + +// importScripts() - script destination +promise_test(async t => { + let worker = new frame.contentWindow.Worker("importer.js"); + + await waitOnMessageFromSW(t); +}, 'importScripts() fetches with a "script" Request.destination'); + +// style destination +///////////////////// +// @import - style destination +promise_test(async t => { + let node = frame.contentWindow.document.createElement("style"); + node.innerHTML = '@import url("dummy?t=import&dest=style")'; + frame.contentWindow.document.body.appendChild(node); + + await waitOnMessageFromSW(t); +}, '@import fetches with a "style" Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html new file mode 100644 index 0000000000..db99202df8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<title>Fetch destination test for prefetching</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/media.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; + +// Set up the service worker and the frame. +promise_test(t => { + const kScope = 'resources/empty.https.html'; + const kScript = 'resources/fetch-destination-worker.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'Initialize global state'); + +// HTMLLinkElement with rel=prefetch - empty string destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "prefetch"; + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest="; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=prefetch fetches with an empty string Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html new file mode 100644 index 0000000000..5935c1ff31 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<title>Fetch destination tests for resources with no load event</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; + +// Set up the service worker and the frame. +promise_test(t => { + const kScope = 'resources/dummy.html'; + const kScript = 'resources/fetch-destination-worker-no-load-event.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'Initialize global state'); + +var waitOnMessageFromSW = async t => { + await new Promise((resolve, reject) => { + frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => { + if (event.data == "PASS") { + resolve(); + } else { + reject(); + } + }); + }).catch(() => {; + assert_unreached("Wrong destination."); + }); + t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; }); +} + +// worker destination +///////////////////// +promise_test(async t => { + // We can use an html file as we don't really care about the dedicated worker successfully loading. + let worker = new frame.contentWindow.Worker("dummy.html?t=worker&dest=worker"); + await waitOnMessageFromSW(t); +}, 'DedicatedWorker fetches with a "worker" Request.destination'); + +promise_test(async t => { + // We can use an html file as we don't really care about the shared worker successfully loading. + let worker = new frame.contentWindow.SharedWorker("dummy.html?t=sharedworker&dest=sharedworker"); + await waitOnMessageFromSW(t); +}, 'SharedWorker fetches with a "sharedworker" Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html new file mode 100644 index 0000000000..1b6cf16914 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html @@ -0,0 +1,485 @@ +<!DOCTYPE html> +<title>Fetch destination tests</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/media.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +let frame; + +// Set up the service worker and the frame. +promise_test(t => { + const kScope = 'resources/empty.https.html'; + const kScript = 'resources/fetch-destination-worker.js'; + return service_worker_unregister_and_register(t, kScript, kScope) + .then(registration => { + add_completion_callback(() => { + registration.unregister(); + }); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => { + return with_iframe(kScope); + }) + .then(f => { + frame = f; + add_completion_callback(() => { f.remove(); }); + }); + }, 'Initialize global state'); + +// Actual tests + +// Image destination +//////////////////// + +// HTMLImageElement - image destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("img"); + node.onload = resolve; + node.onerror = reject; + node.src = "dummy.png?dest=image"; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLImageElement fetches with an "image" Request.destination'); + +// HTMLImageElement with srcset attribute - image destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("img"); + node.onload = resolve; + node.onerror = reject; + node.srcset = "dummy.png?t=srcset&dest=image"; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLImageElement with srcset attribute fetches with an "image" Request.destination'); + +// HTMLImageElement with srcset attribute - image destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let img = frame.contentWindow.document.createElement("img"); + let picture = frame.contentWindow.document.createElement("picture"); + let source = frame.contentWindow.document.createElement("source"); + picture.appendChild(source); + picture.appendChild(img); + img.onload = resolve; + img.onerror = reject; + source.srcset = "dummy.png?t=picture&dest=image"; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLImageElement with a HTMLPictureElement parent attribute fetches with an "image" Request.destination'); + +// SVGImageElement - image destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let svg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','svg'); + svg.setAttributeNS('http://www.w3.org/2000/svg','xlink','http://www.w3.org/1999/xlink'); + let svgimg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','image'); + svgimg.onload = resolve; + svgimg.onerror = reject; + svgimg.setAttributeNS('http://www.w3.org/1999/xlink','href','dummy.png?t=svg&dest=image'); + svg.appendChild(svgimg); + frame.contentWindow.document.documentElement.appendChild(svg); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'SVGImageElement fetches with an "image" Request.destination'); + +// Empty string destination +/////////////////////////// + +// fetch() - empty string destination +promise_test(async t => { + let response = await frame.contentWindow.fetch("dummy?dest="); + assert_true(response.ok); +}, 'fetch() fetches with an empty string Request.destination'); + +// XMLHttpRequest - empty string destination +promise_test(async t => { + let xhr; + await new Promise((resolve, reject) => { + xhr = new frame.contentWindow.XMLHttpRequest(); + xhr.onload = resolve; + xhr.onerror = reject; + xhr.open("GET", "dummy?t=xhr&dest="); + xhr.send(); + }).catch(() => { + assert_unreached("Fetch errored."); + }); + assert_equals(xhr.status, 200); +}, 'XMLHttpRequest() fetches with an empty string Request.destination'); + +// EventSource - empty string destination +promise_test(async t => { + let xhr; + await new Promise((resolve, reject) => { + eventSource = new frame.contentWindow.EventSource("dummy.es?t=eventsource&dest="); + eventSource.onopen = resolve; + eventSource.onerror = reject; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'EventSource() fetches with an empty string Request.destination'); + +// HTMLAudioElement - audio destination +/////////////////////////////////////// +promise_test(async t => { + await new Promise((resolve, reject) => { + let audioURL = getAudioURI("dummy_audio"); + let node = frame.contentWindow.document.createElement("audio"); + node.onloadeddata = resolve; + node.onerror = reject; + node.src = audioURL + "?dest=audio"; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLAudioElement fetches with an "audio" Request.destination'); + +// HTMLVideoElement - video destination +/////////////////////////////////////// +promise_test(async t => { + await new Promise((resolve, reject) => { + let videoURL = getVideoURI("dummy_video"); + let node = frame.contentWindow.document.createElement("video"); + node.onloadeddata = resolve; + node.onerror = reject; + node.src = videoURL + "?dest=video"; + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLVideoElement fetches with a "video" Request.destination'); + +// script destinations +////////////////////// + +// HTMLScriptElement - script destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("script"); + node.onload = resolve; + node.onerror = reject; + node.src = "dummy?dest=script"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLScriptElement fetches with a "script" Request.destination'); + +// audioworklet destination +////////////////////// +promise_test(async t => { + let audioContext = new frame.contentWindow.AudioContext(); + await audioContext.audioWorklet.addModule("dummy?dest=audioworklet"); +}, 'AudioWorklet module fetches with a "audioworklet" Request.destination'); + +// Style destination +//////////////////// + +// HTMLLinkElement with rel=stylesheet - style destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "stylesheet"; + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=style"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=stylesheet fetches with a "style" Request.destination'); + +// Import declaration with `type: "css"` - style destination +promise_test(t => { + return new Promise((resolve, reject) => { + frame.contentWindow.onerror = reject; + + let node = frame.contentWindow.document.createElement("script"); + node.onload = resolve; + node.onerror = reject; + node.src = "import-declaration-type-css.js"; + node.type = "module"; + frame.contentWindow.document.body.appendChild(node); + }).then(() => { + frame.contentWindow.onerror = null; + }); +}, 'Import declaration with `type: "css"` fetches with a "style" Request.destination'); + +// JSON destination +/////////////////// + +// Import declaration with `type: "json"` - json destination +promise_test(t => { + return new Promise((resolve, reject) => { + frame.contentWindow.onerror = reject; + let node = frame.contentWindow.document.createElement("script"); + node.onload = resolve; + node.onerror = reject; + node.src = "import-declaration-type-json.js"; + node.type = "module"; + frame.contentWindow.document.body.appendChild(node); + }).then(() => { + frame.contentWindow.onerror = null; + }); +}, 'Import declaration with `type: "json"` fetches with a "json" Request.destination'); + +// Preload tests +//////////////// +// HTMLLinkElement with rel=preload and as=fetch - empty string destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "fetch"; + if (node.as != "fetch") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?t=2&dest="; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=fetch fetches with an empty string Request.destination'); + +// HTMLLinkElement with rel=preload and as=style - style destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "style"; + if (node.as != "style") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?t=2&dest=style"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=style fetches with a "style" Request.destination'); + +// HTMLLinkElement with rel=preload and as=json - json destination +promise_test(t => { + return new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "json"; + if (node.as != "json") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy.json?t=2&dest=json"; + frame.contentWindow.document.body.appendChild(node); + }); +}, 'HTMLLinkElement with rel=preload and as=json fetches with a "json" Request.destination'); + +// HTMLLinkElement with rel=preload and as=script - script destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "script"; + if (node.as != "script") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?t=2&dest=script"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=script fetches with a "script" Request.destination'); + +// HTMLLinkElement with rel=preload and as=font - font destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "font"; + if (node.as != "font") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?t=2&dest=font"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=font fetches with a "font" Request.destination'); + +// HTMLLinkElement with rel=preload and as=image - image destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "image"; + if (node.as != "image") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy.png?t=2&dest=image"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=image fetches with a "image" Request.destination'); + +// HTMLLinkElement with rel=preload and as=audio - audio destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let audioURL = getAudioURI("dummy_audio"); + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "audio"; + if (node.as != "audio") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = audioURL + "?dest=audio"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=audio fetches with a "audio" Request.destination'); + +// HTMLLinkElement with rel=preload and as=video - video destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let videoURL = getVideoURI("dummy_video"); + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "video"; + if (node.as != "video") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = videoURL + "?dest=video"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=video fetches with a "video" Request.destination'); + +// HTMLLinkElement with rel=preload and as=track - track destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "track"; + if (node.as != "track") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=track"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=track fetches with a "track" Request.destination'); + +// HTMLLinkElement with rel=preload and as=document - document destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "document"; + if (node.as != "document") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=document"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=document fetches with a "document" Request.destination'); + +// HTMLLinkElement with rel=preload and as=worker - worker destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "worker"; + if (node.as != "worker") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=worker"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=worker fetches with a "worker" Request.destination'); + +// HTMLLinkElement with rel=preload and as=sharedworker - sharedworker destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "sharedworker"; + if (node.as != "sharedworker") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=sharedworker"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=sharedworker fetches with a "sharedworker" Request.destination'); + +// HTMLLinkElement with rel=preload and as=xslt - xslt destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "xslt"; + if (node.as != "xslt") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=xslt"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=xslt fetches with a "xslt" Request.destination'); + +// HTMLLinkElement with rel=preload and as=manifest - manifest destination +promise_test(async t => { + await new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "manifest"; + if (node.as != "manifest") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy?dest=manifest"; + frame.contentWindow.document.body.appendChild(node); + }).catch(() => { + assert_unreached("Fetch errored."); + }); +}, 'HTMLLinkElement with rel=preload and as=manifest fetches with a "manifest" Request.destination'); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers new file mode 100644 index 0000000000..9bb8badcad --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers @@ -0,0 +1 @@ +Content-Type: text/event-stream diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json @@ -0,0 +1 @@ +{} diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png Binary files differnew file mode 100644 index 0000000000..01c9666a8d --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf Binary files differnew file mode 100644 index 0000000000..9023592ef5 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3 b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3 Binary files differnew file mode 100644 index 0000000000..0091330f1e --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3 diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga Binary files differnew file mode 100644 index 0000000000..239ad2bd08 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4 b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4 Binary files differnew file mode 100644 index 0000000000..7022e75c15 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4 diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv Binary files differnew file mode 100644 index 0000000000..de99616ece --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm Binary files differnew file mode 100644 index 0000000000..c3d433a3e0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html b/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js new file mode 100644 index 0000000000..b69de0b7df --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + event.waitUntil(async function() { + let destination = new URL(event.request.url).searchParams.get("dest"); + let clients = await self.clients.matchAll({"includeUncontrolled": true}); + clients.forEach(function(client) { + if (client.url.includes("fetch-destination-frame")) { + if (event.request.destination == destination) { + client.postMessage("PASS"); + } else { + client.postMessage("FAIL"); + } + } + }) + }()); + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js new file mode 100644 index 0000000000..76345839ea --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + event.waitUntil(async function() { + let destination = new URL(event.request.url).searchParams.get("dest"); + let clients = await self.clients.matchAll({"includeUncontrolled": true}); + clients.forEach(function(client) { + if (client.url.includes("fetch-destination-iframe")) { + if (event.request.destination == destination) { + client.postMessage("PASS"); + } else { + client.postMessage("FAIL"); + } + } + }) + }()); + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js new file mode 100644 index 0000000000..a583b1272a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + const url = event.request.url; + if (url.includes('dummy') && url.includes('?')) { + event.waitUntil(async function() { + let destination = new URL(url).searchParams.get("dest"); + var result = "FAIL"; + if (event.request.destination == destination || + (event.request.destination == "empty" && destination == "")) { + result = "PASS"; + } + let cl = await clients.matchAll({includeUncontrolled: true}); + for (i = 0; i < cl.length; i++) { + cl[i].postMessage(result); + } + }()) + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js new file mode 100644 index 0000000000..904009c172 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + let destination = new URL(event.request.url).searchParams.get("dest"); + if (event.request.destination == destination || + (event.request.destination == "empty" && destination == "")) { + event.respondWith(fetch(event.request)); + } else { + event.respondWith(Response.error()); + } + } +}); + diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js new file mode 100644 index 0000000000..3c8cf1f44b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js @@ -0,0 +1 @@ +import "./dummy.css?dest=style" with { type: "css" }; diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js new file mode 100644 index 0000000000..b2d964dd82 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js @@ -0,0 +1 @@ +import "./dummy.json?dest=json" with { type: "json" }; diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js b/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js new file mode 100644 index 0000000000..9568474d50 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js @@ -0,0 +1 @@ +importScripts("dummy?t=importScripts&dest=script"); diff --git a/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js b/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js new file mode 100644 index 0000000000..eb13f37f0b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js @@ -0,0 +1,13 @@ +// META: global=window,worker + +// https://fetch.spec.whatwg.org/#forbidden-method +for (const method of [ + 'CONNECT', 'TRACE', 'TRACK', + 'connect', 'trace', 'track' + ]) { + test(function() { + assert_throws_js(TypeError, + function() { new Request('./', {method: method}); } + ); + }, 'Request() with a forbidden method ' + method + ' must throw.'); +} diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js new file mode 100644 index 0000000000..b0d6ba5b80 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js @@ -0,0 +1,11 @@ +// This is a regression test for Chromium issue https://crbug.com/1427266. +test(() => { + const iframe = document.createElement('iframe'); + document.body.append(iframe); + const otherRequest = iframe.contentWindow.Request; + iframe.remove(); + const r1 = new otherRequest('resource', { method: 'POST', body: 'string' }); + const r2 = new otherRequest(r1); + assert_true(r1.bodyUsed); + assert_false(r2.bodyUsed); +}, 'creating a request from another request in a detached realm should work'); diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html b/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html new file mode 100644 index 0000000000..9bb6e0bbf3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>Current page used as a test helper</title> +<base href="success/"> diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html new file mode 100644 index 0000000000..a885b8a0a7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<title>Incumbent page used as a test helper</title> + +<iframe src="../current/current.html" id="c"></iframe> + +<script> +'use strict'; + +window.createRequest = (...args) => { + const current = document.querySelector('#c').contentWindow; + return new current.Request(...args); +}; + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html b/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html new file mode 100644 index 0000000000..df60e72507 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Request constructor URL parsing, with multiple globals in play</title> +<link rel="help" href="https://fetch.spec.whatwg.org/#dom-request"> +<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- This is the entry global --> + +<iframe src="incumbent/incumbent.html"></iframe> + +<script> +'use strict'; + +const loadPromise = new Promise(resolve => { + window.addEventListener("load", () => resolve()); +}); + +promise_test(() => { + return loadPromise.then(() => { + const req = document.querySelector('iframe').contentWindow.createRequest("url"); + + assert_equals(req.url, new URL("current/success/url", location.href).href); + }); +}, "should parse the URL relative to the current settings object"); + +</script> diff --git a/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js new file mode 100644 index 0000000000..b0684d4be0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js @@ -0,0 +1,92 @@ +// META: global=window,worker + +// list of bad ports according to +// https://fetch.spec.whatwg.org/#port-blocking +var BLOCKED_PORTS_LIST = [ + 1, // tcpmux + 7, // echo + 9, // discard + 11, // systat + 13, // daytime + 15, // netstat + 17, // qotd + 19, // chargen + 20, // ftp-data + 21, // ftp + 22, // ssh + 23, // telnet + 25, // smtp + 37, // time + 42, // name + 43, // nicname + 53, // domain + 69, // tftp + 77, // priv-rjs + 79, // finger + 87, // ttylink + 95, // supdup + 101, // hostriame + 102, // iso-tsap + 103, // gppitnp + 104, // acr-nema + 109, // pop2 + 110, // pop3 + 111, // sunrpc + 113, // auth + 115, // sftp + 117, // uucp-path + 119, // nntp + 123, // ntp + 135, // loc-srv / epmap + 137, // netbios-ns + 139, // netbios-ssn + 143, // imap2 + 161, // snmp + 179, // bgp + 389, // ldap + 427, // afp (alternate) + 465, // smtp (alternate) + 512, // print / exec + 513, // login + 514, // shell + 515, // printer + 526, // tempo + 530, // courier + 531, // chat + 532, // netnews + 540, // uucp + 548, // afp + 554, // rtsp + 556, // remotefs + 563, // nntp+ssl + 587, // smtp (outgoing) + 601, // syslog-conn + 636, // ldap+ssl + 989, // ftps-data + 990, // ftps + 993, // ldap+ssl + 995, // pop3+ssl + 1719, // h323gatestat + 1720, // h323hostcall + 1723, // pptp + 2049, // nfs + 3659, // apple-sasl + 4045, // lockd + 5060, // sip + 5061, // sips + 6000, // x11 + 6566, // sane-port + 6665, // irc (alternate) + 6666, // irc (alternate) + 6667, // irc (default) + 6668, // irc (alternate) + 6669, // irc (alternate) + 6697, // irc+tls + 10080, // amanda +]; + +BLOCKED_PORTS_LIST.map(function(a){ + promise_test(function(t){ + return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a)) + }, 'Request on bad port ' + a + ' should throw TypeError.'); +}); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js new file mode 100644 index 0000000000..c5b2001cc8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js @@ -0,0 +1,170 @@ +// META: global=window,worker +// META: title=Request cache - default with conditional requests +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js new file mode 100644 index 0000000000..dfa8369c9a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js @@ -0,0 +1,39 @@ +// META: global=window,worker +// META: title=Request cache - default +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses', + state: "stale", + request_cache: ["default", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists', + state: "fresh", + request_cache: ["default", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "stale", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "fresh", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js new file mode 100644 index 0000000000..00dce096c7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js @@ -0,0 +1,67 @@ +// META: global=window,worker +// META: title=Request cache - force-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses', + state: "stale", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "stale", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "fresh", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "stale", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "fresh", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "stale", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "fresh", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js new file mode 100644 index 0000000000..41fc22baf2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js @@ -0,0 +1,25 @@ +// META: global=window,worker +// META: title=Request cache : no-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache', + state: "stale", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, + { + name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache', + state: "fresh", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js new file mode 100644 index 0000000000..9a28718bf2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: title=Request cache - no store +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "stale", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "fresh", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js new file mode 100644 index 0000000000..1305787c7c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js @@ -0,0 +1,66 @@ +// META: global=window,dedicatedworker,sharedworker +// META: title=Request cache - only-if-cached +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +// FIXME: avoid mixed content requests to enable service worker global +var tests = [ + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses', + state: "stale", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found', + state: "fresh", + request_cache: ["only-if-cached"], + response: ["error"], + expected_validation_headers: [], + expected_no_cache_headers: [] + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js new file mode 100644 index 0000000000..c7bfffb398 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js @@ -0,0 +1,51 @@ +// META: global=window,worker +// META: title=Request cache - reload +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "stale", + request_cache: ["reload", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "fresh", + request_cache: ["reload", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "stale", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false, true], + expected_no_cache_headers: [false, true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "fresh", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/api/request/request-cache.js b/testing/web-platform/tests/fetch/api/request/request-cache.js new file mode 100644 index 0000000000..f2fbecf496 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-cache.js @@ -0,0 +1,223 @@ +/** + * Each test is run twice: once using etag/If-None-Match and once with + * date/If-Modified-Since. Each test run gets its own URL and randomized + * content and operates independently. + * + * The test steps are run with request_cache.length fetch requests issued + * and their immediate results sanity-checked. The cache.py server script + * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma, + * and Cache-Control observed headers for each request it receives. When + * the test fetches have run, this state is retrieved from cache.py and the + * expected_* lists are checked, including their length. + * + * This means that if a request_* fetch is expected to hit the cache and not + * touch the network, then there will be no entry for it in the expect_* + * lists. AKA (request_cache.length - expected_validation_headers.length) + * should equal the number of cache hits that didn't touch the network. + * + * Test dictionary keys: + * - state: required string that determines whether the Expires response for + * the fetched document should be set in the future ("fresh") or past + * ("stale"). + * - vary: optional string to be passed to the server for it to quote back + * in a Vary header on the response to us. + * - cache_control: optional string to be passed to the server for it to + * quote back in a Cache-Control header on the response to us. + * - redirect: optional string "same-origin" or "cross-origin". If + * provided, the server will issue an absolute redirect to the script on + * the same or a different origin, as appropriate. The redirected + * location is the script with the redirect parameter removed, so the + * content/state/etc. will be as if you hadn't specified a redirect. + * - request_cache: required array of cache modes to use (via `cache`). + * - request_headers: optional array of explicit fetch `headers` arguments. + * If provided, the server will log an empty dictionary for each request + * instead of the request headers it would normally log. + * - response: optional array of specialized response handling. Right now, + * "error" array entries indicate a network error response is expected + * which will reject with a TypeError. + * - expected_validation_headers: required boolean array indicating whether + * the server should have seen an If-None-Match/If-Modified-Since header + * in the request. + * - expected_no_cache_headers: required boolean array indicating whether + * the server should have seen Pragma/Cache-control:no-cache headers in + * the request. + * - expected_max_age_headers: optional boolean array indicating whether + * the server should have seen a Cache-Control:max-age=0 header in the + * request. + */ + +var now = new Date(); + +function base_path() { + return location.pathname.replace(/\/[^\/]*$/, '/'); +} +function make_url(uuid, id, value, content, info) { + var dates = { + fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(), + stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(), + }; + var vary = ""; + if ("vary" in info) { + vary = "&vary=" + info.vary; + } + var cache_control = ""; + if ("cache_control" in info) { + cache_control = "&cache_control=" + info.cache_control; + } + var redirect = ""; + + var ignore_request_headers = ""; + if ("request_headers" in info) { + // Ignore the request headers that we send since they may be synthesized by the test. + ignore_request_headers = "&ignore"; + } + var url_sans_redirect = "resources/cache.py?token=" + uuid + + "&content=" + content + + "&" + id + "=" + value + + "&expires=" + dates[info.state] + + vary + cache_control + ignore_request_headers; + // If there's a redirect, the target is the script without any redirect at + // either the same domain or a different domain. + if ("redirect" in info) { + var host_info = get_host_info(); + var origin; + switch (info.redirect) { + case "same-origin": + origin = host_info['HTTP_ORIGIN']; + break; + case "cross-origin": + origin = host_info['HTTP_REMOTE_ORIGIN']; + break; + } + var redirected_url = origin + base_path() + url_sans_redirect; + return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url); + } else { + return url_sans_redirect; + } +} +function expected_status(type, identifier, init) { + if (type == "date" && + init.headers && + init.headers["If-Modified-Since"] == identifier) { + // The server will respond with a 304 in this case. + return [304, "Not Modified"]; + } + return [200, "OK"]; +} +function expected_response_text(type, identifier, init, content) { + if (type == "date" && + init.headers && + init.headers["If-Modified-Since"] == identifier) { + // The server will respond with a 304 in this case. + return ""; + } + return content; +} +function server_state(uuid) { + return fetch("resources/cache.py?querystate&token=" + uuid) + .then(function(response) { + return response.text(); + }).then(function(text) { + // null will be returned if the server never received any requests + // for the given uuid. Normalize that to an empty list consistent + // with our representation. + return JSON.parse(text) || []; + }); +} +function make_test(type, info) { + return function(test) { + var uuid = token(); + var identifier = (type == "tag" ? Math.random() : now.toGMTString()); + var content = Math.random().toString(); + var url = make_url(uuid, type, identifier, content, info); + var fetch_functions = []; + for (var i = 0; i < info.request_cache.length; ++i) { + fetch_functions.push(function(idx) { + var init = {cache: info.request_cache[idx]}; + if ("request_headers" in info) { + init.headers = info.request_headers[idx]; + } + if (init.cache === "only-if-cached") { + // only-if-cached requires we use same-origin mode. + init.mode = "same-origin"; + } + return fetch(url, init) + .then(function(response) { + if ("response" in info && info.response[idx] === "error") { + assert_true(false, "fetch should have been an error"); + return; + } + assert_array_equals([response.status, response.statusText], + expected_status(type, identifier, init)); + return response.text(); + }).then(function(text) { + assert_equals(text, expected_response_text(type, identifier, init, content)); + }, function(reason) { + if ("response" in info && info.response[idx] === "error") { + assert_throws_js(TypeError, function() { throw reason; }); + } else { + throw reason; + } + }); + }); + } + var i = 0; + function run_next_step() { + if (fetch_functions.length) { + return fetch_functions.shift()(i++) + .then(run_next_step); + } else { + return Promise.resolve(); + } + } + return run_next_step() + .then(function() { + // Now, query the server state + return server_state(uuid); + }).then(function(state) { + var expectedState = []; + info.expected_validation_headers.forEach(function (validate) { + if (validate) { + if (type == "tag") { + expectedState.push({"If-None-Match": '"' + identifier + '"'}); + } else { + expectedState.push({"If-Modified-Since": identifier}); + } + } else { + expectedState.push({}); + } + }); + for (var i = 0; i < info.expected_no_cache_headers.length; ++i) { + if (info.expected_no_cache_headers[i]) { + expectedState[i]["Pragma"] = "no-cache"; + expectedState[i]["Cache-Control"] = "no-cache"; + } + } + if ("expected_max_age_headers" in info) { + for (var i = 0; i < info.expected_max_age_headers.length; ++i) { + if (info.expected_max_age_headers[i]) { + expectedState[i]["Cache-Control"] = "max-age=0"; + } + } + } + assert_equals(state.length, expectedState.length); + for (var i = 0; i < state.length; ++i) { + for (var header in state[i]) { + assert_equals(state[i][header], expectedState[i][header]); + delete expectedState[i][header]; + } + for (var header in expectedState[i]) { + assert_false(header in state[i]); + } + } + }); + }; +} + +function run_tests(tests) +{ + tests.forEach(function(info) { + promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response"); + promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response"); + }); +} diff --git a/testing/web-platform/tests/fetch/api/request/request-clone.sub.html b/testing/web-platform/tests/fetch/api/request/request-clone.sub.html new file mode 100644 index 0000000000..c690bb3dc0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-clone.sub.html @@ -0,0 +1,63 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Request clone</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#request"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/utils.js"></script> + </head> + <body> + <script> + var headers = new Headers({"name" : "value"}); + var emptyHeaders = new Headers(); + + var initValuesDict = {"method" : "POST", + "referrer" : "http://{{host}}:{{ports[http][0]}}/", + "referrerPolicy" : "origin", + "mode" : "same-origin", + "credentials" : "include", + "cache" : "no-cache", + "redirect" : "error", + "integrity" : "Request's Integrity", + "headers" : headers, + "body" : "Request's body" + }; + + var expectedInitialized = {"method" : "POST", + "referrer" : "http://{{host}}:{{ports[http][0]}}/", + "referrerPolicy" : "origin", + "mode" : "same-origin", + "credentials" : "include", + "cache" : "no-cache", + "redirect" : "error", + "integrity" : "Request's Integrity", + "headers" : headers, + "body" : "Request's body" + }; + + test(function() { + var RequestInitialized = new Request("", initValuesDict); + var requestToCheck = RequestInitialized.clone(); + checkRequest(requestToCheck, expectedInitialized); + }, "Check cloning a request"); + + test(function() { + var initialRequest = new Request("", {"headers" : new Headers({"a": "1", "b" : "2"})}); + var request = initialRequest.clone(); + assert_equals(request.headers.get("a"), "1", "cloned request should have header 'a'"); + assert_equals(request.headers.get("b"), "2", "cloned request should have header 'b'"); + + initialRequest.headers.delete("a"); + assert_equals(request.headers.get("a"), "1", "cloned request should still have header 'a'"); + + request.headers.delete("a"); + assert_equals(initialRequest.headers.get("b"), "2", "initial request should have header 'b'"); + + }, "Check cloning a request copies the headers"); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js b/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js new file mode 100644 index 0000000000..27bb991871 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js @@ -0,0 +1,21 @@ +promise_test(async function () { + const req1 = new Request("https://example.com/", { + body: "req1", + method: "POST", + }); + + const text1 = await req1.text(); + assert_equals( + text1, + "req1", + "The body of the first request should be 'req1'." + ); + + const req2 = new Request(req1, { body: "req2" }); + const bodyText = await req2.text(); + assert_equals( + bodyText, + "req2", + "The body of the second request should be overridden to 'req2'." + ); +}, "Check that the body of a new request can be overridden when created from an existing Request object"); diff --git a/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js b/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js new file mode 100644 index 0000000000..034a86041a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js @@ -0,0 +1,101 @@ +// META: global=window,worker +// META: title=Request consume empty bodies + +function checkBodyText(test, request) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyBlob(test, request) { + return request.blob().then(function(bodyAsBlob) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); + }); +} + +function checkBodyArrayBuffer(test, request) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyJSON(test, request) { + return request.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormData(test, request) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormDataError(test, request) { + return promise_rejects_js(test, TypeError, request.formData()).then(function() { + assert_false(request.bodyUsed); + }); +} + +function checkRequestWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "headers": headers}); + assert_false(request.bodyUsed); + return checkFunction(test, request); + }, "Consume request's body as " + bodyType); +} + +checkRequestWithNoBody("text", checkBodyText); +checkRequestWithNoBody("blob", checkBodyBlob); +checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkRequestWithNoBody("json (error case)", checkBodyJSON); +checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkRequestWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body}); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return request.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer")); +} + +// FIXME: Add BufferSource, FormData and URLSearchParams. +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkRequestWithEmptyBody("text", "", false); +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkRequestWithEmptyBody("text", "", true); +checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950. +checkRequestWithEmptyBody("FormData", new FormData(), true); +checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/testing/web-platform/tests/fetch/api/request/request-consume.any.js b/testing/web-platform/tests/fetch/api/request/request-consume.any.js new file mode 100644 index 0000000000..aff5d65244 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-consume.any.js @@ -0,0 +1,145 @@ +// META: global=window,worker +// META: title=Request consume +// META: script=../resources/utils.js + +function checkBodyText(request, expectedBody) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as text: bodyUsed turned true"); + }); +} + +function checkBodyBlob(request, expectedBody, checkContentType) { + return request.blob().then(function(bodyAsBlob) { + if (checkContentType) + assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type"); + + var promise = new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as blob: bodyUsed turned true"); + }); + }); +} + +function checkBodyArrayBuffer(request, expectedBody) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); + }); +} + +function checkBodyJSON(request, expectedBody) { + return request.json().then(function(bodyAsJSON) { + var strBody = JSON.stringify(bodyAsJSON) + assert_equals(strBody, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as json: bodyUsed turned true"); + }); +} + +function checkBodyFormData(request, expectedBody) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_true(request.bodyUsed, "body as formData: bodyUsed turned true"); + }); +} + +function checkRequestBody(body, expected, bodyType) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyText(request, expected); + }, "Consume " + bodyType + " request's body as text"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyBlob(request, expected); + }, "Consume " + bodyType + " request's body as blob"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyArrayBuffer(request, expected); + }, "Consume " + bodyType + " request's body as arrayBuffer"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyJSON(request, expected); + }, "Consume " + bodyType + " request's body as JSON"); +} + +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); + +checkRequestBody(textData, textData, "String"); + +var string = "\"123456\""; +function getArrayBuffer() { + var arrayBuffer = new ArrayBuffer(8); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr] = string.charCodeAt(cptr); + return arrayBuffer; +} + +function getArrayBufferWithZeros() { + var arrayBuffer = new ArrayBuffer(10); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr + 1] = string.charCodeAt(cptr); + return arrayBuffer; +} + +checkRequestBody(getArrayBuffer(), string, "ArrayBuffer"); +checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array"); +checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array"); +checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array"); +checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView"); + +promise_test(function(test) { + var formData = new FormData(); + formData.append("name", "value") + var request = new Request("", {"method": "POST", "body": formData }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyFormData(request, formData); +}, "Consume FormData request's body as FormData"); + +function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) { + promise_test(function(test) { + var response = new Response(blobBody); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + return checkFunction(response, blobData); + }, "Consume blob response's body as " + bodyType); +} + +checkBlobResponseBody(blob, textData, "blob", checkBodyBlob); +checkBlobResponseBody(blob, textData, "text", checkBodyText); +checkBlobResponseBody(blob, textData, "json", checkBodyJSON); +checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer); +checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob); + +var goodJSONValues = ["null", "1", "true", "\"string\""]; +goodJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return request.json().then(function(v) { + assert_equals(v, JSON.parse(value)); + }); + }, "Consume JSON from text: '" + JSON.stringify(value) + "'"); +}); + +var badJSONValues = ["undefined", "{", "a", "["]; +badJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return promise_rejects_js(test, SyntaxError, request.json()); + }, "Trying to consume bad JSON text as JSON: '" + value + "'"); +}); diff --git a/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js b/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js new file mode 100644 index 0000000000..8a11de78ff --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js @@ -0,0 +1,109 @@ +// META: global=window,worker +// META: title=Request disturbed +// META: script=../resources/utils.js + +var initValuesDict = {"method" : "POST", + "body" : "Request's body" +}; + +var noBodyConsumed = new Request(""); +var bodyConsumed = new Request("", initValuesDict); + +test(() => { + assert_equals(noBodyConsumed.body, null, "body's default value is null"); + assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); + assert_not_equals(bodyConsumed.body, null, "non-null body"); + assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type"); + assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed"); +}, "Request's body: initial state"); + +noBodyConsumed.blob(); +bodyConsumed.blob(); + +test(function() { + assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); + try { + noBodyConsumed.clone(); + } catch (e) { + assert_unreached("Can use request not disturbed for creating or cloning request"); + } +}, "Request without body cannot be disturbed"); + +test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_throws_js(TypeError, function() { bodyConsumed.clone(); }); +}, "Check cloning a disturbed request"); + +test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_throws_js(TypeError, function() { new Request(bodyConsumed); }); +}, "Check creating a new request from a disturbed request"); + +promise_test(function() { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + const originalBody = bodyConsumed.body; + const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" }); + assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new"); + assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed"); + return bodyReplaced.text().then(text => { + assert_equals(text, "Replaced body"); + }); +}, "Check creating a new request with a new body from a disturbed request"); + +promise_test(function() { + var bodyRequest = new Request("", initValuesDict); + const originalBody = bodyRequest.body; + assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); + var requestFromRequest = new Request(bodyRequest); + assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_equals(bodyRequest.body, originalBody, "body should not change"); + assert_not_equals(originalBody, undefined, "body should not be undefined"); + assert_not_equals(originalBody, null, "body should not be null"); + assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); + return requestFromRequest.text().then(text => { + assert_equals(text, "Request's body"); + }); +}, "Input request used for creating new request became disturbed"); + +promise_test(() => { + const bodyRequest = new Request("", initValuesDict); + const originalBody = bodyRequest.body; + assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); + const requestFromRequest = new Request(bodyRequest, { body : "init body" }); + assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); + assert_equals(bodyRequest.body, originalBody, "body should not change"); + assert_not_equals(originalBody, undefined, "body should not be undefined"); + assert_not_equals(originalBody, null, "body should not be null"); + assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); + + return requestFromRequest.text().then(text => { + assert_equals(text, "init body"); + }); +}, "Input request used for creating new request became disturbed even if body is not used"); + +promise_test(function(test) { + assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); + return promise_rejects_js(test, TypeError, bodyConsumed.blob()); +}, "Check consuming a disturbed request"); + +test(function() { + var req = new Request(URL, {method: 'POST', body: 'hello'}); + assert_false(req.bodyUsed, + 'Request should not be flagged as used if it has not been ' + + 'consumed.'); + assert_throws_js(TypeError, + function() { new Request(req, {method: 'GET'}); }, + 'A get request may not have body.'); + + assert_false(req.bodyUsed, 'After the GET case'); + + assert_throws_js(TypeError, + function() { new Request(req, {method: 'CONNECT'}); }, + 'Request() with a forbidden method must throw.'); + + assert_false(req.bodyUsed, 'After the forbidden method case'); + + var req2 = new Request(req); + assert_true(req.bodyUsed, + 'Request should be flagged as used if it has been consumed.'); +}, 'Request construction failure should not set "bodyUsed"'); diff --git a/testing/web-platform/tests/fetch/api/request/request-error.any.js b/testing/web-platform/tests/fetch/api/request/request-error.any.js new file mode 100644 index 0000000000..9ec8015198 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-error.any.js @@ -0,0 +1,56 @@ +// META: global=window,worker +// META: title=Request error +// META: script=request-error.js + +// badRequestArgTests is from response-error.js +for (const { args, testName } of badRequestArgTests) { + test(() => { + assert_throws_js( + TypeError, + () => new Request(...args), + "Expect TypeError exception" + ); + }, testName); +} + +test(function() { + assert_throws_js( + TypeError, + () => Request("about:blank"), + "Calling Request constructor without 'new' must throw" + ); +}); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from the init request"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var headers = new Headers([]); + var request = new Request(initialRequest, {"headers" : headers}); + assert_false(request.headers.has("Content-Type")); +}, "Request should not get its content-type from the init request if init headers are provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); +}, "Request should get its content-type from the body if none is provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from init headers if one is provided"); + +test(function() { + var options = {"cache": "only-if-cached", "mode": "same-origin"}; + new Request("test", options); +}, "Request with cache mode: only-if-cached and fetch mode: same-origin"); diff --git a/testing/web-platform/tests/fetch/api/request/request-error.js b/testing/web-platform/tests/fetch/api/request/request-error.js new file mode 100644 index 0000000000..cf77313f5b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-error.js @@ -0,0 +1,57 @@ +const badRequestArgTests = [ + { + args: ["", { "window": "http://test.url" }], + testName: "RequestInit's window is not null" + }, + { + args: ["http://:not a valid URL"], + testName: "Input URL is not valid" + }, + { + args: ["http://user:pass@test.url"], + testName: "Input URL has credentials" + }, + { + args: ["", { "mode": "navigate" }], + testName: "RequestInit's mode is navigate" + }, + { + args: ["", { "referrer": "http://:not a valid URL" }], + testName: "RequestInit's referrer is invalid" + }, + { + args: ["", { "method": "IN VALID" }], + testName: "RequestInit's method is invalid" + }, + { + args: ["", { "method": "TRACE" }], + testName: "RequestInit's method is forbidden" + }, + { + args: ["", { "mode": "no-cors", "method": "PUT" }], + testName: "RequestInit's mode is no-cors and method is not simple" + }, + { + args: ["", { "mode": "cors", "cache": "only-if-cached" }], + testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin" + }, + { + args: ["test", { "cache": "only-if-cached", "mode": "cors" }], + testName: "Request with cache mode: only-if-cached and fetch mode cors" + }, + { + args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }], + testName: "Request with cache mode: only-if-cached and fetch mode no-cors" + } +]; + +badRequestArgTests.push( + ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => { + const options = {}; + options[optionProp] = "BAD"; + return { + args: ["", options], + testName: `Bad ${optionProp} init parameter value` + }; + }) +); diff --git a/testing/web-platform/tests/fetch/api/request/request-headers.any.js b/testing/web-platform/tests/fetch/api/request/request-headers.any.js new file mode 100644 index 0000000000..a766bcb5ff --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-headers.any.js @@ -0,0 +1,177 @@ +// META: global=window,worker +// META: title=Request Headers + +var validRequestHeaders = [ + ["Content-Type", "OK"], + ["Potato", "OK"], + ["proxy", "OK"], + ["proxya", "OK"], + ["sec", "OK"], + ["secb", "OK"], + ["Set-Cookie2", "OK"], + ["User-Agent", "OK"], +]; +var invalidRequestHeaders = [ + ["Accept-Charset", "KO"], + ["accept-charset", "KO"], + ["ACCEPT-ENCODING", "KO"], + ["Accept-Encoding", "KO"], + ["Access-Control-Request-Headers", "KO"], + ["Access-Control-Request-Method", "KO"], + ["Connection", "KO"], + ["Content-Length", "KO"], + ["Cookie", "KO"], + ["Cookie2", "KO"], + ["Date", "KO"], + ["DNT", "KO"], + ["Expect", "KO"], + ["Host", "KO"], + ["Keep-Alive", "KO"], + ["Origin", "KO"], + ["Referer", "KO"], + ["Set-Cookie", "KO"], + ["TE", "KO"], + ["Trailer", "KO"], + ["Transfer-Encoding", "KO"], + ["Upgrade", "KO"], + ["Via", "KO"], + ["Proxy-", "KO"], + ["proxy-a", "KO"], + ["Sec-", "KO"], + ["sec-b", "KO"], +]; + +var validRequestNoCorsHeaders = [ + ["Accept", "OK"], + ["Accept-Language", "OK"], + ["content-language", "OK"], + ["content-type", "application/x-www-form-urlencoded"], + ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"], + ["content-type", "multipart/form-data"], + ["content-type", "multipart/form-data;charset=UTF-8"], + ["content-TYPE", "text/plain"], + ["CONTENT-type", "text/plain;charset=UTF-8"], +]; +var invalidRequestNoCorsHeaders = [ + ["Content-Type", "KO"], + ["Potato", "KO"], + ["proxy", "KO"], + ["proxya", "KO"], + ["sec", "KO"], + ["secb", "KO"], + ["Empty-Value", ""], +]; + +validRequestHeaders.forEach(function(header) { + test(function() { + var request = new Request(""); + request.headers.set(header[0], header[1]); + assert_equals(request.headers.get(header[0]), header[1]); + }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\""); +}); +invalidRequestHeaders.forEach(function(header) { + test(function() { + var request = new Request(""); + request.headers.set(header[0], header[1]); + assert_equals(request.headers.get(header[0]), null); + }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\""); +}); + +validRequestNoCorsHeaders.forEach(function(header) { + test(function() { + var requestNoCors = new Request("", {"mode": "no-cors"}); + requestNoCors.headers.set(header[0], header[1]); + assert_equals(requestNoCors.headers.get(header[0]), header[1]); + }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\""); +}); +invalidRequestNoCorsHeaders.forEach(function(header) { + test(function() { + var requestNoCors = new Request("", {"mode": "no-cors"}); + requestNoCors.headers.set(header[0], header[1]); + assert_equals(requestNoCors.headers.get(header[0]), null); + }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\""); +}); + +test(function() { + var headers = new Headers([["Cookie2", "potato"]]); + var request = new Request("", {"headers": headers}); + assert_equals(request.headers.get("Cookie2"), null); +}, "Check that request constructor is filtering headers provided as init parameter"); + +test(function() { + var headers = new Headers([["Content-Type", "potato"]]); + var request = new Request("", {"headers": headers, "mode": "no-cors"}); + assert_equals(request.headers.get("Content-Type"), null); +}, "Check that no-cors request constructor is filtering headers provided as init parameter"); + +test(function() { + var headers = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers": headers}); + var request = new Request(initialRequest, {"mode": "no-cors"}); + assert_equals(request.headers.get("Content-Type"), null); +}, "Check that no-cors request constructor is filtering headers provided as part of request parameter"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from the init request"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders}); + var headers = new Headers([]); + var request = new Request(initialRequest, {"headers" : headers}); + assert_false(request.headers.has("Content-Type")); +}, "Request should not get its content-type from the init request if init headers are provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); +}, "Request should get its content-type from the body if none is provided"); + +test(function() { + var initialHeaders = new Headers([["Content-Type", "potato"]]); + var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); + var request = new Request(initialRequest); + assert_equals(request.headers.get("Content-Type"), "potato"); +}, "Request should get its content-type from init headers if one is provided"); + +test(function() { + var array = [["hello", "worldAHH"]]; + var object = {"hello": 'worldOOH'}; + var headers = new Headers(array); + + assert_equals(headers.get("hello"), "worldAHH"); + + var request1 = new Request("", {"headers": headers}); + var request2 = new Request("", {"headers": array}); + var request3 = new Request("", {"headers": object}); + + assert_equals(request1.headers.get("hello"), "worldAHH"); + assert_equals(request2.headers.get("hello"), "worldAHH"); + assert_equals(request3.headers.get("hello"), "worldOOH"); +}, "Testing request header creations with various objects"); + +promise_test(function(test) { + var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"}); + return request.blob().then(function(blob) { + assert_equals(blob.type, "", "Blob type should be the empty string"); + }); +}, "Testing empty Request Content-Type header"); + +test(function() { + const request1 = new Request(""); + assert_equals(request1.headers, request1.headers); + + const request2 = new Request("", {"headers": {"X-Foo": "bar"}}); + assert_equals(request2.headers, request2.headers); + const headers = request2.headers; + request2.headers.set("X-Foo", "quux"); + assert_equals(headers, request2.headers); + headers.set("X-Other-Header", "baz"); + assert_equals(headers, request2.headers); +}, "Test that Request.headers has the [SameObject] extended attribute"); diff --git a/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html b/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html new file mode 100644 index 0000000000..cc495a6652 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Request init: simple cases</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#request"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + // https://fetch.spec.whatwg.org/#concept-method-normalize + var methods = { + "givenValues" : [ + "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", + "get", "head", "post", "put", "delete", "options", + "Get", "hEad", "poSt", "Put", "deleTe", "optionS", + "PATCH", "patch", "patCh" + ], + "expectedValues" : [ + "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", + "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", + "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", + "PATCH", "patch", "patCh" + ] + }; + var referrers = {"givenValues" : ["/relative/ressource", + "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment", + "http://{{host}}:{{ports[http][0]}}/", + "http://test.url", + "about:client", + "" + ], + "expectedValues" : ["http://{{host}}:{{ports[http][0]}}/relative/ressource", + "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment", + "http://{{host}}:{{ports[http][0]}}/", + "about:client", + "about:client", + "" + ] + }; + var referrerPolicies = {"givenValues" : [ "", + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "unsafe-url", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin" + ], + "expectedValues" : ["", + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "unsafe-url", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin" + ] + }; + var modes = {"givenValues" : ["same-origin", "no-cors", "cors"], + "expectedValues" : ["same-origin", "no-cors", "cors"] + }; + var credentials = {"givenValues" : ["omit", "same-origin", "include"], + "expectedValues" : ["omit", "same-origin", "include"] + }; + var caches = {"givenValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"], + "expectedValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"] + }; + var redirects = {"givenValues" : ["follow", "error", "manual"], + "expectedValues" : ["follow", "error", "manual"] + }; + var integrities = {"givenValues" : ["", "AZERTYUIOP1234567890" ], + "expectedValues" : ["", "AZERTYUIOP1234567890"] + }; + + //there is no getter for window, init's window might be null + var windows = {"givenValues" : [ null ], + "expectedValues" : [undefined] + }; + + var initValuesDict = { "method" : methods, + "referrer" : referrers, + "referrerPolicy" : referrerPolicies, + "mode" : modes, + "credentials" : credentials, + "cache" : caches, + "redirect" : redirects, + "integrity" : integrities, + "window" : windows + }; + + for (var attributeName in initValuesDict) { + var valuesToTest = initValuesDict[attributeName]; + for (var valueIdx in valuesToTest["givenValues"]) { + var givenValue = valuesToTest["givenValues"][valueIdx]; + var expectedValue = valuesToTest["expectedValues"][valueIdx]; + test(function() { + var requestInit = {}; + requestInit[attributeName] = givenValue + var request = new Request("", requestInit); + assert_equals(request[attributeName], expectedValue, + "Expect request's " + attributeName + " is " + expectedValue + " when initialized with " + givenValue); + }, "Check " + attributeName + " init value of " + givenValue + " and associated getter"); + } + } + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/request/request-init-002.any.js b/testing/web-platform/tests/fetch/api/request/request-init-002.any.js new file mode 100644 index 0000000000..abb6689f1e --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-002.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker +// META: title=Request init: headers and body + +test(function() { + var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3" + }; + var headers = new Headers(headerDict); + var request = new Request("", { "headers" : headers }) + for (var name in headerDict) { + assert_equals(request.headers.get(name), headerDict[name], + "request's headers has " + name + " : " + headerDict[name]); + } +}, "Initialize Request with headers values"); + +function makeRequestInit(body, method) { + return {"method": method, "body": body}; +} + +function checkRequestInit(body, bodyType, expectedTextBody) { + promise_test(function(test) { + var request = new Request("", makeRequestInit(body, "POST")); + if (body) { + assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); }); + assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); }); + } else { + new Request("", makeRequestInit(body, "GET")); // should not throw + } + var reqHeaders = request.headers; + var mime = reqHeaders.get("Content-Type"); + assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\""); + return request.text().then(function(bodyAsText) { + //not equals: cannot guess formData exact value + assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body"); + }); + }, `Initialize Request's body with "${body}", ${bodyType}`); +} + +var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); +var formaData = new FormData(); +formaData.append("name", "value"); +var usvString = "This is a USVString" + +checkRequestInit(undefined, undefined, ""); +checkRequestInit(null, null, ""); +checkRequestInit(blob, "application/octet-binary", "This is a blob"); +checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); +checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); +checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!"); + +// Ensure test does not time out in case of missing URLSearchParams support. +if (self.URLSearchParams) { + var urlSearchParams = new URLSearchParams("name=value"); + checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); +} else { + promise_test(function(test) { + return Promise.reject("URLSearchParams not supported"); + }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8"); +} diff --git a/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html b/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html new file mode 100644 index 0000000000..79c91cdfe8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html @@ -0,0 +1,84 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Request: init with request or url</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#request"> + <meta name="help" href="https://url.spec.whatwg.org/#concept-url-serializer"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../resources/utils.js"></script> + <script> + var headers = new Headers( {"name":"value"} ); + var emptyHeaders = new Headers(); + + var initValuesDict = {"method" : "POST", + "referrer" : "http://{{host}}:{{ports[http][0]}}/", + "referrerPolicy" : "origin", + "mode" : "same-origin", + "credentials" : "include", + "cache" : "no-cache", + "redirect" : "error", + "integrity" : "Request's Integrity", + "headers" : headers, + "body" : "Request's body" + }; + + var expectedInitialized = {"method" : "POST", + "referrer" : "http://{{host}}:{{ports[http][0]}}/", + "referrerPolicy" : "origin", + "mode" : "same-origin", + "credentials" : "include", + "cache" : "no-cache", + "redirect" : "error", + "integrity" : "Request's Integrity", + "headers" : headers, + "body" : "Request's body" + }; + + var expectedDefault = {"method" : "GET", + "url" : location.href, + "referrer" : "about:client", + "referrerPolicy" : "", + "mode" : "cors", + "credentials" : "same-origin", + "cache" : "default", + "redirect" : "follow", + "integrity" : "", + "headers" : emptyHeaders + }; + + var requestDefault = new Request(""); + var requestInitialized = new Request("", initValuesDict); + + test(function() { + var requestToCheck = new Request(requestInitialized); + checkRequest(requestToCheck, expectedInitialized); + }, "Check request values when initialized from Request"); + + test(function() { + var requestToCheck = new Request(requestDefault, initValuesDict); + checkRequest(requestToCheck, expectedInitialized); + }, "Check request values when initialized from Request and init values"); + + test(function() { + var url = "http://url.test:1234/path/subpath?query=true"; + url += "#fragment"; + expectedDefault["url"] = url; + var requestToCheck = new Request(url); + checkRequest(requestToCheck, expectedDefault); + }, "Check request values when initialized from url string"); + + test(function() { + var url = "http://url.test:1234/path/subpath?query=true"; + url += "#fragment"; + expectedInitialized["url"] = url; + var requestToCheck = new Request(url , initValuesDict); + checkRequest(requestToCheck, expectedInitialized); + }, "Check request values when initialized from url and init values"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js b/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js new file mode 100644 index 0000000000..18a6969d4f --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js @@ -0,0 +1,141 @@ +function requestFromBody(body) { + return new Request( + "https://example.com", + { + method: "POST", + body, + duplex: "half", + }, + ); +} + +test(() => { + const request = requestFromBody(undefined); + assert_equals(request.headers.get("Content-Type"), null); +}, "Default Content-Type for Request with empty body"); + +test(() => { + const blob = new Blob([]); + const request = requestFromBody(blob); + assert_equals(request.headers.get("Content-Type"), null); +}, "Default Content-Type for Request with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const request = requestFromBody(blob); + assert_equals(request.headers.get("Content-Type"), null); +}, "Default Content-Type for Request with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const request = requestFromBody(blob); + assert_equals(request.headers.get("Content-Type"), "a/b; c=d"); +}, "Default Content-Type for Request with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const request = requestFromBody(buffer); + assert_equals(request.headers.get("Content-Type"), null); +}, "Default Content-Type for Request with buffer source body"); + +promise_test(async () => { + const formData = new FormData(); + formData.append("a", "b"); + const request = requestFromBody(formData); + const boundary = (await request.text()).split("\r\n")[0].slice(2); + assert_equals( + request.headers.get("Content-Type"), + `multipart/form-data; boundary=${boundary}`, + ); +}, "Default Content-Type for Request with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const request = requestFromBody(usp); + assert_equals( + request.headers.get("Content-Type"), + "application/x-www-form-urlencoded;charset=UTF-8", + ); +}, "Default Content-Type for Request with URLSearchParams body"); + +test(() => { + const request = requestFromBody(""); + assert_equals( + request.headers.get("Content-Type"), + "text/plain;charset=UTF-8", + ); +}, "Default Content-Type for Request with string body"); + +test(() => { + const stream = new ReadableStream(); + const request = requestFromBody(stream); + assert_equals(request.headers.get("Content-Type"), null); +}, "Default Content-Type for Request with ReadableStream body"); + +// ----------------------------------------------------------------------------- + +const OVERRIDE_MIME = "test/only; mime=type"; + +function requestFromBodyWithOverrideMime(body) { + return new Request( + "https://example.com", + { + method: "POST", + body, + headers: { "Content-Type": OVERRIDE_MIME }, + duplex: "half", + }, + ); +} + +test(() => { + const request = requestFromBodyWithOverrideMime(undefined); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with empty body"); + +test(() => { + const blob = new Blob([]); + const request = requestFromBodyWithOverrideMime(blob); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const request = requestFromBodyWithOverrideMime(blob); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const request = requestFromBodyWithOverrideMime(blob); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const request = requestFromBodyWithOverrideMime(buffer); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with buffer source body"); + +test(() => { + const formData = new FormData(); + const request = requestFromBodyWithOverrideMime(formData); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const request = requestFromBodyWithOverrideMime(usp); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with URLSearchParams body"); + +test(() => { + const request = requestFromBodyWithOverrideMime(""); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with string body"); + +test(() => { + const stream = new ReadableStream(); + const request = requestFromBodyWithOverrideMime(stream); + assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Request with ReadableStream body"); diff --git a/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js b/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js new file mode 100644 index 0000000000..eb5073c857 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js @@ -0,0 +1,26 @@ +var priorities = ["high", + "low", + "auto" + ]; + +for (idx in priorities) { + test(() => { + new Request("", {priority: priorities[idx]}); + }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error"); +} + +test(() => { + assert_throws_js(TypeError, () => { + new Request("", {priority: 'invalid'}); + }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value"); +}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid"); + +for (idx in priorities) { + promise_test(function(t) { + return fetch('hello.txt', { priority: priorities[idx] }); + }, "fetch() with a '" + priorities[idx] + "' priority completes successfully"); +} + +promise_test(function(t) { + return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' })); +}, "fetch() with an invalid priority returns a rejected promise with a TypeError"); diff --git a/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js b/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js new file mode 100644 index 0000000000..f0ae441a00 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js @@ -0,0 +1,147 @@ +// META: global=window,worker + +"use strict"; + +const duplex = "half"; +const method = "POST"; + +test(() => { + const body = new ReadableStream(); + const request = new Request("...", { method, body, duplex }); + assert_equals(request.body, body); +}, "Constructing a Request with a stream holds the original object."); + +test((t) => { + const body = new ReadableStream(); + body.getReader(); + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "Constructing a Request with a stream on which getReader() is called"); + +test((t) => { + const body = new ReadableStream(); + body.getReader().read(); + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "Constructing a Request with a stream on which read() is called"); + +promise_test(async (t) => { + const body = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }); + const reader = body.getReader(); + await reader.read(); + reader.releaseLock(); + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "Constructing a Request with a stream on which read() and releaseLock() are called"); + +test((t) => { + const request = new Request("...", { method: "POST", body: "..." }); + request.body.getReader(); + assert_throws_js(TypeError, () => new Request(request)); + // This doesn't throw. + new Request(request, { body: "..." }); +}, "Constructing a Request with a Request on which body.getReader() is called"); + +test((t) => { + const request = new Request("...", { method: "POST", body: "..." }); + request.body.getReader().read(); + assert_throws_js(TypeError, () => new Request(request)); + // This doesn't throw. + new Request(request, { body: "..." }); +}, "Constructing a Request with a Request on which body.getReader().read() is called"); + +promise_test(async (t) => { + const request = new Request("...", { method: "POST", body: "..." }); + const reader = request.body.getReader(); + await reader.read(); + reader.releaseLock(); + assert_throws_js(TypeError, () => new Request(request)); + // This doesn't throw. + new Request(request, { body: "..." }); +}, "Constructing a Request with a Request on which read() and releaseLock() are called"); + +test((t) => { + new Request("...", { method, body: null }); +}, "It is OK to omit .duplex when the body is null."); + +test((t) => { + new Request("...", { method, body: "..." }); +}, "It is OK to omit .duplex when the body is a string."); + +test((t) => { + new Request("...", { method, body: new Uint8Array(3) }); +}, "It is OK to omit .duplex when the body is a Uint8Array."); + +test((t) => { + new Request("...", { method, body: new Blob([]) }); +}, "It is OK to omit .duplex when the body is a Blob."); + +test((t) => { + const body = new ReadableStream(); + assert_throws_js(TypeError, + () => new Request("...", { method, body })); +}, "It is error to omit .duplex when the body is a ReadableStream."); + +test((t) => { + new Request("...", { method, body: null, duplex: "half" }); +}, "It is OK to set .duplex = 'half' when the body is null."); + +test((t) => { + new Request("...", { method, body: "...", duplex: "half" }); +}, "It is OK to set .duplex = 'half' when the body is a string."); + +test((t) => { + new Request("...", { method, body: new Uint8Array(3), duplex: "half" }); +}, "It is OK to set .duplex = 'half' when the body is a Uint8Array."); + +test((t) => { + new Request("...", { method, body: new Blob([]), duplex: "half" }); +}, "It is OK to set .duplex = 'half' when the body is a Blob."); + +test((t) => { + const body = new ReadableStream(); + new Request("...", { method, body, duplex: "half" }); +}, "It is OK to set .duplex = 'half' when the body is a ReadableStream."); + +test((t) => { + const body = null; + const duplex = "full"; + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "It is error to set .duplex = 'full' when the body is null."); + +test((t) => { + const body = "..."; + const duplex = "full"; + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "It is error to set .duplex = 'full' when the body is a string."); + +test((t) => { + const body = new Uint8Array(3); + const duplex = "full"; + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "It is error to set .duplex = 'full' when the body is a Uint8Array."); + +test((t) => { + const body = new Blob([]); + const duplex = "full"; + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "It is error to set .duplex = 'full' when the body is a Blob."); + +test((t) => { + const body = new ReadableStream(); + const duplex = "full"; + assert_throws_js(TypeError, + () => new Request("...", { method, body, duplex })); +}, "It is error to set .duplex = 'full' when the body is a ReadableStream."); + +test((t) => { + const body = new ReadableStream(); + const duplex = "half"; + const req1 = new Request("...", { method, body, duplex }); + const req2 = new Request(req1); +}, "It is OK to omit duplex when init.body is not given and input.body is given."); + diff --git a/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html b/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html new file mode 100644 index 0000000000..548ab38d7e --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html @@ -0,0 +1,97 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Request Keepalive Quota Tests</title> + <meta name="timeout" content="long"> + <meta name="help" href="https://fetch.spec.whatwg.org/#request"> + <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin"> + <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com"> + <meta name="variant" content="?include=fast"> + <meta name="variant" content="?include=slow-1"> + <meta name="variant" content="?include=slow-2"> + <meta name="variant" content="?include=slow-3"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/subset-tests-by-key.js"></script> + </head> + <body> + <script> + 'use strict'; + + // We want to ensure that our keepalive requests hang slightly before completing so we can validate + // the effects of a rolling quota. To do this we will utilize trickle.py with a 1s delay. This should + // prevent any of the Fetch's from finishing in this window. + const trickleURL = '../resources/trickle.py?count=1&ms='; + const noDelay = 0; + const standardDelay = 1000; + function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); + } + + // We should expect 64KiB of rolling quota for any type of keep-alive request sent. + const expectedQuota = 65536; + + function fetchKeepAliveRequest(delay, bodySize) { + // Create a body of the specified size that's filled with *'s + const body = '*'.repeat(bodySize); + return fetch(trickleURL + delay, {keepalive: true, body, method: 'POST'}).then(res => { + return res.text(); + }).then(() => { + return wait(1); + }); + } + + // Test 1 Byte + subsetTestByKey("fast", promise_test, function(test) { + return fetchKeepAliveRequest(noDelay, 1 /* bodySize */); + }, 'A Keep-Alive fetch() with a small body should succeed.'); + + // Test Quota full limit + subsetTestByKey("fast", promise_test, function(test) { + return fetchKeepAliveRequest(noDelay, expectedQuota /* bodySize */); + }, 'A Keep-Alive fetch() with a body at the Quota Limit should succeed.'); + + // Test Quota + 1 Byte + subsetTestByKey("fast", promise_test, function(test) { + return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, expectedQuota + 1)); + }, 'A Keep-Alive fetch() with a body over the Quota Limit should reject.'); + + // Test the Quota becomes available upon promise completion. + subsetTestByKey("slow-1", promise_test, function (test) { + // Fill our Quota then try to send a second fetch. + return fetchKeepAliveRequest(standardDelay, expectedQuota).then(() => { + // Now validate that we can send another Keep-Alive fetch for the full size of the quota. + return fetchKeepAliveRequest(noDelay, expectedQuota); + }); + }, 'A Keep-Alive fetch() should return its allocated Quota upon promise resolution.'); + + // Ensure only the correct amount of Quota becomes available when a fetch completes. + subsetTestByKey("slow-2", promise_test, function(test) { + // Create a fetch that uses all but 1 Byte of the Quota and runs for 2x as long as the other requests. + const first = fetchKeepAliveRequest(standardDelay * 2, expectedQuota - 1); + + // Now create a single Byte request that will complete quicker. + const second = fetchKeepAliveRequest(standardDelay, 1 /* bodySize */).then(() => { + // We shouldn't be able to create a 2 Byte request right now as only 1 Byte should have freed up. + return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 2 /* bodySize */)); + }).then(() => { + // Now validate that we can send another Keep-Alive fetch for just 1 Byte. + return fetchKeepAliveRequest(noDelay, 1 /* bodySize */); + }); + + return Promise.all([first, second]); + }, 'A Keep-Alive fetch() should return only its allocated Quota upon promise resolution.'); + + // Test rejecting a fetch() after the quota is used up. + subsetTestByKey("slow-3", promise_test, function (test) { + // Fill our Quota then try to send a second fetch. + const p = fetchKeepAliveRequest(standardDelay, expectedQuota); + + const q = promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 1 /* bodySize */)); + return Promise.all([p, q]); + }, 'A Keep-Alive fetch() should not be allowed if the Quota is used up.'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js b/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js new file mode 100644 index 0000000000..cb4506db46 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker +// META: title=Request keepalive +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +test(() => { + assert_false(new Request('/').keepalive, 'default'); + assert_true(new Request('/', {keepalive: true}).keepalive, 'true'); + assert_false(new Request('/', {keepalive: false}).keepalive, 'false'); + assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish'); + assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy'); +}, 'keepalive flag'); + +test(() => { + const init = {method: 'POST', keepalive: true, body: new ReadableStream()}; + assert_throws_js(TypeError, () => {new Request('/', init)}); +}, 'keepalive flag with stream body'); diff --git a/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html b/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html new file mode 100644 index 0000000000..7be3608d73 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<body> +<script> +const worker = 'resources/request-reset-attributes-worker.js'; + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +promise_test(async (t) => { + const scope = 'resources/hello.txt?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, + 'old: false, new: false'); + await new Promise((resolve) => { + frame.onload = resolve; + frame.contentWindow.location.reload(); + }); + assert_equals(frame.contentDocument.body.textContent, + 'old: true, new: false'); + } finally { + if (frame) { + frame.remove(); + } + if (reg) { + await reg.unregister(); + } + } + }, 'Request.isReloadNavigation is reset with non-empty RequestInit'); + +promise_test(async (t) => { + const scope = 'resources/hello.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, + 'old: false, new: 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/hello.html?ignore'; + }); + await wait(0); + await new Promise((resolve) => { + frame.onload = resolve; + frame.contentWindow.history.go(-1); + }); + assert_equals(frame.contentDocument.body.textContent, + 'old: true, new: false'); + } finally { + if (frame) { + frame.remove(); + } + if (reg) { + await reg.unregister(); + } + } +}, 'Request.isHistoryNavigation is reset with non-empty RequestInit'); + +promise_test(async (t) => { + const scope = 'resources/hello.txt?name=mode'; + 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, + 'old: navigate, new: same-origin'); + } finally { + if (frame) { + frame.remove(); + } + if (reg) { + await reg.unregister(); + } + } + }, 'Request.mode is reset with non-empty RequestInit when it\'s "navigate"'); +</script> diff --git a/testing/web-platform/tests/fetch/api/request/request-structure.any.js b/testing/web-platform/tests/fetch/api/request/request-structure.any.js new file mode 100644 index 0000000000..5e78553855 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/request-structure.any.js @@ -0,0 +1,143 @@ +// META: global=window,worker +// META: title=Request structure + +var request = new Request(""); +var methods = ["clone", + //Request implements Body + "arrayBuffer", + "blob", + "formData", + "json", + "text" + ]; +var attributes = ["method", + "url", + "headers", + "destination", + "referrer", + "referrerPolicy", + "mode", + "credentials", + "cache", + "redirect", + "integrity", + "isReloadNavigation", + "isHistoryNavigation", + "duplex", + //Request implements Body + "bodyUsed" + ]; +var internalAttributes = ["priority", + "internalpriority", + "blocking" + ]; + +function isReadOnly(request, attributeToCheck) { + var defaultValue = undefined; + var newValue = undefined; + switch (attributeToCheck) { + case "method": + defaultValue = "GET"; + newValue = "POST"; + break; + + case "url": + //default value is base url + //i.e http://example.com/fetch/api/request-structure.html + newValue = "http://url.test"; + break; + + case "headers": + request.headers = new Headers ({"name":"value"}); + assert_false(request.headers.has("name"), "Headers attribute is read only"); + return; + + case "destination": + defaultValue = ""; + newValue = "worker"; + break; + + case "referrer": + defaultValue = "about:client"; + newValue = "http://url.test"; + break; + + case "referrerPolicy": + defaultValue = ""; + newValue = "unsafe-url"; + break; + + case "mode": + defaultValue = "cors"; + newValue = "navigate"; + break; + + case "credentials": + defaultValue = "same-origin"; + newValue = "cors"; + break; + + case "cache": + defaultValue = "default"; + newValue = "reload"; + break; + + case "redirect": + defaultValue = "follow"; + newValue = "manual"; + break; + + case "integrity": + newValue = "CannotWriteIntegrity"; + break; + + case "bodyUsed": + defaultValue = false; + newValue = true; + break; + + case "isReloadNavigation": + defaultValue = false; + newValue = true; + break; + + case "isHistoryNavigation": + defaultValue = false; + newValue = true; + break; + + case "duplex": + defaultValue = "half"; + newValue = "full"; + break; + + default: + return; + } + + request[attributeToCheck] = newValue; + if (defaultValue === undefined) + assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only"); + else + assert_equals(request[attributeToCheck], defaultValue, + "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue); +} + +for (var idx in methods) { + test(function() { + assert_true(methods[idx] in request, "request has " + methods[idx] + " method"); + }, "Request has " + methods[idx] + " method"); +} + +for (var idx in attributes) { + test(function() { + assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute"); + isReadOnly(request, attributes[idx]); + }, "Check " + attributes[idx] + " attribute"); +} + +for (var idx in internalAttributes) { + test(function() { + assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute"); + }, "Request does not expose " + internalAttributes[idx] + " attribute"); +}
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/request/resources/cache.py b/testing/web-platform/tests/fetch/api/request/resources/cache.py new file mode 100644 index 0000000000..ca0bd644b4 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/resources/cache.py @@ -0,0 +1,67 @@ +from wptserve.utils import isomorphic_decode + +def main(request, response): + token = request.GET.first(b"token", None) + if b"querystate" in request.GET: + from json import JSONEncoder + response.headers.set(b"Content-Type", b"text/plain") + return JSONEncoder().encode(request.server.stash.take(token)) + content = request.GET.first(b"content", None) + tag = request.GET.first(b"tag", None) + date = request.GET.first(b"date", None) + expires = request.GET.first(b"expires", None) + vary = request.GET.first(b"vary", None) + cc = request.GET.first(b"cache_control", None) + redirect = request.GET.first(b"redirect", None) + inm = request.headers.get(b"If-None-Match", None) + ims = request.headers.get(b"If-Modified-Since", None) + pragma = request.headers.get(b"Pragma", None) + cache_control = request.headers.get(b"Cache-Control", None) + ignore = b"ignore" in request.GET + + if tag: + tag = b'"%s"' % tag + + server_state = request.server.stash.take(token) + if not server_state: + server_state = [] + state = dict() + if not ignore: + if inm: + state[u"If-None-Match"] = isomorphic_decode(inm) + if ims: + state[u"If-Modified-Since"] = isomorphic_decode(ims) + if pragma: + state[u"Pragma"] = isomorphic_decode(pragma) + if cache_control: + state[u"Cache-Control"] = isomorphic_decode(cache_control) + server_state.append(state) + request.server.stash.put(token, server_state) + + if tag: + response.headers.set(b"ETag", b'%s' % tag) + elif date: + response.headers.set(b"Last-Modified", date) + if expires: + response.headers.set(b"Expires", expires) + if vary: + response.headers.set(b"Vary", vary) + if cc: + response.headers.set(b"Cache-Control", cc) + + # The only-if-cached redirect tests wants CORS to be okay, the other tests + # are all same-origin anyways and don't care. + response.headers.set(b"Access-Control-Allow-Origin", b"*") + + if redirect: + response.headers.set(b"Location", redirect) + response.status = (302, b"Redirect") + return b"" + elif ((inm is not None and inm == tag) or + (ims is not None and ims == date)): + response.status = (304, b"Not Modified") + return b"" + else: + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/plain") + return content diff --git a/testing/web-platform/tests/fetch/api/request/resources/hello.txt b/testing/web-platform/tests/fetch/api/request/resources/hello.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/resources/hello.txt @@ -0,0 +1 @@ +hello diff --git a/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js b/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js new file mode 100644 index 0000000000..4b264ca2fe --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js @@ -0,0 +1,19 @@ +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; + } + + const name = params.get('name'); + const old_attribute = event.request[name]; + // If any of |init|'s member is present... + const init = {cache: 'no-store'} + const new_attribute = (new Request(event.request, init))[name]; + + event.respondWith( + new Response(`old: ${old_attribute}, new: ${new_attribute}`)); + }); diff --git a/testing/web-platform/tests/fetch/api/request/url-encoding.html b/testing/web-platform/tests/fetch/api/request/url-encoding.html new file mode 100644 index 0000000000..31c1ed3920 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/request/url-encoding.html @@ -0,0 +1,25 @@ +<!doctype html> +<meta charset=windows-1252> +<title>Fetch: URL encoding</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +const expectedURL = new URL("?%C3%9F", location.href).href; +const expectedURL2 = new URL("?%EF%BF%BD", location.href).href; +test(() => { + let r = new Request("?\u00DF"); + assert_equals(r.url, expectedURL); + + r = new Request("?\uD83D"); + assert_equals(r.url, expectedURL2); +}, "URL encoding and Request"); + +promise_test(() => { + return fetch("?\u00DF").then(res => { + assert_equals(res.url, expectedURL); + return fetch("?\uD83D").then(res2 => { + assert_equals(res2.url, expectedURL2); + }); + }); +}, "URL encoding and fetch()"); +</script> diff --git a/testing/web-platform/tests/fetch/api/resources/authentication.py b/testing/web-platform/tests/fetch/api/resources/authentication.py new file mode 100644 index 0000000000..8b6b00b087 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/authentication.py @@ -0,0 +1,14 @@ +def main(request, response): + user = request.auth.username + password = request.auth.password + + if user == b"user" and password == b"password": + return b"Authentication done" + + realm = b"test" + if b"realm" in request.GET: + realm = request.GET.first(b"realm") + + return ((401, b"Unauthorized"), + [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')], + b"Please login with credentials 'user' and 'password'") diff --git a/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py b/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py new file mode 100644 index 0000000000..94a77adead --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py @@ -0,0 +1,13 @@ +import time + +def main(request, response): + delay = float(request.GET.first(b"ms", 1000)) / 1E3 + count = int(request.GET.first(b"count", 50)) + time.sleep(delay) + response.headers.set(b"Transfer-Encoding", b"chunked") + response.write_status_headers() + time.sleep(delay) + for i in range(count): + response.writer.write_content(b"a\r\nTEST_CHUNK\r\n") + time.sleep(delay) + response.writer.write_content(b"garbage") diff --git a/testing/web-platform/tests/fetch/api/resources/basic.html b/testing/web-platform/tests/fetch/api/resources/basic.html new file mode 100644 index 0000000000..e23afd4bf6 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/basic.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<!-- + Duplicating /common/blank.html to make service worker scoping simpler in + ../abort/serviceworker-intercepted.https.html +--> diff --git a/testing/web-platform/tests/fetch/api/resources/cache.py b/testing/web-platform/tests/fetch/api/resources/cache.py new file mode 100644 index 0000000000..4de751e30b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/cache.py @@ -0,0 +1,18 @@ +ETAG = b'"123abc"' +CONTENT_TYPE = b"text/plain" +CONTENT = b"lorem ipsum dolor sit amet" + + +def main(request, response): + # let caching kick in if possible (conditional GET) + etag = request.headers.get(b"If-None-Match", None) + if etag == ETAG: + response.headers.set(b"X-HTTP-STATUS", 304) + response.status = (304, b"Not Modified") + return b"" + + # cache miss, so respond with the actual content + response.status = (200, b"OK") + response.headers.set(b"ETag", ETAG) + response.headers.set(b"Content-Type", CONTENT_TYPE) + return CONTENT diff --git a/testing/web-platform/tests/fetch/api/resources/clean-stash.py b/testing/web-platform/tests/fetch/api/resources/clean-stash.py new file mode 100644 index 0000000000..ee8c69ac44 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/clean-stash.py @@ -0,0 +1,6 @@ +def main(request, response): + token = request.GET.first(b"token") + if request.server.stash.take(token) is not None: + return b"1" + else: + return b"0" diff --git a/testing/web-platform/tests/fetch/api/resources/cors-top.txt b/testing/web-platform/tests/fetch/api/resources/cors-top.txt new file mode 100644 index 0000000000..83a3157d14 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/cors-top.txt @@ -0,0 +1 @@ +top
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers b/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/testing/web-platform/tests/fetch/api/resources/data.json b/testing/web-platform/tests/fetch/api/resources/data.json new file mode 100644 index 0000000000..76519fa8cc --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/data.json @@ -0,0 +1 @@ +{"key": "value"} diff --git a/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py b/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py new file mode 100644 index 0000000000..a651aeb4e8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py @@ -0,0 +1,14 @@ +def main(request, response): + headers = [(b"Content-Type", "text/html"), + (b"Cache-Control", b"no-cache")] + + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Headers", b'Authorization')) + + if b"authorization" in request.headers: + return 200, headers, request.headers.get(b"Authorization") + return 200, headers, "none" diff --git a/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py b/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py new file mode 100644 index 0000000000..0be3ece4a5 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py @@ -0,0 +1,7 @@ +def handle_headers(frame, request, response): + response.status = 200 + response.headers.update([('Content-Type', 'text/plain')]) + response.write_status_headers() + +def handle_data(frame, request, response): + response.writer.write_data(frame.data) diff --git a/testing/web-platform/tests/fetch/api/resources/echo-content.py b/testing/web-platform/tests/fetch/api/resources/echo-content.py new file mode 100644 index 0000000000..5e137e15d7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/echo-content.py @@ -0,0 +1,12 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + + headers = [(b"X-Request-Method", isomorphic_encode(request.method)), + (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")), + (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")), + # Avoid any kind of content sniffing on the response. + (b"Content-Type", b"text/plain")] + content = request.body + + return headers, content diff --git a/testing/web-platform/tests/fetch/api/resources/empty.txt b/testing/web-platform/tests/fetch/api/resources/empty.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/empty.txt diff --git a/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py b/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py new file mode 100644 index 0000000000..a26cd8064c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py @@ -0,0 +1,35 @@ +import time + + +def url_dir(request): + return u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + + +def stash_write(request, key, value): + """Write to the stash, overwriting any previous value""" + request.server.stash.take(key, url_dir(request)) + request.server.stash.put(key, value, url_dir(request)) + + +def main(request, response): + stateKey = request.GET.first(b"stateKey", b"") + abortKey = request.GET.first(b"abortKey", b"") + + if stateKey: + stash_write(request, stateKey, 'open') + + response.headers.set(b"Content-type", b"text/plain") + response.write_status_headers() + + # Writing an initial 2k so browsers realise it's there. *shrug* + response.writer.write(b"." * 2048) + + while True: + if not response.writer.write(b"."): + break + if abortKey and request.server.stash.take(abortKey, url_dir(request)): + break + time.sleep(0.01) + + if stateKey: + stash_write(request, stateKey, 'closed') diff --git a/testing/web-platform/tests/fetch/api/resources/inspect-headers.py b/testing/web-platform/tests/fetch/api/resources/inspect-headers.py new file mode 100644 index 0000000000..9ed566e607 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/inspect-headers.py @@ -0,0 +1,24 @@ +def main(request, response): + headers = [] + if b"headers" in request.GET: + checked_headers = request.GET.first(b"headers").split(b"|") + for header in checked_headers: + if header in request.headers: + headers.append((b"x-request-" + header, request.headers.get(header, b""))) + + if b"cors" in request.GET: + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + headers.append((b"Access-Control-Allow-Methods", b"GET, POST, HEAD")) + exposed_headers = [b"x-request-" + header for header in checked_headers] + headers.append((b"Access-Control-Expose-Headers", b", ".join(exposed_headers))) + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + else: + headers.append((b"Access-Control-Allow-Headers", b", ".join(request.headers))) + + headers.append((b"content-type", b"text/plain")) + return headers, b"" diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js b/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js new file mode 100644 index 0000000000..f6f511631e --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js @@ -0,0 +1,176 @@ +// Utility functions to help testing keepalive requests. + +// Returns a URL to an iframe that loads a keepalive URL on iframe loaded. +// +// The keepalive URL points to a target that stores `token`. The token will then +// be posted back on iframe loaded to the parent document. +// `method` defaults to GET. +// `frameOrigin` to specify the origin of the iframe to load. If not set, +// default to a different site origin. +// `requestOrigin` to specify the origin of the fetch request target. +// `sendOn` to specify the name of the event when the keepalive request should +// be sent instead of the default 'load'. +// `mode` to specify the fetch request's CORS mode. +// `disallowCrossOrigin` to ask the iframe to set up a server that disallows +// cross origin requests. +function getKeepAliveIframeUrl(token, method, { + frameOrigin = 'DEFAULT', + requestOrigin = '', + sendOn = 'load', + mode = 'cors', + disallowCrossOrigin = false +} = {}) { + const https = location.protocol.startsWith('https'); + frameOrigin = frameOrigin === 'DEFAULT' ? + get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] : + frameOrigin; + return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` + + `token=${token}&` + + `method=${method}&` + + `sendOn=${sendOn}&` + + `mode=${mode}&` + (disallowCrossOrigin ? `disallowCrossOrigin=1&` : ``) + + `origin=${requestOrigin}`; +} + +// Returns a different-site URL to an iframe that loads a keepalive URL. +// +// By default, the keepalive URL points to a target that redirects to another +// same-origin destination storing `token`. The token will then be posted back +// to parent document. +// +// The URL redirects can be customized from `origin1` to `origin2` if provided. +// Sets `withPreflight` to true to get URL enabling preflight. +function getKeepAliveAndRedirectIframeUrl( + token, origin1, origin2, withPreflight) { + const https = location.protocol.startsWith('https'); + const frameOrigin = + get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; + return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` + + `token=${token}&` + + `origin1=${origin1}&` + + `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``); +} + +async function iframeLoaded(iframe) { + return new Promise((resolve) => iframe.addEventListener('load', resolve)); +} + +// Obtains the token from the message posted by iframe after loading +// `getKeepAliveAndRedirectIframeUrl()`. +async function getTokenFromMessage() { + return new Promise((resolve) => { + window.addEventListener('message', (event) => { + resolve(event.data); + }, {once: true}); + }); +} + +// Tells if `token` has been stored in the server. +async function queryToken(token) { + const response = await fetch(`../resources/stash-take.py?key=${token}`); + const json = await response.json(); + return json; +} + +// A helper to assert the existence of `token` that should have been stored in +// the server by fetching ../resources/stash-put.py. +// +// This function simply wait for a custom amount of time before trying to +// retrieve `token` from the server. +// `expectTokenExist` tells if `token` should be present or not. +// +// NOTE: +// In order to parallelize the work, we are going to have an async_test +// for the rest of the work. Note that we want the serialized behavior +// for the steps so far, so we don't want to make the entire test case +// an async_test. +function assertStashedTokenAsync( + testName, token, {expectTokenExist = true} = {}) { + async_test(test => { + new Promise(resolve => test.step_timeout(resolve, 3000 /*ms*/)) + .then(test.step_func(() => { + return queryToken(token); + })) + .then(test.step_func(result => { + if (expectTokenExist) { + assert_equals(result, 'on', `token should be on (stashed).`); + test.done(); + } else { + assert_not_equals( + result, 'on', `token should not be on (stashed).`); + return Promise.reject(`Failed to retrieve token from server`); + } + })) + .catch(test.step_func(e => { + if (expectTokenExist) { + test.unreached_func(e); + } else { + test.done(); + } + })); + }, testName); +} + +/** + * In an iframe, and in `load` event handler, test to fetch a keepalive URL that + * involves in redirect to another URL. + * + * `unloadIframe` to unload the iframe before verifying stashed token to + * simulate the situation that unloads after fetching. Note that this test is + * different from `keepaliveRedirectInUnloadTest()` in that the the latter + * performs fetch() call directly in `unload` event handler, while this test + * does it in `load`. + */ +function keepaliveRedirectTest(desc, { + origin1 = '', + origin2 = '', + withPreflight = false, + unloadIframe = false, + expectFetchSucceed = true, +} = {}) { + desc = `[keepalive][iframe][load] ${desc}` + + (unloadIframe ? ' [unload at end]' : ''); + promise_test(async (test) => { + const tokenToStash = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveAndRedirectIframeUrl( + tokenToStash, origin1, origin2, withPreflight); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), tokenToStash); + if (unloadIframe) { + iframe.remove(); + } + + assertStashedTokenAsync( + desc, tokenToStash, {expectTokenExist: expectFetchSucceed}); + }, `${desc}; setting up`); +} + +/** + * Opens a different site window, and in `unload` event handler, test to fetch + * a keepalive URL that involves in redirect to another URL. + */ +function keepaliveRedirectInUnloadTest(desc, { + origin1 = '', + origin2 = '', + url2 = '', + withPreflight = false, + expectFetchSucceed = true +} = {}) { + desc = `[keepalive][new window][unload] ${desc}`; + + promise_test(async (test) => { + const targetUrl = + `${HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html?` + + `origin1=${origin1}&` + + `origin2=${origin2}&` + + `url2=${url2}&` + (withPreflight ? `with-headers` : ``); + const w = window.open(targetUrl); + const token = await getTokenFromMessage(); + w.close(); + + assertStashedTokenAsync( + desc, token, {expectTokenExist: expectFetchSucceed}); + }, `${desc}; setting up`); +} diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html b/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html new file mode 100644 index 0000000000..f9dae5a34e --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<meta charset="utf-8"> +<script> +const SEARCH_PARAMS = new URL(location.href).searchParams; +const ORIGIN = SEARCH_PARAMS.get('origin') || ''; +const FRAME_ORIGIN = new URL(location.href).origin; +const TOKEN = SEARCH_PARAMS.get('token') || ''; +const METHOD = SEARCH_PARAMS.get('method') || 'GET'; +const SEND_ON_EVENT = SEARCH_PARAMS.get('sendOn') || 'load'; +const MODE = SEARCH_PARAMS.get('mode') || 'cors'; +const DISALLOW_CROSS_ORIGIN = SEARCH_PARAMS.get('disallowCrossOrigin') || ''; +// CORS requests are allowed by this URL by default. +const url = `${ORIGIN}/fetch/api/resources/stash-put.py?key=${TOKEN}&value=on&mode=${MODE}` + + `&frame_origin=${FRAME_ORIGIN}` + (DISALLOW_CROSS_ORIGIN ? `&disallow_cross_origin=1` : ''); + +addEventListener(SEND_ON_EVENT, () => { + let p = fetch(url, {keepalive: true, method: METHOD, mode: MODE}); + window.parent.postMessage(TOKEN, '*'); +}); +</script> +</html> diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html new file mode 100644 index 0000000000..fdee00f312 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html @@ -0,0 +1,23 @@ +<!doctype html> +<html> +<meta charset="utf-8"> +<script> +const SEARCH_PARAMS = new URL(location.href).searchParams; +const ORIGIN1 = SEARCH_PARAMS.get('origin1') || ''; +const ORIGIN2 = SEARCH_PARAMS.get('origin2') || ''; +const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers'); +const TOKEN = SEARCH_PARAMS.get('token') || ''; + +const url = + `${ORIGIN1}/fetch/api/resources/redirect.py?` + + `delay=500&` + + `allow_headers=foo&` + + `location=${ORIGIN2}/fetch/api/resources/stash-put.py?key=${TOKEN}%26value=on`; + +addEventListener('load', () => { + const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined; + let p = fetch(url, {keepalive: true, headers}); + window.parent.postMessage(TOKEN, '*'); +}); +</script> +</html> diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html new file mode 100644 index 0000000000..c18650796c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html @@ -0,0 +1,42 @@ +<!doctype html> +<html> +<meta charset="utf-8"> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +const TOKEN = token(); +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +} = get_host_info(); + +const SEARCH_PARAMS = new URL(location.href).searchParams; +const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers'); +const ORIGIN1 = SEARCH_PARAMS.get('origin1') || ''; +const ORIGIN2 = SEARCH_PARAMS.get('origin2') || ''; +const URL2 = SEARCH_PARAMS.get('url2') || ''; + +const REDIRECT_DESTINATION = URL2 ? URL2 : + `${ORIGIN2}/fetch/api/resources/stash-put.py` + + `?key=${TOKEN}&value=on`; +const FROM_URL = + `${ORIGIN1}/fetch/api/resources/redirect.py?` + + `delay=500&` + + `allow_headers=foo&` + + `location=${encodeURIComponent(REDIRECT_DESTINATION)}`; + +addEventListener('load', () => { + const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined; + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentWindow.addEventListener('unload', () => { + iframe.contentWindow.fetch(FROM_URL, {keepalive: true, headers}); + }); + + window.opener.postMessage(TOKEN, '*'); + // Do NOT remove `iframe` here. We want to check the case where the nested + // frame is implicitly closed by window closure. +}); +</script> +</html> diff --git a/testing/web-platform/tests/fetch/api/resources/method.py b/testing/web-platform/tests/fetch/api/resources/method.py new file mode 100644 index 0000000000..c1a111b4cd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/method.py @@ -0,0 +1,18 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + headers = [] + if b"cors" in request.GET: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO")) + headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo")) + headers.append((b"Access-Control-Expose-Headers", b"x-request-method")) + + headers.append((b"x-request-method", isomorphic_encode(request.method))) + headers.append((b"x-request-content-type", request.headers.get(b"Content-Type", b"NO"))) + headers.append((b"x-request-content-length", request.headers.get(b"Content-Length", b"NO"))) + headers.append((b"x-request-content-encoding", request.headers.get(b"Content-Encoding", b"NO"))) + headers.append((b"x-request-content-language", request.headers.get(b"Content-Language", b"NO"))) + headers.append((b"x-request-content-location", request.headers.get(b"Content-Location", b"NO"))) + return headers, request.body diff --git a/testing/web-platform/tests/fetch/api/resources/preflight.py b/testing/web-platform/tests/fetch/api/resources/preflight.py new file mode 100644 index 0000000000..f983ef9522 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/preflight.py @@ -0,0 +1,78 @@ +def main(request, response): + headers = [(b"Content-Type", b"text/plain")] + stashed_data = {b'control_request_headers': b"", b'preflight': b"0", b'preflight_referrer': b""} + + token = None + if b"token" in request.GET: + token = request.GET.first(b"token") + + if b"origin" in request.GET: + for origin in request.GET[b'origin'].split(b", "): + headers.append((b"Access-Control-Allow-Origin", origin)) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + + if b"clear-stash" in request.GET: + if request.server.stash.take(token) is not None: + return headers, b"1" + else: + return headers, b"0" + + if b"credentials" in request.GET: + headers.append((b"Access-Control-Allow-Credentials", b"true")) + + if request.method == u"OPTIONS": + if not b"Access-Control-Request-Method" in request.headers: + response.set_error(400, u"No Access-Control-Request-Method header") + return b"ERROR: No access-control-request-method in preflight!" + + if request.headers.get(b"Accept", b"") != b"*/*": + response.set_error(400, u"Request does not have 'Accept: */*' header") + return b"ERROR: Invalid access in preflight!" + + if b"control_request_headers" in request.GET: + stashed_data[b'control_request_headers'] = request.headers.get(b"Access-Control-Request-Headers", None) + + if b"max_age" in request.GET: + headers.append((b"Access-Control-Max-Age", request.GET[b'max_age'])) + + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + + if b"allow_methods" in request.GET: + headers.append((b"Access-Control-Allow-Methods", request.GET[b'allow_methods'])) + + preflight_status = 200 + if b"preflight_status" in request.GET: + preflight_status = int(request.GET.first(b"preflight_status")) + + stashed_data[b'preflight'] = b"1" + stashed_data[b'preflight_referrer'] = request.headers.get(b"Referer", b"") + stashed_data[b'preflight_user_agent'] = request.headers.get(b"User-Agent", b"") + if token: + request.server.stash.put(token, stashed_data) + + return preflight_status, headers, b"" + + + if token: + data = request.server.stash.take(token) + if data: + stashed_data = data + + if b"checkUserAgentHeaderInPreflight" in request.GET and request.headers.get(b"User-Agent") != stashed_data[b'preflight_user_agent']: + return 400, headers, b"ERROR: No user-agent header in preflight" + + #use x-* headers for returning value to bodyless responses + headers.append((b"Access-Control-Expose-Headers", b"x-did-preflight, x-control-request-headers, x-referrer, x-preflight-referrer, x-origin")) + headers.append((b"x-did-preflight", stashed_data[b'preflight'])) + if stashed_data[b'control_request_headers'] != None: + headers.append((b"x-control-request-headers", stashed_data[b'control_request_headers'])) + headers.append((b"x-preflight-referrer", stashed_data[b'preflight_referrer'])) + headers.append((b"x-referrer", request.headers.get(b"Referer", b""))) + headers.append((b"x-origin", request.headers.get(b"Origin", b""))) + + if token: + request.server.stash.put(token, stashed_data) + + return headers, b"" diff --git a/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py b/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py new file mode 100644 index 0000000000..1a5f7feb2a --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py @@ -0,0 +1,3 @@ +def main(request, response): + headers = [(b"Location", b"")] + return 302, headers, b"" diff --git a/testing/web-platform/tests/fetch/api/resources/redirect.h2.py b/testing/web-platform/tests/fetch/api/resources/redirect.h2.py new file mode 100644 index 0000000000..6937014587 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/redirect.h2.py @@ -0,0 +1,14 @@ +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def handle_headers(frame, request, response): + status = 302 + if b'redirect_status' in request.GET: + status = int(request.GET[b'redirect_status']) + response.status = status + + if b'location' in request.GET: + url = isomorphic_decode(request.GET[b'location']) + response.headers[b'Location'] = isomorphic_encode(url) + + response.headers.update([('Content-Type', 'text/plain')]) + response.write_status_headers() diff --git a/testing/web-platform/tests/fetch/api/resources/redirect.py b/testing/web-platform/tests/fetch/api/resources/redirect.py new file mode 100644 index 0000000000..d52ab5f3ee --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/redirect.py @@ -0,0 +1,73 @@ +import time + +from urllib.parse import urlencode, urlparse + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + stashed_data = {b'count': 0, b'preflight': b"0"} + status = 302 + headers = [(b"Content-Type", b"text/plain"), + (b"Cache-Control", b"no-cache"), + (b"Pragma", b"no-cache")] + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + + token = None + if b"token" in request.GET: + token = request.GET.first(b"token") + data = request.server.stash.take(token) + if data: + stashed_data = data + + if request.method == u"OPTIONS": + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + stashed_data[b'preflight'] = b"1" + #Preflight is not redirected: return 200 + if not b"redirect_preflight" in request.GET: + if token: + request.server.stash.put(request.GET.first(b"token"), stashed_data) + return 200, headers, u"" + + if b"redirect_status" in request.GET: + status = int(request.GET[b'redirect_status']) + elif b"redirect_status" in request.POST: + status = int(request.POST[b'redirect_status']) + + stashed_data[b'count'] += 1 + + if b"location" in request.GET: + url = isomorphic_decode(request.GET[b'location']) + if b"simple" not in request.GET: + scheme = urlparse(url).scheme + if scheme == u"" or scheme == u"http" or scheme == u"https": + url += u"&" if u'?' in url else u"?" + #keep url parameters in location + url_parameters = {} + for item in request.GET.items(): + url_parameters[isomorphic_decode(item[0])] = isomorphic_decode(item[1][0]) + url += urlencode(url_parameters) + #make sure location changes during redirection loop + url += u"&count=" + str(stashed_data[b'count']) + headers.append((b"Location", isomorphic_encode(url))) + + if b"redirect_referrerpolicy" in request.GET: + headers.append((b"Referrer-Policy", request.GET[b'redirect_referrerpolicy'])) + + if b"delay" in request.GET: + time.sleep(float(request.GET.first(b"delay", 0)) / 1E3) + + if token: + request.server.stash.put(request.GET.first(b"token"), stashed_data) + if b"max_count" in request.GET: + max_count = int(request.GET[b'max_count']) + #stop redirecting and return count + if stashed_data[b'count'] > max_count: + # -1 because the last is not a redirection + return str(stashed_data[b'count'] - 1) + + return status, headers, u"" diff --git a/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html b/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html new file mode 100644 index 0000000000..6e5d506547 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html @@ -0,0 +1,34 @@ +<!doctype html> +<html> +<script> +async function no_cors_should_be_rejected() { + let thrown = false; + try { + const resp = await fetch('top.txt'); + } catch (e) { + thrown = true; + } + if (!thrown) { + throw Error('fetching "top.txt" should be rejected.'); + } +} + +async function null_origin_should_be_accepted() { + const url = 'top.txt?pipe=header(access-control-allow-origin,null)|' + + 'header(cache-control,no-store)'; + const resp = await fetch(url); +} + +async function test() { + try { + await no_cors_should_be_rejected(); + await null_origin_should_be_accepted(); + parent.postMessage('PASS', '*'); + } catch (e) { + parent.postMessage(e.message, '*'); + } +} + +test(); +</script> +</html> diff --git a/testing/web-platform/tests/fetch/api/resources/script-with-header.py b/testing/web-platform/tests/fetch/api/resources/script-with-header.py new file mode 100644 index 0000000000..9a9c70ef5c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/script-with-header.py @@ -0,0 +1,7 @@ +def main(request, response): + headers = [(b"Content-type", request.GET.first(b"mime"))] + if b"content" in request.GET and request.GET.first(b"content") == b"empty": + content = b'' + else: + content = b"console.log('Script loaded')" + return 200, headers, content diff --git a/testing/web-platform/tests/fetch/api/resources/stash-put.py b/testing/web-platform/tests/fetch/api/resources/stash-put.py new file mode 100644 index 0000000000..91c198abb7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/stash-put.py @@ -0,0 +1,41 @@ +from wptserve.utils import isomorphic_decode + +def should_be_treated_as_same_origin_request(request): + """Tells whether request should be treated as same-origin request.""" + # In both of the following cases, allow to proceed with handling to simulate + # 'no-cors' mode: response is sent, but browser will make it opaque. + if request.GET.first(b'mode') == b'no-cors': + return True + + # We can't rely on the Origin header field of a fetch request, as it is only + # present for 'cors' mode or methods other than 'GET'/'HEAD' (i.e. present for + # 'POST'). See https://fetch.spec.whatwg.org/#http-origin + assert 'frame_origin ' in request.GET + frame_origin = request.GET.first(b'frame_origin').decode('utf-8') + host_origin = request.url_parts.scheme + '://' + request.url_parts.netloc + return frame_origin == host_origin + +def main(request, response): + if request.method == u'OPTIONS': + # CORS preflight + response.headers.set(b'Access-Control-Allow-Origin', b'*') + response.headers.set(b'Access-Control-Allow-Methods', b'*') + response.headers.set(b'Access-Control-Allow-Headers', b'*') + return 'done' + + if b'disallow_cross_origin' not in request.GET: + response.headers.set(b'Access-Control-Allow-Origin', b'*') + elif not should_be_treated_as_same_origin_request(request): + # As simple requests will not trigger preflight, we have to manually block + # cors requests before making any changes to storage. + # https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests + # https://fetch.spec.whatwg.org/#cors-preflight-fetch + return 'not stashing for cors request' + + url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + key = request.GET.first(b'key') + value = request.GET.first(b'value') + # value here must be a text string. It will be json.dump()'ed in stash-take.py. + request.server.stash.put(key, isomorphic_decode(value), url_dir) + + return 'done' diff --git a/testing/web-platform/tests/fetch/api/resources/stash-take.py b/testing/web-platform/tests/fetch/api/resources/stash-take.py new file mode 100644 index 0000000000..e6db80dd86 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/stash-take.py @@ -0,0 +1,9 @@ +from wptserve.handlers import json_handler + + +@json_handler +def main(request, response): + dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + key = request.GET.first(b"key") + response.headers.set(b'Access-Control-Allow-Origin', b'*') + return request.server.stash.take(key, dir) diff --git a/testing/web-platform/tests/fetch/api/resources/status.py b/testing/web-platform/tests/fetch/api/resources/status.py new file mode 100644 index 0000000000..05a59d5a63 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/status.py @@ -0,0 +1,11 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + code = int(request.GET.first(b"code", 200)) + text = request.GET.first(b"text", b"OMG") + content = request.GET.first(b"content", b"") + type = request.GET.first(b"type", b"") + status = (code, text) + headers = [(b"Content-Type", type), + (b"X-Request-Method", isomorphic_encode(request.method))] + return status, headers, content diff --git a/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js b/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js new file mode 100644 index 0000000000..19d4b189d8 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js @@ -0,0 +1,19 @@ +async function messageClient(clientId, message) { + const client = await clients.get(clientId); + client.postMessage(message); +} + +addEventListener('fetch', event => { + let resolve; + const promise = new Promise(r => resolve = r); + + function onAborted() { + messageClient(event.clientId, event.request.signal.reason); + resolve(); + } + + messageClient(event.clientId, 'fetch event has arrived'); + + event.respondWith(promise.then(() => new Response('hello'))); + event.request.signal.addEventListener('abort', onAborted); +}); diff --git a/testing/web-platform/tests/fetch/api/resources/sw-intercept.js b/testing/web-platform/tests/fetch/api/resources/sw-intercept.js new file mode 100644 index 0000000000..b8166b62a5 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/sw-intercept.js @@ -0,0 +1,10 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); diff --git a/testing/web-platform/tests/fetch/api/resources/top.txt b/testing/web-platform/tests/fetch/api/resources/top.txt new file mode 100644 index 0000000000..83a3157d14 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/top.txt @@ -0,0 +1 @@ +top
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/api/resources/trickle.py b/testing/web-platform/tests/fetch/api/resources/trickle.py new file mode 100644 index 0000000000..99833f1b38 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/trickle.py @@ -0,0 +1,15 @@ +import time + +def main(request, response): + delay = float(request.GET.first(b"ms", 500)) / 1E3 + count = int(request.GET.first(b"count", 50)) + # Read request body + request.body + time.sleep(delay) + if not b"notype" in request.GET: + response.headers.set(b"Content-type", b"text/plain") + response.write_status_headers() + time.sleep(delay) + for i in range(count): + response.writer.write_content(b"TEST_TRICKLE\n") + time.sleep(delay) diff --git a/testing/web-platform/tests/fetch/api/resources/utils.js b/testing/web-platform/tests/fetch/api/resources/utils.js new file mode 100644 index 0000000000..3721d9bf9c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/resources/utils.js @@ -0,0 +1,120 @@ +var RESOURCES_DIR = "../resources/"; + +function dirname(path) { + return path.replace(/\/[^\/]*$/, '/') +} + +function checkRequest(request, ExpectedValuesDict) { + for (var attribute in ExpectedValuesDict) { + switch(attribute) { + case "headers": + for (var key in ExpectedValuesDict["headers"].keys()) { + assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key), + "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key)); + } + break; + + case "body": + //for checking body's content, a dedicated asyncronous/promise test should be used + assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header") + break; + + case "method": + case "referrer": + case "referrerPolicy": + case "credentials": + case "cache": + case "redirect": + case "integrity": + case "url": + case "destination": + assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute") + break; + + default: + break; + } + } +} + +function stringToArray(str) { + var array = new Uint8Array(str.length); + for (var i=0, strLen = str.length; i < strLen; i++) + array[i] = str.charCodeAt(i); + return array; +} + +function encode_utf8(str) +{ + if (self.TextEncoder) + return (new TextEncoder).encode(str); + return stringToArray(unescape(encodeURIComponent(str))); +} + +function validateBufferFromString(buffer, expectedValue, message) +{ + return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message); +} + +function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromString(reader, expectedValue, newBuffer); + } + validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream"); + }); +} + +function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromPartialString(reader, expectedValue, newBuffer); + } + + var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer); + return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream"); + }); +} + +// From streams tests +function delay(milliseconds) +{ + return new Promise(function(resolve) { + step_timeout(resolve, milliseconds); + }); +} + +function requestForbiddenHeaders(desc, forbiddenHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": forbiddenHeaders} + var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in forbiddenHeaders) + assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined"); + }); + }, desc); +} diff --git a/testing/web-platform/tests/fetch/api/response/json.any.js b/testing/web-platform/tests/fetch/api/response/json.any.js new file mode 100644 index 0000000000..15f050e632 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/json.any.js @@ -0,0 +1,14 @@ +// See also /xhr/json.any.js + +promise_test(async t => { + const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`); + const json = await response.json(); + assert_array_equals(Object.keys(json), ["b", "a"]); + assert_equals(json.a, 2); + assert_equals(json.b, 3); +}, "Ensure the correct JSON parser is used"); + +promise_test(async t => { + const response = await fetch("/xhr/resources/utf16-bom.json"); + return promise_rejects_js(t, SyntaxError, response.json()); +}, "Ensure UTF-16 results in an error"); diff --git a/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html new file mode 100644 index 0000000000..fe5e7d4c07 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> + new Response(new ReadableStream({ + start(c) { + + for (const i of new Array(40000).fill()) { + c.enqueue(new Uint8Array(0)); + } + c.close(); + + } + })).text(); +</script> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html new file mode 100644 index 0000000000..9bb6e0bbf3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>Current page used as a test helper</title> +<base href="success/"> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html new file mode 100644 index 0000000000..f63372e64c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Incumbent page used as a test helper</title> + +<iframe src="../current/current.html" id="c"></iframe> +<iframe src="../relevant/relevant.html" id="r"></iframe> + +<script> +'use strict'; + +window.createRedirectResponse = (...args) => { + const current = document.querySelector('#c').contentWindow; + const relevant = document.querySelector('#r').contentWindow; + return current.Response.redirect.call(relevant.Response, ...args); +}; + +</script> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html new file mode 100644 index 0000000000..44f42eda49 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Relevant page used as a test helper</title> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html new file mode 100644 index 0000000000..5f2f42a1ce --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Response.redirect URL parsing, with multiple globals in play</title> +<link rel="help" href="https://fetch.spec.whatwg.org/#dom-response-redirect"> +<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- This is the entry global --> + +<iframe src="incumbent/incumbent.html"></iframe> + +<script> +'use strict'; + +const loadPromise = new Promise(resolve => { + window.addEventListener("load", () => resolve()); +}); + +promise_test(() => { + return loadPromise.then(() => { + const res = document.querySelector('iframe').contentWindow.createRedirectResponse("url"); + + assert_equals(res.headers.get("Location"), new URL("current/success/url", location.href).href); + }); +}, "should parse the redirect Location URL relative to the current settings object"); + +</script> diff --git a/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html new file mode 100644 index 0000000000..64b0755666 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title></title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> + <script> +function performMicrotaskCheckpoint() { + document.createNodeIterator(document, -1, { + acceptNode() { + return NodeFilter.FILTER_ACCEPT; + } + }).nextNode(); +} + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + // Add a getter for "then" that will incidentally be invoked + // during promise resolution. + Object.prototype.__defineGetter__('then', () => { + // Clean up behind ourselves. + delete Object.prototype.then; + + // This promise should (like all promises) be resolved + // asynchronously. + var executed = false; + Promise.resolve().then(_ => { executed = true; }); + + // This shouldn't run microtasks! They should only run + // after the fetch is resolved. + performMicrotaskCheckpoint(); + + // The fulfill handler above shouldn't have run yet. If it has run, + // throw to reject this promise and fail the test. + assert_false(executed, "shouldn't have run microtasks yet"); + + // Otherwise act as if there's no "then" property so the promise + // fulfills and the test passes. + return undefined; + }); + + // Create a read request, incidentally resolving a promise with an + // object value, thereby invoking the getter installed above. + return response.body.getReader().read(); + }); +}, "reading from a body stream should occur in a microtask scope"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + // Add a getter for "then" that will incidentally be invoked + // during promise resolution. + Object.prototype.__defineGetter__('then', () => { + // Clean up behind ourselves. + delete Object.prototype.then; + + // This promise should (like all promises) be resolved + // asynchronously. + var executed = false; + Promise.resolve().then(_ => { executed = true; }); + + // This shouldn't run microtasks! They should only run + // after the fetch is resolved. + performMicrotaskCheckpoint(); + + // The fulfill handler above shouldn't have run yet. If it has run, + // throw to reject this promise and fail the test. + assert_false(executed, "shouldn't have run microtasks yet"); + + // Otherwise act as if there's no "then" property so the promise + // fulfills and the test passes. + return undefined; + }); + + // Create a read request, incidentally resolving a promise with an + // object value, thereby invoking the getter installed above. + return response.body.pipeTo(new WritableStream({ + write(chunk) {} + })) + }); +}, "piping from a body stream to a JS-written WritableStream should occur in a microtask scope"); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js new file mode 100644 index 0000000000..91140d1afd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +// META: title=Response consume blob and http bodies +// META: script=../resources/utils.js + +promise_test(function(test) { + return new Response(new Blob([], { "type" : "text/plain" })).body.cancel(); +}, "Cancelling a starting blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["This is data"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + reader.read(); + return reader.cancel(); +}, "Cancelling a loading blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["T"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + + var closedPromise = reader.closed.then(function() { + return reader.cancel(); + }); + reader.read().then(function readMore({done, value}) { + if (!done) return reader.read().then(readMore); + }); + return closedPromise; +}, "Cancelling a closed blob Response stream"); + +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + return response.body.cancel(); + }); +}, "Cancelling a starting Response stream"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + var reader = response.body.getReader(); + return reader.read().then(function() { + return reader.cancel(); + }); + }); +}, "Cancelling a loading Response stream"); + +promise_test(function() { + async function readAll(reader) { + while (true) { + const {value, done} = await reader.read(); + if (done) + return; + } + } + + return fetch(RESOURCES_DIR + "top.txt").then(function(response) { + var reader = response.body.getReader(); + return readAll(reader).then(() => reader.cancel()); + }); +}, "Cancelling a closed Response stream"); + +promise_test(async () => { + const response = await fetch(RESOURCES_DIR + "top.txt"); + const { body } = response; + await body.cancel(); + assert_equals(body, response.body, ".body should not change after cancellation"); +}, "Accessing .body after canceling it"); diff --git a/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js new file mode 100644 index 0000000000..da54616c37 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js @@ -0,0 +1,32 @@ +// Verify that calling Response clone() in a detached iframe doesn't crash. +// Regression test for https://crbug.com/1082688. + +'use strict'; + +promise_test(async () => { + // Wait for the document body to be available. + await new Promise(resolve => { + onload = resolve; + }); + + window.iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.srcdoc = `<!doctype html> +<script> +const response = new Response('body'); +window.parent.postMessage('okay', '*'); +window.parent.iframe.remove(); +response.clone(); +</script> +`; + + await new Promise(resolve => { + onmessage = evt => { + if (evt.data === 'okay') { + resolve(); + } + }; + }); + + // If it got here without crashing, the test passed. +}, 'clone within removed iframe should not crash'); diff --git a/testing/web-platform/tests/fetch/api/response/response-clone.any.js b/testing/web-platform/tests/fetch/api/response/response-clone.any.js new file mode 100644 index 0000000000..f5cda75149 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-clone.any.js @@ -0,0 +1,140 @@ +// META: global=window,worker +// META: title=Response clone +// META: script=../resources/utils.js + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "" +}; + +var response = new Response(); +var clonedResponse = response.clone(); +test(function() { + for (var attributeName in defaultValues) { + var expectedValue = defaultValues[attributeName]; + assert_equals(clonedResponse[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + } +}, "Check Response's clone with default values, without body"); + +var body = "This is response body"; +var headersInit = { "name" : "value" }; +var responseInit = { "status" : 200, + "statusText" : "GOOD", + "headers" : headersInit +}; +var response = new Response(body, responseInit); +var clonedResponse = response.clone(); +test(function() { + assert_equals(clonedResponse.status, responseInit["status"], + "Expect response.status is " + responseInit["status"]); + assert_equals(clonedResponse.statusText, responseInit["statusText"], + "Expect response.statusText is " + responseInit["statusText"]); + assert_equals(clonedResponse.headers.get("name"), "value", + "Expect response.headers has name:value header"); +}, "Check Response's clone has the expected attribute values"); + +promise_test(function(test) { + return validateStreamFromString(response.body.getReader(), body); +}, "Check orginal response's body after cloning"); + +promise_test(function(test) { + return validateStreamFromString(clonedResponse.body.getReader(), body); +}, "Check cloned response's body"); + +promise_test(function(test) { + var disturbedResponse = new Response("data"); + return disturbedResponse.text().then(function() { + assert_true(disturbedResponse.bodyUsed, "response is disturbed"); + assert_throws_js(TypeError, function() { disturbedResponse.clone(); }, + "Expect TypeError exception"); + }); +}, "Cannot clone a disturbed response"); + +promise_test(function(t) { + var clone; + var result; + var response; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + response = res; + return clone.text(); + }).then(function(r) { + assert_equals(r.length, 26); + result = r; + return response.text(); + }).then(function(r) { + assert_equals(r, result, "cloned responses should provide the same data"); + }); + }, 'Cloned responses should provide the same data'); + +promise_test(function(t) { + var clone; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + res.body.cancel(); + assert_true(res.bodyUsed); + assert_false(clone.bodyUsed); + return clone.arrayBuffer(); + }).then(function(r) { + assert_equals(r.byteLength, 26); + assert_true(clone.bodyUsed); + }); +}, 'Cancelling stream should not affect cloned one'); + +function testReadableStreamClone(initialBuffer, bufferType) +{ + promise_test(function(test) { + var response = new Response(new ReadableStream({start : function(controller) { + controller.enqueue(initialBuffer); + controller.close(); + }})); + + var clone = response.clone(); + var stream1 = response.body; + var stream2 = clone.body; + + var buffer; + return stream1.getReader().read().then(function(data) { + assert_false(data.done); + assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer"); + return stream2.getReader().read(); + }).then(function(data) { + assert_false(data.done); + if (initialBuffer instanceof ArrayBuffer) { + assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal"); + assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content"); + } else if (initialBuffer instanceof DataView) { + assert_true(data.value instanceof DataView, "Cloned buffer is DataView"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal"); + assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal"); + for (let i = 0; i < initialBuffer.byteLength; ++i) { + assert_equals( + data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}"); + } + } else { + assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); + } + assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type"); + assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer"); + }); + }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)"); +} + +var arrayBuffer = new ArrayBuffer(16); +testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array"); +testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array"); +testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array"); +testReadableStreamClone(arrayBuffer, "ArrayBuffer"); +testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array"); +testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray"); +testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array"); +testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array"); +testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array"); +testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array"); +testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array"); +testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array"); +testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView"); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js new file mode 100644 index 0000000000..0fa85ecbcb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js @@ -0,0 +1,99 @@ +// META: global=window,worker +// META: title=Response consume empty bodies + +function checkBodyText(test, response) { + return response.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyBlob(test, response) { + return response.blob().then(function(bodyAsBlob) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); + }); +} + +function checkBodyArrayBuffer(test, response) { + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyJSON(test, response) { + return response.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormData(test, response) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormDataError(test, response) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_false(response.bodyUsed); + }); +} + +function checkResponseWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var response = new Response(undefined, { "headers": headers }); + assert_false(response.bodyUsed); + return checkFunction(test, response); + }, "Consume response's body as " + bodyType); +} + +checkResponseWithNoBody("text", checkBodyText); +checkResponseWithNoBody("blob", checkBodyBlob); +checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkResponseWithNoBody("json (error case)", checkBodyJSON); +checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkResponseWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var response = new Response(body); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return response.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer")); +} + +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkResponseWithEmptyBody("text", "", false); +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkResponseWithEmptyBody("text", "", true); +checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +checkResponseWithEmptyBody("FormData", new FormData(), true); +checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js new file mode 100644 index 0000000000..f89d7341ac --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js @@ -0,0 +1,80 @@ +// META: global=window,worker +// META: title=Response consume +// META: script=../resources/utils.js + +promise_test(function(test) { + var body = ""; + var response = new Response(""); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty text response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(new Blob([], { "type" : "text/plain" })); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty blob response's body as readableStream"); + +var formData = new FormData(); +formData.append("name", "value"); +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); +var urlSearchParamsData = "name=value"; +var urlSearchParams = new URLSearchParams(urlSearchParamsData); + +for (const mode of [undefined, "byob"]) { + promise_test(function(test) { + var response = new Response(blob); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read blob response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(textData); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read text response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(urlSearchParams); + return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData); + }, `Read URLSearchParams response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var arrayBuffer = new ArrayBuffer(textData.length); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < textData.length; cptr++) + int8Array[cptr] = textData.charCodeAt(cptr); + + return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData); + }, `Read array buffer response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(formData); + return validateStreamFromPartialString(response.body.getReader({ mode }), + "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); + }, `Read form data response's body as readableStream with mode=${mode}`); +} + +test(function() { + assert_equals(Response.error().body, null); +}, "Getting an error Response stream"); + +test(function() { + assert_equals(Response.redirect("/").body, null); +}, "Getting a redirect Response stream"); + +promise_test(async function(test) { + var buffer = new ArrayBuffer(textData.length); + + var body = new Response(textData).body; + const reader = body.getReader( {mode: 'byob'} ); + + let offset = 3; + while (offset < textData.length) { + const {done, value} = await reader.read(new Uint8Array(buffer, offset)); + if (done) { + break; + } + buffer = value.buffer; + offset += value.byteLength; + } + + validateBufferFromString(buffer, `\0\0\0\"This is response's bo`, 'Buffer should be validated'); +}, `Reading with offset from Response stream`); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume.html b/testing/web-platform/tests/fetch/api/response/response-consume.html new file mode 100644 index 0000000000..89fc49fd3c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume.html @@ -0,0 +1,317 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Response consume</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#response"> + <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/utils.js"></script> + </head> + <body> + <script> + function blobToFormDataResponse(name, blob) { + var formData = new FormData(); + formData.append(name, blob); + return new Response(formData); + } + + function readBlobAsArrayBuffer(blob) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result); + }; + reader.onerror = function(evt) { + reject("Blob's reader failed"); + }; + reader.readAsArrayBuffer(blob); + }); + } + + function blobToTypeViaFetch(blob) { + var url = URL.createObjectURL(blob); + return fetch(url).then(function(response) { + return response.headers.get('Content-Type'); + }); + } + + function responsePromise(body, responseInit) { + return new Promise(function(resolve, reject) { + resolve(new Response(body, responseInit)); + }); + } + + function responseStringToMultipartFormTextData(response, name, value) { + assert_true(response.headers.has("Content-Type"), "Response contains Content-Type header"); + var boundaryMatches = response.headers.get("Content-Type").match(/;\s*boundary=("?)([^";\s]*)\1/); + assert_true(!!boundaryMatches, "Response contains boundary parameter"); + return stringToMultipartFormTextData(boundaryMatches[2], name, value); + } + + function streamResponsePromise(streamData, responseInit) { + return new Promise(function(resolve, reject) { + var stream = new ReadableStream({ + start: function(controller) { + controller.enqueue(stringToArray(streamData)); + controller.close(); + } + }); + resolve(new Response(stream, responseInit)); + }); + } + + function stringToMultipartFormTextData(multipartBoundary, name, value) { + return ('--' + multipartBoundary + '\r\n' + + 'Content-Disposition: form-data;name="' + name + '"\r\n' + + '\r\n' + + value + '\r\n' + + '--' + multipartBoundary + '--\r\n'); + } + + function checkBodyText(test, response, expectedBody) { + return response.text().then( function(bodyAsText) { + assert_equals(bodyAsText, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as text: bodyUsed turned true"); + }); + } + + function checkBodyBlob(test, response, expectedBody, expectedType) { + return response.blob().then(function(bodyAsBlob) { + assert_equals(bodyAsBlob.type, expectedType || "text/plain", "Blob body type should be computed from the response Content-Type"); + + var promise = blobToTypeViaFetch(bodyAsBlob).then(function(type) { + assert_equals(type, expectedType || "text/plain", 'Type via blob URL'); + return new Promise( function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function () { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + }); + return promise.then(function(body) { + assert_equals(body, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as blob: bodyUsed turned true"); + }); + }); + } + + function checkBodyArrayBuffer(test, response, expectedBody) { + return response.arrayBuffer().then( function(bodyAsArrayBuffer) { + validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); + }); + } + + function checkBodyJSON(test, response, expectedBody) { + return response.json().then(function(bodyAsJSON) { + var strBody = JSON.stringify(bodyAsJSON) + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as json: bodyUsed turned true"); + }); + } + + function checkBodyFormDataMultipart(test, response, expectedBody) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + var entryName = "name"; + var strBody = responseStringToMultipartFormTextData(response, entryName, bodyAsFormData.get(entryName)); + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkBodyFormDataUrlencoded(test, response, expectedBody) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + var entryName = "name"; + var strBody = entryName + "=" + bodyAsFormData.get(entryName); + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkBodyFormDataError(test, response, expectedBody) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkResponseBody(responsePromise, expectedBody, checkFunction, bodyTypes) { + promise_test(function(test) { + return responsePromise.then(function(response) { + assert_false(response.bodyUsed, "bodyUsed is false at init"); + return checkFunction(test, response, expectedBody); + }); + }, "Consume response's body: " + bodyTypes); + } + + var textData = JSON.stringify("This is response's body"); + var textResponseInit = { "headers": [["Content-Type", "text/PLAIN"]] }; + var blob = new Blob([textData], { "type": "application/octet-stream" }); + var multipartBoundary = "boundary-" + Math.random(); + var formData = new FormData(); + var formTextResponseInit = { "headers": [["Content-Type", 'multipart/FORM-data; boundary="' + multipartBoundary + '"']] }; + var formTextData = stringToMultipartFormTextData(multipartBoundary, "name", textData); + var formBlob = new Blob([formTextData]); + var urlSearchParamsData = "name=value"; + var urlSearchParams = new URLSearchParams(urlSearchParamsData); + var urlSearchParamsType = "application/x-www-form-urlencoded;charset=UTF-8"; + var urlSearchParamsResponseInit = { "headers": [["Content-Type", urlSearchParamsType]] }; + var urlSearchParamsBlob = new Blob([urlSearchParamsData], { "type": urlSearchParamsType }); + formData.append("name", textData); + + // https://fetch.spec.whatwg.org/#concept-body-package-data + // "UTF-8 decoded without BOM" is used for formData(), either in + // "multipart/form-data" and "application/x-www-form-urlencoded" cases, + // so BOMs in the values should be kept. + // (The "application/x-www-form-urlencoded" cases are tested in + // url/urlencoded-parser.any.js) + var textDataWithBom = "\uFEFFquick\uFEFFfox\uFEFF"; + var formTextDataWithBom = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom); + var formTextDataWithBomExpectedForMultipartFormData = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom); + + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyText, "from text to text"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyBlob, "from text to blob"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyArrayBuffer, "from text to arrayBuffer"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyJSON, "from text to json"); + checkResponseBody(responsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from text with correct multipart type to formData"); + checkResponseBody(responsePromise(formTextDataWithBom, formTextResponseInit), formTextDataWithBomExpectedForMultipartFormData, checkBodyFormDataMultipart, "from text with correct multipart type to formData with BOM"); + checkResponseBody(responsePromise(formTextData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct multipart type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from text with correct urlencoded type to formData"); + checkResponseBody(responsePromise(urlSearchParamsData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct urlencoded type to formData (error case)"); + + checkResponseBody(responsePromise(blob, textResponseInit), textData, checkBodyBlob, "from blob to blob"); + checkResponseBody(responsePromise(blob), textData, checkBodyText, "from blob to text"); + checkResponseBody(responsePromise(blob), textData, checkBodyArrayBuffer, "from blob to arrayBuffer"); + checkResponseBody(responsePromise(blob), textData, checkBodyJSON, "from blob to json"); + checkResponseBody(responsePromise(formBlob, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from blob with correct multipart type to formData"); + checkResponseBody(responsePromise(formBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct multipart type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParamsBlob, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from blob with correct urlencoded type to formData"); + checkResponseBody(responsePromise(urlSearchParamsBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct urlencoded type to formData (error case)"); + + function checkFormDataResponseBody(responsePromise, expectedName, expectedValue, checkFunction, bodyTypes) { + promise_test(function(test) { + return responsePromise.then(function(response) { + assert_false(response.bodyUsed, "bodyUsed is false at init"); + var expectedBody = responseStringToMultipartFormTextData(response, expectedName, expectedValue); + return Promise.resolve().then(function() { + if (checkFunction === checkBodyFormDataMultipart) + return expectedBody; + // Modify expectedBody to use the same spacing for + // Content-Disposition parameters as Response and FormData does. + var response2 = new Response(formData); + return response2.text().then(function(formDataAsText) { + var reName = /[ \t]*;[ \t]*name=/; + var nameMatches = formDataAsText.match(reName); + return expectedBody.replace(reName, nameMatches[0]); + }); + }).then(function(expectedBody) { + return checkFunction(test, response, expectedBody); + }); + }); + }, "Consume response's body: " + bodyTypes); + } + + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyFormDataMultipart, "from FormData to formData"); + checkResponseBody(responsePromise(formData, textResponseInit), undefined, checkBodyFormDataError, "from FormData without correct type to formData (error case)"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, response.headers.get('Content-Type').toLowerCase()); }, "from FormData to blob"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyText, "from FormData to text"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyArrayBuffer, "from FormData to arrayBuffer"); + + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyFormDataUrlencoded, "from URLSearchParams to formData"); + checkResponseBody(responsePromise(urlSearchParams, textResponseInit), urlSearchParamsData, checkBodyFormDataError, "from URLSearchParams without correct type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, "application/x-www-form-urlencoded;charset=utf-8"); }, "from URLSearchParams to blob"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyText, "from URLSearchParams to text"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyArrayBuffer, "from URLSearchParams to arrayBuffer"); + + checkResponseBody(streamResponsePromise(textData, textResponseInit), textData, checkBodyBlob, "from stream to blob"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyText, "from stream to text"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyArrayBuffer, "from stream to arrayBuffer"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyJSON, "from stream to json"); + checkResponseBody(streamResponsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from stream with correct multipart type to formData"); + checkResponseBody(streamResponsePromise(formTextData), formTextData, checkBodyFormDataError, "from stream without correct multipart type to formData (error case)"); + checkResponseBody(streamResponsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from stream with correct urlencoded type to formData"); + checkResponseBody(streamResponsePromise(urlSearchParamsData), urlSearchParamsData, checkBodyFormDataError, "from stream without correct urlencoded type to formData (error case)"); + + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyBlob, "from fetch to blob"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyText, "from fetch to text"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyArrayBuffer, "from fetch to arrayBuffer"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyFormDataError, "from fetch without correct type to formData (error case)"); + + promise_test(function(test) { + var response = new Response(new Blob([ + "--boundary\r\n", + "Content-Disposition: form-data; name=string\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "1\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-default-charset\r\n", + "Content-Type: text/plain; charset=utf-8\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "2\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-non-default-charset\r\n", + "Content-Type: text/plain; charset=iso-8859-1\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "3\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-non-default-type\r\n", + "Content-Type: application/octet-stream\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "4\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=file; filename=file1\r\n", + "Content-Type: application/octet-stream; x-param=x-value\r\n", + "\r\n", new Uint8Array([5, 0x0, 0xFF]), "\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=\"file-without-type\"; filename=\"file2\"\r\n", + "\r\n", new Uint8Array([6, 0x0, 0x7F, 0xFF]), "\r\n", + "--boundary--\r\n" + ]), { "headers": [["Content-Type", 'multipart/form-data; boundary="boundary"']] }); + return response.formData().then(function(bodyAsFormData) { + // Non-file parts must always be decoded using utf-8 encoding. + assert_equals(bodyAsFormData.get("string"), "value\u00A01", "Retrieve and verify response's 1st entry value"); + assert_equals(bodyAsFormData.get("string-with-default-charset"), "value\u00A02", "Retrieve and verify response's 2nd entry value"); + assert_equals(bodyAsFormData.get("string-with-non-default-charset"), "value\u00A03", "Retrieve and verify response's 3rd entry value"); + assert_equals(bodyAsFormData.get("string-with-non-default-type"), "value\u00A04", "Retrieve and verify response's 4th entry value"); + // The name of a File must be taken from the filename parameter in + // the Content-Disposition header field. + assert_equals(bodyAsFormData.get("file").name, "file1", "Retrieve and verify response's 5th entry name property"); + assert_equals(bodyAsFormData.get("file-without-type").name, "file2", "Retrieve and verify response's 6th entry name property"); + // The type of a File must be taken from the Content-Type header field + // which defaults to "text/plain". + assert_equals(bodyAsFormData.get("file").type, "application/octet-stream; x-param=x-value", "Retrieve and verify response's 5th entry type property"); + assert_equals(bodyAsFormData.get("file-without-type").type, "text/plain", "Retrieve and verify response's 6th entry type property"); + + return Promise.resolve().then(function() { + return blobToFormDataResponse("file", bodyAsFormData.get("file")).text().then(function(bodyAsText) { + // Verify that filename, name and type are preserved. + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file1\2[;\r]/i, "Retrieve and verify response's 5th entry filename parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file\2[;\r]/i, "Retrieve and verify response's 5th entry name parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Type: *application\/octet-stream; x-param=x-value\r\n/i, "Retrieve and verify response's 5th entry type field"); + // Verify that the content is preserved. + return readBlobAsArrayBuffer(bodyAsFormData.get("file")).then(function(arrayBuffer) { + assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([5, 0x0, 0xFF]), "Retrieve and verify response's 5th entry content"); + }); + }); + }).then(function() { + return blobToFormDataResponse("file-without-type", bodyAsFormData.get("file-without-type")).text().then(function(bodyAsText) { + // Verify that filename, name and type are preserved. + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file2\2[;\r]/i, "Retrieve and verify response's 6th entry filename parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file-without-type\2[;\r]/i, "Retrieve and verify response's 6th entry name parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Type: *text\/plain\r\n/i, "Retrieve and verify response's 6th entry type field"); + // Verify that the content is preserved. + return readBlobAsArrayBuffer(bodyAsFormData.get("file-without-type")).then(function(arrayBuffer) { + assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([6, 0x0, 0x7F, 0xFF]), "Retrieve and verify response's 6th entry content"); + }); + }); + }); + }); + }, "Consume response's body: from multipart form data blob to formData"); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js new file mode 100644 index 0000000000..118eb7d5cb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker +// META: title=Response Receives Propagated Error from ReadableStream + +function newStreamWithStartError() { + var err = new Error("Start error"); + return [new ReadableStream({ + start(controller) { + controller.error(err); + } + }), + err] +} + +function newStreamWithPullError() { + var err = new Error("Pull error"); + return [new ReadableStream({ + pull(controller) { + controller.error(err); + } + }), + err] +} + +function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) { + promise_test(test => { + return promise_rejects_exactly( + test, + err, + new Response(stream)[responseReaderMethod](), + 'CustomTestError should propagate' + ) + }, testDescription) +} + + +promise_test(test => { + var [stream, err] = newStreamWithStartError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error") + +promise_test(test => { + var [stream, err] = newStreamWithPullError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error") + + +// test start() errors for all Body reader methods +runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise'); + +// test pull() errors for all Body reader methods +runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise'); diff --git a/testing/web-platform/tests/fetch/api/response/response-error.any.js b/testing/web-platform/tests/fetch/api/response/response-error.any.js new file mode 100644 index 0000000000..a76bc43802 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-error.any.js @@ -0,0 +1,27 @@ +// META: global=window,worker +// META: title=Response error + +var invalidStatus = [0, 100, 199, 600, 1000]; +invalidStatus.forEach(function(status) { + test(function() { + assert_throws_js(RangeError, function() { new Response("", { "status" : status }); }, + "Expect RangeError exception when status is " + status); + },"Throws RangeError when responseInit's status is " + status); +}); + +var invalidStatusText = ["\n", "Ā"]; +invalidStatusText.forEach(function(statusText) { + test(function() { + assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); }, + "Expect TypeError exception " + statusText); + },"Throws TypeError when responseInit's statusText is " + statusText); +}); + +var nullBodyStatus = [204, 205, 304]; +nullBodyStatus.forEach(function(status) { + test(function() { + assert_throws_js(TypeError, + function() { new Response("body", {"status" : status }); }, + "Expect TypeError exception "); + },"Throws TypeError when building a response with body and a body status of " + status); +}); diff --git a/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js new file mode 100644 index 0000000000..ea5192bfb1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js @@ -0,0 +1,23 @@ +// META: global=window,worker + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + stream.getReader(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which getReader() is called"); + +test(() => { + const stream = new ReadableStream(); + stream.getReader().read(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() is called"); + +promise_test(async () => { + const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }), + reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() and releaseLock() are called"); diff --git a/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js new file mode 100644 index 0000000000..4a67d067a7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js @@ -0,0 +1,8 @@ +// META: global=window,worker +// META: title=Response: error static method + +promise_test (async () => { + const response = await fetch("../resources/data.json"); + assert_throws_js(TypeError, () => { response.headers.append("name", "value"); }); + assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable"); +}, "Ensure response headers are immutable"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-001.any.js b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js new file mode 100644 index 0000000000..559e49ad11 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +// META: title=Response init: simple cases + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "", + "body" : null +}; + +var statusCodes = { "givenValues" : [200, 300, 400, 500, 599], + "expectedValues" : [200, 300, 400, 500, 599] +}; +var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)], + "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)] +}; +var initValuesDict = { "status" : statusCodes, + "statusText" : statusTexts +}; + +function isOkStatus(status) { + return 200 <= status && 299 >= status; +} + +var response = new Response(); +for (var attributeName in defaultValues) { + test(function() { + var expectedValue = defaultValues[attributeName]; + assert_equals(response[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + }, "Check default value for " + attributeName + " attribute"); +} + +for (var attributeName in initValuesDict) { + test(function() { + var valuesToTest = initValuesDict[attributeName]; + for (var valueIdx in valuesToTest["givenValues"]) { + var givenValue = valuesToTest["givenValues"][valueIdx]; + var expectedValue = valuesToTest["expectedValues"][valueIdx]; + var responseInit = {}; + responseInit[attributeName] = givenValue; + var response = new Response("", responseInit); + assert_equals(response[attributeName], expectedValue, + "Expect response." + attributeName + " is " + expectedValue + + " when initialized with " + givenValue); + assert_equals(response.ok, isOkStatus(response.status), + "Expect response.ok is " + isOkStatus(response.status)); + } + }, "Check " + attributeName + " init values and associated getter"); +} + +test(function() { + const response1 = new Response(""); + assert_equals(response1.headers, response1.headers); + + const response2 = new Response("", {"headers": {"X-Foo": "bar"}}); + assert_equals(response2.headers, response2.headers); + const headers = response2.headers; + response2.headers.set("X-Foo", "quux"); + assert_equals(headers, response2.headers); + headers.set("X-Other-Header", "baz"); + assert_equals(headers, response2.headers); +}, "Test that Response.headers has the [SameObject] extended attribute"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-002.any.js b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js new file mode 100644 index 0000000000..6c0a46e480 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Response init: body and headers +// META: script=../resources/utils.js + +test(function() { + var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3" + }; + var headers = new Headers(headerDict); + var response = new Response("", { "headers" : headers }) + for (var name in headerDict) { + assert_equals(response.headers.get(name), headerDict[name], + "response's headers has " + name + " : " + headerDict[name]); + } +}, "Initialize Response with headers values"); + +function checkResponseInit(body, bodyType, expectedTextBody) { + promise_test(function(test) { + var response = new Response(body); + var resHeaders = response.headers; + var mime = resHeaders.get("Content-Type"); + assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" "); + return response.text().then(function(bodyAsText) { + //not equals: cannot guess formData exact value + assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body"); + }); + }, "Initialize Response's body with " + bodyType); +} + +var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); +var formaData = new FormData(); +formaData.append("name", "value"); +var urlSearchParams = "URLSearchParams are not supported"; +//avoid test timeout if not implemented +if (self.URLSearchParams) + urlSearchParams = new URLSearchParams("name=value"); +var usvString = "This is a USVString" + +checkResponseInit(blob, "application/octet-binary", "This is a blob"); +checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); +checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); +checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); + +promise_test(function(test) { + var body = "This is response body"; + var response = new Response(body); + return validateStreamFromString(response.body.getReader(), body); +}, "Read Response's body as readableStream"); + +promise_test(function(test) { + var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]}); + return response.blob().then(function(blob) { + assert_equals(blob.type, "", "Blob type should be the empty string"); + }); +}, "Testing empty Response Content-Type header"); + +test(function() { + var response = new Response(null, {status: 204}); + assert_equals(response.body, null); +}, "Testing null Response body"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js new file mode 100644 index 0000000000..3a7744c287 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js @@ -0,0 +1,125 @@ +test(() => { + const response = new Response(); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with empty body"); + +test(() => { + const blob = new Blob([]); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), "a/b; c=d"); +}, "Default Content-Type for Response with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const response = new Response(buffer); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with buffer source body"); + +promise_test(async () => { + const formData = new FormData(); + formData.append("a", "b"); + const response = new Response(formData); + const boundary = (await response.text()).split("\r\n")[0].slice(2); + assert_equals( + response.headers.get("Content-Type"), + `multipart/form-data; boundary=${boundary}`, + ); +}, "Default Content-Type for Response with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const response = new Response(usp); + assert_equals( + response.headers.get("Content-Type"), + "application/x-www-form-urlencoded;charset=UTF-8", + ); +}, "Default Content-Type for Response with URLSearchParams body"); + +test(() => { + const response = new Response(""); + assert_equals( + response.headers.get("Content-Type"), + "text/plain;charset=UTF-8", + ); +}, "Default Content-Type for Response with string body"); + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with ReadableStream body"); + +// ----------------------------------------------------------------------------- + +const OVERRIDE_MIME = "test/only; mime=type"; + +function responseWithOverrideMime(body) { + return new Response( + body, + { headers: { "Content-Type": OVERRIDE_MIME } }, + ); +} + +test(() => { + const response = responseWithOverrideMime(undefined); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with empty body"); + +test(() => { + const blob = new Blob([]); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const response = responseWithOverrideMime(buffer); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with buffer source body"); + +test(() => { + const formData = new FormData(); + const response = responseWithOverrideMime(formData); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const response = responseWithOverrideMime(usp); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with URLSearchParams body"); + +test(() => { + const response = responseWithOverrideMime(""); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with string body"); + +test(() => { + const stream = new ReadableStream(); + const response = responseWithOverrideMime(stream); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with ReadableStream body"); diff --git a/testing/web-platform/tests/fetch/api/response/response-static-error.any.js b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js new file mode 100644 index 0000000000..4097eab37b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: title=Response: error static method + +test(function() { + var responseError = Response.error(); + assert_equals(responseError.type, "error", "Network error response's type is error"); + assert_equals(responseError.status, 0, "Network error response's status is 0"); + assert_equals(responseError.statusText, "", "Network error response's statusText is empty"); + assert_equals(responseError.body, null, "Network error response's body is null"); + + assert_true(responseError.headers.entries().next().done, "Headers should be empty"); +}, "Check response returned by static method error()"); + +test(function() { + const headers = Response.error().headers; + + // Avoid false positives if expected API is not available + assert_true(!!headers); + assert_equals(typeof headers.append, 'function'); + + assert_throws_js(TypeError, function () { headers.append('name', 'value'); }); +}, "the 'guard' of the Headers instance should be immutable"); diff --git a/testing/web-platform/tests/fetch/api/response/response-static-json.any.js b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js new file mode 100644 index 0000000000..5ec79e69aa --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js @@ -0,0 +1,96 @@ +// META: global=window,worker +// META: title=Response: json static method + +const APPLICATION_JSON = "application/json"; +const FOO_BAR = "foo/bar"; + +const INIT_TESTS = [ + [undefined, 200, "", APPLICATION_JSON, {}], + [{ status: 400 }, 400, "", APPLICATION_JSON, {}], + [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}], + [{ headers: {} }, 200, "", APPLICATION_JSON, {}], + [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}], + [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }], +]; + +for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) { + promise_test(async function () { + const response = Response.json("hello world", init); + assert_equals(response.type, "default", "Response's type is default"); + assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus); + assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText)); + assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType); + for (const key in expectedHeaders) { + assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key])); + } + + const data = await response.json(); + assert_equals(data, "hello world", "Response's body is 'hello world'"); + }, `Check response returned by static json() with init ${JSON.stringify(init)}`); +} + +const nullBodyStatus = [204, 205, 304]; +for (const status of nullBodyStatus) { + test(function () { + assert_throws_js( + TypeError, + function () { + Response.json("hello world", { status: status }); + }, + ); + }, `Throws TypeError when calling static json() with a status of ${status}`); +} + +promise_test(async function () { + const response = Response.json({ foo: "bar" }); + const data = await response.json(); + assert_equals(typeof data, "object", "Response's json body is an object"); + assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }"); +}, "Check static json() encodes JSON objects correctly"); + +test(function () { + assert_throws_js( + TypeError, + function () { + Response.json(Symbol("foo")); + }, + ); +}, "Check static json() throws when data is not encodable"); + +test(function () { + const a = { b: 1 }; + a.a = a; + assert_throws_js( + TypeError, + function () { + Response.json(a); + }, + ); +}, "Check static json() throws when data is circular"); + +promise_test(async function () { + class CustomError extends Error { + name = "CustomError"; + } + assert_throws_js( + CustomError, + function () { + Response.json({ get foo() { throw new CustomError("bar") }}); + } + ) +}, "Check static json() propagates JSON serializer errors"); + +const encodingChecks = [ + ["𝌆", [34, 240, 157, 140, 134, 34]], + ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]], + ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]], +]; + +for (const [input, expected] of encodingChecks) { + promise_test(async function () { + const response = Response.json(input); + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + assert_array_equals(data, expected); + }, `Check response returned by static json() with input ${input}`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js new file mode 100644 index 0000000000..b16c56d830 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js @@ -0,0 +1,40 @@ +// META: global=window,worker +// META: title=Response: redirect static method + +var url = "http://test.url:1234/"; +test(function() { + const redirectResponse = Response.redirect(url); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, 302, "Default redirect status is 302"); + assert_equals(redirectResponse.headers.get("Location"), url, + "redirected response has Location header with the correct url"); + assert_equals(redirectResponse.statusText, ""); +}, "Check default redirect response"); + +[301, 302, 303, 307, 308].forEach(function(status) { + test(function() { + const redirectResponse = Response.redirect(url, status); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, status, "Redirect status is " + status); + assert_equals(redirectResponse.headers.get("Location"), url); + assert_equals(redirectResponse.statusText, ""); + }, "Check response returned by static method redirect(), status = " + status); +}); + +test(function() { + var invalidUrl = "http://:This is not an url"; + assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); }, + "Expect TypeError exception"); +}, "Check error returned when giving invalid url to redirect()"); + +var invalidRedirectStatus = [200, 309, 400, 500]; +invalidRedirectStatus.forEach(function(invalidStatus) { + test(function() { + assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); }, + "Expect RangeError exception"); + }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus); +}); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js new file mode 100644 index 0000000000..d3d92e1677 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: title=Response causes TypeError from bad chunk type + +function runChunkTest(responseReaderMethod, testDescription) { + promise_test(test => { + let stream = new ReadableStream({ + start(controller) { + controller.enqueue("not Uint8Array"); + controller.close(); + } + }); + + return promise_rejects_js(test, TypeError, + new Response(stream)[responseReaderMethod](), + 'TypeError should propagate' + ) + }, testDescription) +} + +runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError'); +runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError'); +runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError'); +runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError'); +runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError'); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js new file mode 100644 index 0000000000..64f65f16f2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js @@ -0,0 +1,44 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + const reader = response.body.getReader(); + reader.releaseLock(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.blob().then(function(blob) { + assert_true(blob instanceof Blob); + }); + }); + }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.text().then(function(text) { + assert_true(text.length > 0); + }); + }); + }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.json().then(function(json) { + assert_equals(typeof json, "object"); + }); + }); + }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.arrayBuffer().then(function(arrayBuffer) { + assert_true(arrayBuffer.byteLength > 0); + }); + }); + }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js new file mode 100644 index 0000000000..c46a180a18 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js @@ -0,0 +1,35 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithLockedReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + response.body.getReader(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js new file mode 100644 index 0000000000..35fb086469 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js @@ -0,0 +1,36 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithDisturbedReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + const reader = response.body.getReader(); + reader.read(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js new file mode 100644 index 0000000000..490672febd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js @@ -0,0 +1,35 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithCancelledReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + response.body.cancel(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js new file mode 100644 index 0000000000..348fc39383 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js @@ -0,0 +1,19 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +for (const bodySource of ["fetch", "stream", "string"]) { + for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) { + promise_test( + async () => { + const response = await responseFromBodySource(bodySource); + response[consumeAs](); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function () { + response.body.getReader(); + }); + }, + `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`, + ); + } +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js new file mode 100644 index 0000000000..61d8544f07 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js @@ -0,0 +1,76 @@ +// META: global=window,worker +// META: title=ReadableStream disturbed tests, via Response's bodyUsed property + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A non-closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel(); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "A non-closed stream on which cancel() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.close(); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "An errored stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "An errored stream on which cancel() has been called"); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js new file mode 100644 index 0000000000..5341b75271 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +test(() => { + const r = new Response(new ReadableStream()); + // highWaterMark: 0 means that nothing will actually be read from the body. + r.body.pipeTo(new WritableStream({}, {highWaterMark: 0})); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeTo on Response body should disturb it synchronously'); + +test(() => { + const r = new Response(new ReadableStream()); + r.body.pipeThrough({ + writable: new WritableStream({}, {highWaterMark: 0}), + readable: new ReadableStream() + }); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeThrough on Response body should disturb it synchronously'); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js new file mode 100644 index 0000000000..50bb586aa0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js @@ -0,0 +1,17 @@ +const BODY = '{"key": "value"}'; + +function responseFromBodySource(bodySource) { + if (bodySource === "fetch") { + return fetch("../resources/data.json"); + } else if (bodySource === "stream") { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(BODY)); + controller.close(); + }, + }); + return new Response(stream); + } else { + return new Response(BODY); + } +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js new file mode 100644 index 0000000000..8fef66c8a2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js @@ -0,0 +1,117 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(async () => { + // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so + // these tests use add_completion_callback for cleanup instead. + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: undefined}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject value: undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(undefined); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(8.2); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject 8.2 via Object.prototype.then.'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const resp = new Response(hello); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' + + 'should not be possible'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const u8a123 = new Uint8Array([1, 2, 3]); + const u8a456 = new Uint8Array([4, 5, 6]); + const resp = new Response(u8a123); + const writtenBytes = []; + const ws = new WritableStream({ + write(chunk) { + writtenBytes.push(...Array.from(chunk)); + } + }); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: u8a456}); + }; + await resp.body.pipeTo(ws); + delete Object.prototype.then; + assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]'); +}, 'intercepting arraybuffer to body readable stream conversion via ' + + 'Object.prototype.then should not be possible'); diff --git a/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html b/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html new file mode 100644 index 0000000000..60a784cd84 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html @@ -0,0 +1,264 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Connection partitioning by site</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/utils.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<!-- Used to open about:blank tabs from opaque origins --> +<iframe id="iframe0" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe> +<iframe id="iframe1" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe> +<script> +const host = get_host_info(); + +// These two origins must correspond to different sites for this test to pass. +const POPUP_ORIGINS = [ + host.ORIGIN, + host.HTTP_NOTSAMESITE_ORIGIN +]; + +// This origin should ideally correspond to a different site from the two above, but the +// tests will still pass if it matches the site of one of the other two origins. +const OTHER_ORIGIN = host.REMOTE_ORIGIN; + +// Except for the csp_sandbox and about:blanks, each test opens up two windows, one at +// POPUP_ORIGINS[0], one at POPUP_ORIGINS[1], and has them request subresources from +// subresource_origin. All requests (HTML, JS, and fetch requests) for each window go +// through network-partition-key.py and have a partition_id parameter, which is used +// to check if any request for one window uses the same socket as a request for the +// other window. +// +// Whenever requests from the two different popup windows use the same connection, the +// fetch requests all start returning 400 errors, but other requests will continue to +// succeed, to make for clearer errors. +// +// include_credentials indicates whether the fetch requests use credentials or not, +// which is interesting as uncredentialed sockets have separate connection pools. +const tests = [ + { + name: 'With credentials', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Without credentials', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: false, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Cross-site resources with credentials', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Cross-site resources without credentials', + subresource_origin: OTHER_ORIGIN, + include_credentials: false, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Iframes', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + { + type: 'iframe', + iframe_origin: OTHER_ORIGIN + }, + { + type: 'iframe', + iframe_origin: OTHER_ORIGIN + } + ] + }, + { + name: 'Workers', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'worker'}, + {type: 'worker'} + ] + }, + { + name: 'Workers with cross-site resources', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'worker'}, + {type: 'worker'} + ] + }, + { + name: 'CSP sandbox', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'csp_sandbox'}, + {type: 'csp_sandbox'} + ] + }, + { + name: 'about:blank from opaque origin iframe', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'opaque_about_blank'}, + {type: 'opaque_about_blank'} + ] + }, +]; + +const BASE_PATH = window.location.pathname.replace(/\/[^\/]*$/, '/'); + +function create_script_url(origin, uuid, partition_id, dispatch) { + return `${origin}${BASE_PATH}resources/network-partition-key.py?uuid=${uuid}&partition_id=${partition_id}&dispatch=${dispatch}` +} + +function run_test(test) { + var uuid = token(); + + // Used to track the opened popup windows, so they can be closed at the end of the test. + // They could be closed immediately after use, but safest to keep them open, as browsers + // could use closing a window as a hint to close idle sockets that the window used. + var popup_windows = []; + + // Creates a popup window at |url| and waits for a test result. Returns a promise. + function create_popup_and_wait_for_result(url) { + return new Promise(function(resolve, reject) { + popup_windows.push(window.open(url)); + // Listen for the result + function message_listener(event) { + if (event.data.result === 'success') { + resolve(); + } else if (event.data.result === 'error') { + reject(event.data.details); + } else { + reject('Unexpected message.'); + } + } + window.addEventListener('message', message_listener, {once: 'true'}); + }); + } + + // Navigates iframe to url and waits for a test result. Returns a promise. + function navigate_iframe_and_wait_for_result(iframe, url) { + return new Promise(function(resolve, reject) { + iframe.src = url; + // Listen for the result + function message_listener(event) { + if (event.data.result === 'success') { + resolve(); + } else if (event.data.result === 'error') { + reject(event.data.details); + } else { + reject('Unexpected message.'); + } + } + window.addEventListener('message', message_listener, {once: 'true'}); + }); + } + + function make_test_function(test, index) { + var popup_params = test.popup_params[index]; + return function() { + var popup_path; + var additional_url_params = ''; + var origin = POPUP_ORIGINS[index]; + var partition_id = POPUP_ORIGINS[index]; + if (popup_params.type == 'main_frame') { + popup_path = 'resources/network-partition-checker.html'; + } else if (popup_params.type == 'iframe') { + popup_path = 'resources/network-partition-iframe-checker.html'; + additional_url_params = `&other_origin=${popup_params.iframe_origin}`; + } else if (popup_params.type == 'worker') { + popup_path = 'resources/network-partition-worker-checker.html'; + // The origin of the dedicated worker must mutch the page that loads it. + additional_url_params = `&other_origin=${POPUP_ORIGINS[index]}`; + } else if (popup_params.type == 'csp_sandbox') { + // For the Content-Security-Policy sandbox test, all requests are from the same origin, but + // the origin should be treated as an opaque origin, so sockets should not be reused. + origin = test.subresource_origin; + partition_id = index; + popup_path = 'resources/network-partition-checker.html'; + // Don't check partition of root document, since the document isn't sandboxed until the + // root document is fetched. + additional_url_params = '&sandbox=true&nocheck_partition=true' + } else if (popup_params.type=='opaque_about_blank') { + popup_path = 'resources/network-partition-about-blank-checker.html'; + } else if (popup_params.type == 'iframe') { + throw 'Unrecognized popup_params.type.'; + } + var url = create_script_url(origin, uuid, partition_id, 'fetch_file'); + url += `&subresource_origin=${test.subresource_origin}` + url += `&include_credentials=${test.include_credentials}` + url += `&path=${BASE_PATH.substring(1)}${popup_path}`; + url += additional_url_params; + + if (popup_params.type=='opaque_about_blank') { + return navigate_iframe_and_wait_for_result(iframe = document.getElementById('iframe' + index), url); + } + + return create_popup_and_wait_for_result(url); + } + } + + // Takes a Promise, and cleans up state when the promise has completed, successfully or not, re-throwing + // any exception from the passed in Promise. + async function clean_up_when_done(promise) { + var error; + try { + await promise; + } catch (e) { + error = e; + } + + popup_windows.map(function (win) { win.close(); }); + + try { + var cleanup_url = create_script_url(host.ORIGIN, uuid, host.ORIGIN, 'clean_up'); + var response = await fetch(cleanup_url, {credentials: 'omit', mode: 'cors'}); + assert_equals(await response.text(), 'cleanup complete', `Sever state cleanup failed`); + } catch (e) { + // Prefer error from the passed in Promise over errors from the fetch request to clean up server state. + error = error || e; + } + if (error) + throw error; + } + + return clean_up_when_done( + make_test_function(test, 0)() + .then(make_test_function(test, 1))); +} + +tests.forEach(function (test) { + promise_test( + function() { return run_test(test); }, + test.name); +}) + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html new file mode 100644 index 0000000000..7a8b613237 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>about:blank Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> +</head> +<body> +<script> + async function fetch_and_reply() { + // Load about:blank in a new tab, and inject the network partition checking code into it. + var win; + try { + win = window.open(); + var url = 'SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html&sandbox=true'; + var response = await fetch(url, {credentials: 'omit', mode: 'cors'}); + win.document.write(await response.text()); + } catch (e) { + win.close(); + window.parent.postMessage({result: 'error', details: e.message}, '*'); + return; + } + + // Listen for first message from the new window and pass it back to the parent. + function message_listener(event) { + window.parent.postMessage(event.data, '*'); + win.close(); + } + window.addEventListener('message', message_listener, {once: true}); + } + fetch_and_reply(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html new file mode 100644 index 0000000000..b058f61124 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html @@ -0,0 +1,30 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + async function fetch_and_reply() { + // If this is a top level window, report to the opener. Otherwise, this is an iframe, + // so report to the parent. + var report_to = window.opener; + if (!report_to) + report_to = window.parent; + try { + await check_partition_ids(); + report_to.postMessage({result: 'success'}, '*'); + } catch (e) { + report_to.postMessage({result: 'error', details: e.message}, '*'); + } + } + fetch_and_reply(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html new file mode 100644 index 0000000000..f76ed18447 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Iframe Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + // Listen for first message from the iframe, and pass it back to the opener. + function message_listener(event) { + window.opener.postMessage(event.data, '*'); + } + window.addEventListener('message', message_listener, {once: 'true'}); +</script> +<iframe src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js new file mode 100644 index 0000000000..bd66109380 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js @@ -0,0 +1,47 @@ +// Runs multiple fetches that validate connections see only a single partition_id. +// Requests are run in parallel so that they use multiple connections to maximize the +// chance of exercising all matching connections in the connection pool. Only returns +// once all requests have completed to make cleaning up server state non-racy. +function check_partition_ids(location) { + const NUM_FETCHES = 20; + + var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition'; + + // Not a perfect parse of the query string, but good enough for this test. + var include_credentials = base_url.search('include_credentials=true') != -1; + var exclude_credentials = base_url.search('include_credentials=false') != -1; + if (include_credentials != !exclude_credentials) + throw new Exception('Credentials mode not specified'); + + + // Run NUM_FETCHES in parallel. + var fetches = []; + for (i = 0; i < NUM_FETCHES; ++i) { + var fetch_params = { + credentials: 'omit', + mode: 'cors', + headers: { + 'Header-To-Force-CORS': 'cors' + }, + }; + + // Use a unique URL for each request, in case the caching layer serializes multiple + // requests for the same URL. + var url = `${base_url}&${token()}`; + + fetches.push(fetch(url, fetch_params).then( + function (response) { + return response.text().then(function(text) { + assert_equals(text, 'ok', `Socket unexpectedly reused`); + }); + })); + } + + // Wait for all promises to complete. + return Promise.allSettled(fetches).then(function (results) { + results.forEach(function (result) { + if (result.status != 'fulfilled') + throw result.reason; + }); + }); +} diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py new file mode 100644 index 0000000000..32fe4999b7 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py @@ -0,0 +1,130 @@ +import mimetypes +import os + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +# Test server that tracks the last partition_id was used with each connection for each uuid, and +# lets consumers query if multiple different partition_ids have been been used for any socket. +# +# Server assumes that ports aren't reused, so a client address and a server port uniquely identify +# a connection. If that constraint is ever violated, the test will be flaky. No sockets being +# closed for the duration of the test is sufficient to ensure that, though even if sockets are +# closed, the OS should generally prefer to use new ports for new connections, if any are +# available. +def main(request, response): + response.headers.set(b"Cache-Control", b"no-store") + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + partition_id = request.GET.first(b"partition_id", None) + + if not uuid or not dispatch or not partition_id: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + # Unless nocheck_partition is true, check partition_id against server_state, and update server_state. + stash = request.server.stash + test_failed = False + request_count = 0; + connection_count = 0; + if request.GET.first(b"nocheck_partition", None) != b"True": + # Need to grab the lock to access the Stash, since requests are made in parallel. + with stash.lock: + # Don't use server hostname here, since H2 allows multiple hosts to reuse a connection. + # Server IP is not currently available, unfortunately. + address_key = isomorphic_encode(str(request.client_address) + u"|" + str(request.url_parts.port)) + server_state = stash.take(uuid) or {b"test_failed": False, + b"request_count": 0, b"connection_count": 0} + request_count = server_state[b"request_count"] + request_count += 1 + server_state[b"request_count"] = request_count + if address_key in server_state: + if server_state[address_key] != partition_id: + server_state[b"test_failed"] = True + else: + connection_count = server_state[b"connection_count"] + connection_count += 1 + server_state[b"connection_count"] = connection_count + server_state[address_key] = partition_id + test_failed = server_state[b"test_failed"] + stash.put(uuid, server_state) + + origin = request.headers.get(b"Origin") + if origin: + response.headers.set(b"Access-Control-Allow-Origin", origin) + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + + if request.method == u"OPTIONS": + return handle_preflight(request, response) + + if dispatch == b"fetch_file": + return handle_fetch_file(request, response, partition_id, uuid) + + if dispatch == b"check_partition": + status = request.GET.first(b"status", 200) + if test_failed: + return simple_response(request, response, status, b"OK", b"Multiple partition IDs used on a socket") + body = b"ok" + if request.GET.first(b"addcounter", False): + body += (". Request was sent " + str(request_count) + " times. " + + str(connection_count) + " connections were created.").encode('utf-8') + return simple_response(request, response, status, b"OK", body) + + if dispatch == b"clean_up": + stash.take(uuid) + if test_failed: + return simple_response(request, response, 200, b"OK", b"Test failed, but cleanup completed.") + return simple_response(request, response, 200, b"OK", b"cleanup complete") + + return simple_response(request, response, 404, b"Not Found", b"Unrecognized dispatch parameter: " + dispatch) + +def handle_preflight(request, response): + response.status = (200, b"OK") + response.headers.set(b"Access-Control-Allow-Methods", b"GET") + response.headers.set(b"Access-Control-Allow-Headers", b"header-to-force-cors") + response.headers.set(b"Access-Control-Max-Age", b"86400") + return b"Preflight request" + +def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"): + response.status = (status_code, status_message) + response.headers.set(b"Content-Type", content_type) + return body + +def handle_fetch_file(request, response, partition_id, uuid): + subresource_origin = request.GET.first(b"subresource_origin", None) + rel_path = request.GET.first(b"path", None) + + # This needs to be passed on to subresources so they all have access to it. + include_credentials = request.GET.first(b"include_credentials", None) + if not subresource_origin or not rel_path or not include_credentials: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + cur_path = os.path.realpath(isomorphic_decode(__file__)) + base_path = os.path.abspath(os.path.join(os.path.dirname(cur_path), os.pardir, os.pardir, os.pardir)) + path = os.path.abspath(os.path.join(base_path, isomorphic_decode(rel_path))) + + # Basic security check. + if not path.startswith(base_path): + return simple_response(request, response, 404, b"Not found", b"Invalid path") + + sandbox = request.GET.first(b"sandbox", None) + if sandbox == b"true": + response.headers.set(b"Content-Security-Policy", b"sandbox allow-scripts") + + file = open(path, mode="rb") + body = file.read() + file.close() + + subresource_path = b"/" + isomorphic_encode(os.path.relpath(isomorphic_decode(__file__), base_path)).replace(b'\\', b'/') + subresource_params = b"?partition_id=" + partition_id + b"&uuid=" + uuid + b"&subresource_origin=" + subresource_origin + b"&include_credentials=" + include_credentials + body = body.replace(b"SUBRESOURCE_PREFIX:", subresource_origin + subresource_path + subresource_params) + + other_origin = request.GET.first(b"other_origin", None) + if other_origin: + body = body.replace(b"OTHER_PREFIX:", other_origin + subresource_path + subresource_params) + + mimetypes.init() + mimetype_pair = mimetypes.guess_type(path) + mimetype = mimetype_pair[0] + + if mimetype == None or mimetype_pair[1] != None: + return simple_response(request, response, 500, b"Server Error", b"Unknown MIME type") + return simple_response(request, response, 200, b"OK", body, mimetype) diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html new file mode 100644 index 0000000000..e6b7ea7673 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Worker Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + // Workers must be same origin as the page loading them, but it's simpler to reuse the + // OTHER_PREFIX mechanism in the Python code than to craft the URL in Javascript here. + var worker = new Worker('OTHER_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-worker.js'); + function message_listener(event) { + window.opener.postMessage(event.data, '*'); + worker.terminate(); + } + worker.addEventListener('message', message_listener); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js new file mode 100644 index 0000000000..1745edfacb --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js @@ -0,0 +1,15 @@ +// This tests the partition key of fetches to subresouce_origin made by the worker and +// imported scripts from subresource_origin. +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js'); + +async function fetch_and_reply() { + try { + await check_partition_ids(); + self.postMessage({result: 'success'}); + } catch (e) { + self.postMessage({result: 'error', details: e.message}); + } +} +fetch_and_reply(); diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/bad-gzip-body.any.js b/testing/web-platform/tests/fetch/content-encoding/gzip/bad-gzip-body.any.js new file mode 100644 index 0000000000..17bc1261a3 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/bad-gzip-body.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker + +promise_test((test) => { + return fetch("resources/bad-gzip-body.py").then(res => { + assert_equals(res.status, 200); + }); +}, "Fetching a resource with bad gzip content should still resolve"); + +[ + "arrayBuffer", + "blob", + "formData", + "json", + "text" +].forEach(method => { + promise_test(t => { + return fetch("resources/bad-gzip-body.py").then(res => { + assert_equals(res.status, 200); + return promise_rejects_js(t, TypeError, res[method]()); + }); + }, "Consuming the body of a resource with bad gzip content with " + method + "() should reject"); +}); diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/big-gzip-body.https.any.js b/testing/web-platform/tests/fetch/content-encoding/gzip/big-gzip-body.https.any.js new file mode 100644 index 0000000000..b5d62c9804 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/big-gzip-body.https.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker + +const EXPECTED_SIZE = 27000000; +const EXPECTED_SHA256 = [ + 74, 100, 37, 243, 147, 61, 116, 60, 241, 221, 126, + 18, 24, 71, 204, 28, 50, 62, 201, 130, 152, 225, + 217, 183, 10, 201, 143, 214, 102, 155, 212, 248, + ]; + +promise_test(async () => { + const response = await fetch('resources/big.text.gz'); + assert_true(response.ok); + const arrayBuffer = await response.arrayBuffer(); + assert_equals(arrayBuffer.byteLength, EXPECTED_SIZE, + 'uncompressed size should match'); + const sha256 = await crypto.subtle.digest('SHA-256', arrayBuffer); + assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, + 'digest should match'); +}, 'large gzip data should be decompressed successfully'); + +promise_test(async () => { + const response = await fetch('resources/big.text.gz'); + assert_true(response.ok); + const reader = response.body.getReader({mode: 'byob'}); + let offset = 0; + // Pre-allocate space for the output. The response body will be read + // chunk-by-chunk into this array. + let ab = new ArrayBuffer(EXPECTED_SIZE); + while (offset < EXPECTED_SIZE) { + // To stress the data pipe, we want to use a different size read each + // time. Unfortunately, JavaScript doesn't have a seeded random number + // generator, so this creates the possibility of making this test flaky if + // it doesn't work for some edge cases. + let size = Math.floor(Math.random() * 65535 + 1); + if (size + offset > EXPECTED_SIZE) { + size = EXPECTED_SIZE - offset; + } + const u8 = new Uint8Array(ab, offset, size); + const { value, done } = await reader.read(u8); + ab = value.buffer; + // Check that we got our original array back. + assert_equals(ab.byteLength, EXPECTED_SIZE, + 'backing array should be the same size'); + assert_equals(offset, value.byteOffset, 'offset should match'); + assert_less_than_equal(value.byteLength, size, + 'we should not have got more than we asked for'); + offset = value.byteOffset + value.byteLength; + if (done) break; + } + assert_equals(offset, EXPECTED_SIZE, + 'we should have read the whole thing'); + const sha256 = await crypto.subtle.digest('SHA-256', new Uint8Array(ab)); + assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, + 'digest should match'); +}, 'large gzip data should be decompressed successfully with byte stream'); diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/gzip-body.any.js b/testing/web-platform/tests/fetch/content-encoding/gzip/gzip-body.any.js new file mode 100644 index 0000000000..37758b7d91 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/gzip-body.any.js @@ -0,0 +1,16 @@ +// META: global=window,worker + +const expectedDecompressedSize = 10500; +[ + "text", + "octetstream" +].forEach(contentType => { + promise_test(async t => { + let response = await fetch(`resources/foo.${contentType}.gz`); + assert_true(response.ok); + let arrayBuffer = await response.arrayBuffer() + let u8 = new Uint8Array(arrayBuffer); + assert_equals(u8.length, expectedDecompressedSize); + }, `fetched gzip data with content type ${contentType} should be decompressed.`); +}); + diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/bad-gzip-body.py b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/bad-gzip-body.py new file mode 100644 index 0000000000..a79b94ed04 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/bad-gzip-body.py @@ -0,0 +1,3 @@ +def main(request, response): + headers = [(b"Content-Encoding", b"gzip")] + return headers, b"not actually gzip" diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz Binary files differnew file mode 100644 index 0000000000..13441bc399 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz.headers b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz.headers new file mode 100644 index 0000000000..55d2345c23 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/big.text.gz.headers @@ -0,0 +1,3 @@ +Content-type: text/plain +Content-Encoding: gzip +Cache-Control: no-store diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz Binary files differnew file mode 100644 index 0000000000..f3df4cb89b --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers new file mode 100644 index 0000000000..27d4f401f1 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers @@ -0,0 +1,2 @@ +Content-type: application/octet-stream +Content-Encoding: gzip diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz Binary files differnew file mode 100644 index 0000000000..05a5cce07b --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz diff --git a/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz.headers b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz.headers new file mode 100644 index 0000000000..7def3ddc14 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/gzip/resources/foo.text.gz.headers @@ -0,0 +1,2 @@ +Content-type: text/plain +Content-Encoding: gzip diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/bad-zstd-body.https.any.js b/testing/web-platform/tests/fetch/content-encoding/zstd/bad-zstd-body.https.any.js new file mode 100644 index 0000000000..3f32e4dfba --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/bad-zstd-body.https.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker + +promise_test((test) => { + return fetch("resources/bad-zstd-body.py").then(res => { + assert_equals(res.status, 200); + }); +}, "Fetching a resource with bad zstd content should still resolve"); + +[ + "arrayBuffer", + "blob", + "formData", + "json", + "text" +].forEach(method => { + promise_test(t => { + return fetch("resources/bad-zstd-body.py").then(res => { + assert_equals(res.status, 200); + return promise_rejects_js(t, TypeError, res[method]()); + }); + }, "Consuming the body of a resource with bad zstd content with " + method + "() should reject"); +}); diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js b/testing/web-platform/tests/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js new file mode 100644 index 0000000000..c1dc944956 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js @@ -0,0 +1,9 @@ +// META: global=window,worker +// See https://github.com/facebook/zstd/issues/2713 for discussion about +// standardizing window size limits. + +promise_test(async t => { + const response = await fetch('resources/big.window.zst'); + assert_true(response.ok); + await promise_rejects_js(t, TypeError, response.text()); +}, 'Consuming the body of a resource with too large of a zstd window size should reject'); diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/big-zstd-body.https.any.js b/testing/web-platform/tests/fetch/content-encoding/zstd/big-zstd-body.https.any.js new file mode 100644 index 0000000000..6835f6e425 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/big-zstd-body.https.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker + +const EXPECTED_SIZE = 27000000; +const EXPECTED_SHA256 = [ + 74, 100, 37, 243, 147, 61, 116, 60, 241, 221, 126, + 18, 24, 71, 204, 28, 50, 62, 201, 130, 152, 225, + 217, 183, 10, 201, 143, 214, 102, 155, 212, 248, + ]; + +promise_test(async () => { + const response = await fetch('resources/big.text.zst'); + assert_true(response.ok); + const arrayBuffer = await response.arrayBuffer(); + assert_equals(arrayBuffer.byteLength, EXPECTED_SIZE, + 'uncompressed size should match'); + const sha256 = await crypto.subtle.digest('SHA-256', arrayBuffer); + assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, + 'digest should match'); +}, 'large zstd data should be decompressed successfully'); + +promise_test(async () => { + const response = await fetch('resources/big.text.zst'); + assert_true(response.ok); + const reader = response.body.getReader({mode: 'byob'}); + let offset = 0; + // Pre-allocate space for the output. The response body will be read + // chunk-by-chunk into this array. + let ab = new ArrayBuffer(EXPECTED_SIZE); + while (offset < EXPECTED_SIZE) { + // To stress the data pipe, we want to use a different size read each + // time. Unfortunately, JavaScript doesn't have a seeded random number + // generator, so this creates the possibility of making this test flaky if + // it doesn't work for some edge cases. + let size = Math.floor(Math.random() * 65535 + 1); + if (size + offset > EXPECTED_SIZE) { + size = EXPECTED_SIZE - offset; + } + const u8 = new Uint8Array(ab, offset, size); + const { value, done } = await reader.read(u8); + ab = value.buffer; + // Check that we got our original array back. + assert_equals(ab.byteLength, EXPECTED_SIZE, + 'backing array should be the same size'); + assert_equals(offset, value.byteOffset, 'offset should match'); + assert_less_than_equal(value.byteLength, size, + 'we should not have got more than we asked for'); + offset = value.byteOffset + value.byteLength; + if (done) break; + } + assert_equals(offset, EXPECTED_SIZE, + 'we should have read the whole thing'); + const sha256 = await crypto.subtle.digest('SHA-256', new Uint8Array(ab)); + assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, + 'digest should match'); +}, 'large zstd data should be decompressed successfully with byte stream'); diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/bad-zstd-body.py b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/bad-zstd-body.py new file mode 100644 index 0000000000..496f26881d --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/bad-zstd-body.py @@ -0,0 +1,3 @@ +def main(request, response): + headers = [(b"Content-Encoding", b"zstd")] + return headers, b"not actually zstd" diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst Binary files differnew file mode 100644 index 0000000000..30eda2443f --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst.headers b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst.headers new file mode 100644 index 0000000000..ea5b05cf2c --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.text.zst.headers @@ -0,0 +1,3 @@ +Content-type: text/plain +Content-Encoding: zstd +Cache-Control: no-store diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst Binary files differnew file mode 100644 index 0000000000..a1bca73228 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst.headers b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst.headers new file mode 100644 index 0000000000..c5974e126a --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/big.window.zst.headers @@ -0,0 +1,2 @@ +Content-type: text/plain +Content-Encoding: zstd diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst Binary files differnew file mode 100644 index 0000000000..a73bbdd224 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers new file mode 100644 index 0000000000..e397816f54 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers @@ -0,0 +1,2 @@ +Content-type: application/octet-stream +Content-Encoding: zstd diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst Binary files differnew file mode 100644 index 0000000000..a73bbdd224 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst.headers b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst.headers new file mode 100644 index 0000000000..c5974e126a --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/resources/foo.text.zst.headers @@ -0,0 +1,2 @@ +Content-type: text/plain +Content-Encoding: zstd diff --git a/testing/web-platform/tests/fetch/content-encoding/zstd/zstd-body.https.any.js b/testing/web-platform/tests/fetch/content-encoding/zstd/zstd-body.https.any.js new file mode 100644 index 0000000000..8692385743 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-encoding/zstd/zstd-body.https.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker + +const expectedDecompressedSize = 10500; +[ + "text", + "octetstream" +].forEach(contentType => { + promise_test(async t => { + let response = await fetch(`resources/foo.${contentType}.zst`); + assert_true(response.ok); + let arrayBuffer = await response.arrayBuffer() + let u8 = new Uint8Array(arrayBuffer); + assert_equals(u8.length, expectedDecompressedSize); + }, `fetched zstd data with content type ${contentType} should be decompressed.`); +}); diff --git a/testing/web-platform/tests/fetch/content-length/api-and-duplicate-headers.any.js b/testing/web-platform/tests/fetch/content-length/api-and-duplicate-headers.any.js new file mode 100644 index 0000000000..8015289f8d --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/api-and-duplicate-headers.any.js @@ -0,0 +1,23 @@ +promise_test(async t => { + const response = await fetch("resources/identical-duplicates.asis"); + assert_equals(response.statusText, "BLAH"); + assert_equals(response.headers.get("test"), "x, x"); + assert_equals(response.headers.get("content-type"), "text/plain, text/plain"); + assert_equals(response.headers.get("content-length"), "6, 6"); + const text = await response.text(); + assert_equals(text, "Test.\n"); +}, "fetch() and duplicate Content-Length/Content-Type headers"); + +async_test(t => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "resources/identical-duplicates.asis"); + xhr.send(); + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.statusText, "BLAH"); + assert_equals(xhr.getResponseHeader("test"), "x, x"); + assert_equals(xhr.getResponseHeader("content-type"), "text/plain, text/plain"); + assert_equals(xhr.getResponseHeader("content-length"), "6, 6"); + assert_equals(xhr.getAllResponseHeaders(), "content-length: 6, 6\r\ncontent-type: text/plain, text/plain\r\ntest: x, x\r\n"); + assert_equals(xhr.responseText, "Test.\n"); + }); +}, "XMLHttpRequest and duplicate Content-Length/Content-Type headers"); diff --git a/testing/web-platform/tests/fetch/content-length/content-length.html b/testing/web-platform/tests/fetch/content-length/content-length.html new file mode 100644 index 0000000000..cda9b5b523 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/content-length.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<!-- CAUTION: if updating this test also update the expected content-length in the .headers file --> +<title>Content-Length Test</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ single_test: true }); +onload = function() { + assert_equals(document.body.textContent, "PASS"); + done(); +} +</script> +<body>PASS +but FAIL if this is in the body.
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/content-length/content-length.html.headers b/testing/web-platform/tests/fetch/content-length/content-length.html.headers new file mode 100644 index 0000000000..25389b7c0f --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/content-length.html.headers @@ -0,0 +1 @@ +Content-Length: 403 diff --git a/testing/web-platform/tests/fetch/content-length/parsing.window.js b/testing/web-platform/tests/fetch/content-length/parsing.window.js new file mode 100644 index 0000000000..5028ad943d --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/parsing.window.js @@ -0,0 +1,18 @@ +promise_test(() => { + return fetch("resources/content-lengths.json").then(res => res.json()).then(runTests); +}, "Loading JSON…"); + +function runTests(testUnits) { + testUnits.forEach(({ input, output }) => { + promise_test(t => { + const result = fetch(`resources/content-length.py?length=${encodeURIComponent(input)}`); + if (output === null) { + return promise_rejects_js(t, TypeError, result); + } else { + return result.then(res => res.text()).then(text => { + assert_equals(text.length, output); + }); + } + }, `Input: ${format_value(input)}. Expected: ${output === null ? "network error" : output}.`); + }); +} diff --git a/testing/web-platform/tests/fetch/content-length/resources/content-length.py b/testing/web-platform/tests/fetch/content-length/resources/content-length.py new file mode 100644 index 0000000000..92cfadeb06 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/resources/content-length.py @@ -0,0 +1,10 @@ +def main(request, response): + response.add_required_headers = False + output = b"HTTP/1.1 200 OK\r\n" + output += b"Content-Type: text/plain;charset=UTF-8\r\n" + output += b"Connection: close\r\n" + output += request.GET.first(b"length") + b"\r\n" + output += b"\r\n" + output += b"Fact: this is really forty-two bytes long." + response.writer.write(output) + response.close_connection = True diff --git a/testing/web-platform/tests/fetch/content-length/resources/content-lengths.json b/testing/web-platform/tests/fetch/content-length/resources/content-lengths.json new file mode 100644 index 0000000000..ac6f1a2468 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/resources/content-lengths.json @@ -0,0 +1,142 @@ +[ + { + "input": "Content-Length: 42", + "output": 42 + }, + { + "input": "Content-Length: 42,42", + "output": 42 + }, + { + "input": "Content-Length: 42\r\nContent-Length: 42", + "output": 42 + }, + { + "input": "Content-Length: 42\r\nContent-Length: 42,42", + "output": 42 + }, + { + "input": "Content-Length: 30", + "output": 30 + }, + { + "input": "Content-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 30\r\nContent-Length: 30", + "output": 30 + }, + { + "input": "Content-Length: 30\r\nContent-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 30,30\r\nContent-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 30,30, 30 \r\nContent-Length: 30 ", + "output": 30 + }, + { + "input": "Content-Length: 30,42\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 30,42\r\nContent-Length: 30,42", + "output": null + }, + { + "input": "Content-Length: 42,30", + "output": null + }, + { + "input": "Content-Length: 30,42", + "output": null + }, + { + "input": "Content-Length: 42\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 30\r\nContent-Length: 42", + "output": null + }, + { + "input": "Content-Length: 30,", + "output": null + }, + { + "input": "Content-Length: ,30", + "output": null + }, + { + "input": "Content-Length: 30\r\nContent-Length: \t", + "output": null + }, + { + "input": "Content-Length: \r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: aaaah\r\nContent-Length: nah", + "output": null + }, + { + "input": "Content-Length: aaaah, nah", + "output": null + }, + { + "input": "Content-Length: aaaah\r\nContent-Length: aaaah", + "output": 42 + }, + { + "input": "Content-Length: aaaah, aaaah", + "output": 42 + }, + { + "input": "Content-Length: aaaah", + "output": 42 + }, + { + "input": "Content-Length: 42s", + "output": 42 + }, + { + "input": "Content-Length: 30s", + "output": 42 + }, + { + "input": "Content-Length: -1", + "output": 42 + }, + { + "input": "Content-Length: 0x20", + "output": 42 + }, + { + "input": "Content-Length: 030", + "output": 30 + }, + { + "input": "Content-Length: 030\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 030, 30", + "output": null + }, + { + "input": "Content-Length: \"30\"", + "output": 42 + }, + { + "input": "Content-Length:30\r\nContent-Length:,\r\nContent-Length:30", + "output": null + }, + { + "input": "Content-Length: ", + "output": 42 + } +] diff --git a/testing/web-platform/tests/fetch/content-length/resources/identical-duplicates.asis b/testing/web-platform/tests/fetch/content-length/resources/identical-duplicates.asis new file mode 100644 index 0000000000..f38c9a4b8a --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/resources/identical-duplicates.asis @@ -0,0 +1,9 @@ +HTTP/1.1 200 BLAH +Test: x +Test: x +Content-Type: text/plain +Content-Type: text/plain +Content-Length: 6 +Content-Length: 6 + +Test. diff --git a/testing/web-platform/tests/fetch/content-length/too-long.window.js b/testing/web-platform/tests/fetch/content-length/too-long.window.js new file mode 100644 index 0000000000..f8cefaa9c2 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-length/too-long.window.js @@ -0,0 +1,4 @@ +promise_test(async t => { + const result = await fetch(`resources/content-length.py?length=${encodeURIComponent("Content-Length: 50")}`); + await promise_rejects_js(t, TypeError, result.text()); +}, "Content-Length header value of network response exceeds response body"); diff --git a/testing/web-platform/tests/fetch/content-type/README.md b/testing/web-platform/tests/fetch/content-type/README.md new file mode 100644 index 0000000000..f553b7ee8e --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/README.md @@ -0,0 +1,20 @@ +# `resources/content-types.json` + +An array of tests. Each test has these fields: + +* `contentType`: an array of values for the `Content-Type` header. A harness needs to run the test twice if there are multiple values. One time with the values concatenated with `,` followed by a space and one time with multiple `Content-Type` declarations, each on their own line with one of the values, in order. +* `encoding`: the expected encoding, null for the default. +* `mimeType`: the result of extracting a MIME type and serializing it. +* `documentContentType`: the MIME type expected to be exposed in DOM documents. + +(These tests are currently somewhat geared towards browser use, but could be generalized easily enough if someone wanted to contribute tests for MIME types that would cause downloads in the browser or some such.) + +# `resources/script-content-types.json` + +An array of tests, surprise. Each test has these fields: + +* `contentType`: see above. +* `executes`: whether the script is expected to execute. +* `encoding`: how the script is expected to be decoded. + +These tests are expected to be loaded through `<script src>` and the server is expected to set `X-Content-Type-Options: nosniff`. diff --git a/testing/web-platform/tests/fetch/content-type/multipart-malformed.any.js b/testing/web-platform/tests/fetch/content-type/multipart-malformed.any.js new file mode 100644 index 0000000000..9de0edc24a --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/multipart-malformed.any.js @@ -0,0 +1,22 @@ +// This is a repro for Chromium issue https://crbug.com/1412007. +promise_test(t => { + const form_string = + "--Boundary_with_capital_letters\r\n" + + "Content-Type: application/json\r\n" + + 'Content-Disposition: form-data; name="does_this_work"\r\n' + + "\r\n" + + 'YES\r\n' + + "--Boundary_with_capital_letters-Random junk"; + + const r = new Response(new Blob([form_string]), { + headers: [ + [ + "Content-Type", + "multipart/form-data; boundary=Boundary_with_capital_letters", + ], + ], + }); + + return promise_rejects_js(t, TypeError, r.formData(), + "form data should fail to parse"); +}, "Invalid form data should not crash the browser"); diff --git a/testing/web-platform/tests/fetch/content-type/multipart.window.js b/testing/web-platform/tests/fetch/content-type/multipart.window.js new file mode 100644 index 0000000000..03b037a0e6 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/multipart.window.js @@ -0,0 +1,33 @@ +// META: title=Ensure capital letters can be used in the boundary value. +setup({ single_test: true }); +(async () => { + const form_string = + "--Boundary_with_capital_letters\r\n" + + "Content-Type: application/json\r\n" + + 'Content-Disposition: form-data; name="does_this_work"\r\n' + + "\r\n" + + 'YES\r\n' + + "--Boundary_with_capital_letters--\r\n"; + + const r = new Response(new Blob([form_string]), { + headers: [ + [ + "Content-Type", + "multipart/form-data; boundary=Boundary_with_capital_letters", + ], + ], + }); + + var s = ""; + try { + const fd = await r.formData(); + for (const [key, value] of fd.entries()) { + s += (`${key} = ${value}`); + } + } catch (ex) { + s = ex; + } + + assert_equals(s, "does_this_work = YES"); + done(); +})(); diff --git a/testing/web-platform/tests/fetch/content-type/resources/content-type.py b/testing/web-platform/tests/fetch/content-type/resources/content-type.py new file mode 100644 index 0000000000..1f077b6289 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/resources/content-type.py @@ -0,0 +1,18 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + values = request.GET.get_list(b"value") + content = request.GET.first(b"content", b"<b>hi</b>\n") + output = b"HTTP/1.1 200 OK\r\n" + output += b"X-Content-Type-Options: nosniff\r\n" + if b"single_header" in request.GET: + output += b"Content-Type: " + b",".join(values) + b"\r\n" + else: + for value in values: + output += b"Content-Type: " + value + b"\r\n" + output += b"Content-Length: " + isomorphic_encode(str(len(content))) + b"\r\n" + output += b"Connection: close\r\n" + output += b"\r\n" + output += content + response.writer.write(output) + response.close_connection = True diff --git a/testing/web-platform/tests/fetch/content-type/resources/content-types.json b/testing/web-platform/tests/fetch/content-type/resources/content-types.json new file mode 100644 index 0000000000..9578fc503c --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/resources/content-types.json @@ -0,0 +1,122 @@ +[ + { + "contentType": ["", "text/plain"], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/plain", ""], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/html", "text/plain"], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/plain;charset=gbk", "text/html"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/plain;charset=gbk", "text/html;charset=windows-1254"], + "encoding": "windows-1254", + "mimeType": "text/html;charset=windows-1254", + "documentContentType": "text/html" + }, + { + "contentType": ["text/plain;charset=gbk", "text/plain"], + "encoding": "GBK", + "mimeType": "text/plain;charset=gbk", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/plain;charset=gbk", "text/plain;charset=windows-1252"], + "encoding": "windows-1252", + "mimeType": "text/plain;charset=windows-1252", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/html;charset=gbk", "text/html;x=\",text/plain"], + "encoding": "GBK", + "mimeType": "text/html;x=\",text/plain\";charset=gbk", + "documentContentType": "text/html" + }, + { + "contentType": ["text/plain;charset=gbk;x=foo", "text/plain"], + "encoding": "GBK", + "mimeType": "text/plain;charset=gbk", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/html;charset=gbk", "text/plain", "text/html"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/plain", "*/*"], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/html", "*/*"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["*/*", "text/html"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/plain", "*/*;charset=gbk"], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + }, + { + "contentType": ["text/html", "*/*;charset=gbk"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/html;x=\"", "text/plain"], + "encoding": null, + "mimeType": "text/html;x=\", text/plain\"", + "documentContentType": "text/html" + }, + { + "contentType": ["text/html;\"", "text/plain"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/html;\"", "\\\"", "text/plain"], + "encoding": null, + "mimeType": "text/html", + "documentContentType": "text/html" + }, + { + "contentType": ["text/html;\"", "\\\"", "text/plain", "\";charset=GBK"], + "encoding": "GBK", + "mimeType": "text/html;charset=GBK", + "documentContentType": "text/html" + }, + { + "contentType": ["text/html;\"", "\"", "text/plain"], + "encoding": null, + "mimeType": "text/plain", + "documentContentType": "text/plain" + } +] diff --git a/testing/web-platform/tests/fetch/content-type/resources/script-content-types.json b/testing/web-platform/tests/fetch/content-type/resources/script-content-types.json new file mode 100644 index 0000000000..b8a843bcd8 --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/resources/script-content-types.json @@ -0,0 +1,92 @@ +[ + { + "contentType": ["text/javascript;charset=windows-1252"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["text/javascript;\";charset=windows-1252"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["text/javascript\u000C"], + "executes": false, + "encoding": null + }, + { + "contentType": ["\"text/javascript\""], + "executes": false, + "encoding": null + }, + { + "contentType": ["text/ javascript"], + "executes": false, + "encoding": null + }, + { + "contentType": ["text /javascript"], + "executes": false, + "encoding": null + }, + { + "contentType": ["x/x", "text/javascript"], + "executes": true, + "encoding": null + }, + { + "contentType": ["x/x;charset=windows-1252", "text/javascript"], + "executes": true, + "encoding": null + }, + { + "contentType": ["text/javascript", "x/x"], + "executes": false, + "encoding": null + }, + { + "contentType": ["text/javascript; charset=windows-1252", "text/javascript"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["text/javascript;\"", "x/x"], + "executes": true, + "encoding": null + }, + { + "contentType": ["text/javascript", ""], + "executes": true, + "encoding": null + }, + { + "contentType": ["text/javascript", "error"], + "executes": true, + "encoding": null + }, + { + "contentType": ["text/javascript;charset=windows-1252", "x/x", "text/javascript"], + "executes": true, + "encoding": null + }, + { + "contentType": ["text/javascript;charset=windows-1252", "error", "text/javascript"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["text/javascript;charset=windows-1252", "", "text/javascript"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["text/javascript;charset=windows-1252;\"", "\\\"", "x/x"], + "executes": true, + "encoding": "windows-1252" + }, + { + "contentType": ["x/x;\"", "x/y;\\\"", "text/javascript;charset=windows-1252;\"", "text/javascript"], + "executes": true, + "encoding": null + } +] diff --git a/testing/web-platform/tests/fetch/content-type/response.window.js b/testing/web-platform/tests/fetch/content-type/response.window.js new file mode 100644 index 0000000000..746f51c9bb --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/response.window.js @@ -0,0 +1,72 @@ +promise_test(() => { + return fetch("resources/content-types.json").then(res => res.json()).then(runTests); +}, "Loading JSON…"); + +function runTests(tests) { + tests.forEach(testUnit => { + runFrameTest(testUnit, false); + runFrameTest(testUnit, true); + runFetchTest(testUnit, false); + runFetchTest(testUnit, true); + runRequestResponseTest(testUnit, "Request"); + runRequestResponseTest(testUnit, "Response"); + }); +} + +function runFrameTest(testUnit, singleHeader) { + // Note: window.js is always UTF-8 + const encoding = testUnit.encoding !== null ? testUnit.encoding : "UTF-8"; + async_test(t => { + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + frame.src = getURL(testUnit.contentType, singleHeader); + frame.onload = t.step_func_done(() => { + // Edge requires toUpperCase() + const doc = frame.contentDocument; + assert_equals(doc.characterSet.toUpperCase(), encoding.toUpperCase()); + if (testUnit.documentContentType === "text/plain") { + assert_equals(doc.body.textContent, "<b>hi</b>\n"); + } else if (testUnit.documentContentType === "text/html") { + assert_equals(doc.body.firstChild.localName, "b"); + assert_equals(doc.body.firstChild.textContent, "hi"); + } + assert_equals(doc.contentType, testUnit.documentContentType); + }); + }, getDesc("<iframe>", testUnit.contentType, singleHeader)); +} + +function getDesc(type, input, singleHeader) { + return type + ": " + (singleHeader ? "combined" : "separate") + " response Content-Type: " + input.join(" "); +} + +function getURL(input, singleHeader) { + // Edge does not support URLSearchParams + let url = "resources/content-type.py?" + if (singleHeader) { + url += "single_header&" + } + input.forEach(val => { + url += "value=" + encodeURIComponent(val) + "&"; + }); + return url; +} + +function runFetchTest(testUnit, singleHeader) { + promise_test(async t => { + const blob = await (await fetch(getURL(testUnit.contentType, singleHeader))).blob(); + assert_equals(blob.type, testUnit.mimeType); + }, getDesc("fetch()", testUnit.contentType, singleHeader)); +} + +function runRequestResponseTest(testUnit, stringConstructor) { + promise_test(async t => { + // Cannot give Response a body as that will set Content-Type, but Request needs a URL + const constructorArgument = stringConstructor === "Request" ? "about:blank" : undefined; + const r = new self[stringConstructor](constructorArgument); + testUnit.contentType.forEach(val => { + r.headers.append("Content-Type", val); + }); + const blob = await r.blob(); + assert_equals(blob.type, testUnit.mimeType); + }, getDesc(stringConstructor, testUnit.contentType, true)); +} diff --git a/testing/web-platform/tests/fetch/content-type/script.window.js b/testing/web-platform/tests/fetch/content-type/script.window.js new file mode 100644 index 0000000000..31598957ef --- /dev/null +++ b/testing/web-platform/tests/fetch/content-type/script.window.js @@ -0,0 +1,48 @@ +promise_test(() => { + return fetch("resources/script-content-types.json").then(res => res.json()).then(runTests); +}, "Loading JSON…"); + +self.stringFromExecutedScript = undefined; + +function runTests(allTestData) { + allTestData.forEach(testData => { + runScriptTest(testData, false); + if (testData.contentType.length > 1) { + runScriptTest(testData, true); + } + }); +} + +function runScriptTest(testData, singleHeader) { + async_test(t => { + const script = document.createElement("script"); + t.add_cleanup(() => { + script.remove() + self.stringFromExecutedScript = undefined; + }); + script.src = getURL(testData.contentType, singleHeader); + document.head.appendChild(script); + if (testData.executes) { + script.onload = t.step_func_done(() => { + assert_equals(self.stringFromExecutedScript, testData.encoding === "windows-1252" ? "€" : "€"); + }); + script.onerror = t.unreached_func("onerror"); + } else { + script.onerror = t.step_func_done(); + script.onload = t.unreached_func("onload"); + } + }, (singleHeader ? "combined" : "separate") + " " + testData.contentType.join(" ")); +} + +function getURL(input, singleHeader) { + // Edge does not support URLSearchParams + let url = "resources/content-type.py?" + if (singleHeader) { + url += "single_header&" + } + input.forEach(val => { + url += "value=" + encodeURIComponent(val) + "&"; + }); + url += "&content=" + encodeURIComponent("self.stringFromExecutedScript = \"€\""); + return url; +} diff --git a/testing/web-platform/tests/fetch/corb/README.md b/testing/web-platform/tests/fetch/corb/README.md new file mode 100644 index 0000000000..f29562b060 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/README.md @@ -0,0 +1,67 @@ +# Tests related to Cross-Origin Resource Blocking (CORB). + +### Summary + +This directory contains tests related to the +[Cross-Origin Resource Blocking (CORB)](https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md) +algorithm. + +The tests in this directory interact with various, random features, +but the tests have been grouped together into the `fetch/corb` directory, +because all of these tests verify behavior that is important to the CORB +algorithm. + + +### CORB is not universally implemented yet + +CORB has been included +in the [Fetch spec](https://fetch.spec.whatwg.org/#corb) +since [May 2018](https://github.com/whatwg/fetch/pull/686). + +Some tests in this directory (e.g. +`css-with-json-parser-breaker`) cover behavior spec-ed outside of CORB (making +sure that CORB doesn't change the existing web behavior) and therefore are +valuable independently from CORB's standardization efforts and should already +be passing across all browsers. + +Tests that cover behavior that is changed by CORB are currently marked as +[tentative](https://web-platform-tests.org/writing-tests/file-names.html) +(using `.tentative` substring in their filename). +Such tests may fail unless CORB is enabled. In practice this means that: +* Such tests will pass in Chromium + (where CORB is enabled by default [since M68](https://crrev.com/553830)). +* Such tests may fail in other browsers. + + +### Limitations of WPT test coverage + +CORB is a defense-in-depth and in general should not cause changes in behavior +that can be observed by web features or by end users. This makes CORB difficult +or even impossible to test via WPT. + +WPT tests can cover the following: + +* Helping verify CORB has no observable impact in specific scenarios. + Examples: + * image rendering of (an empty response of) a html document blocked by CORB + should be indistinguishable from rendering such html document without CORB - + `img-html-correctly-labeled.sub.html` + * CORB shouldn't block responses that don't sniff as a CORB-protected document + type - `img-png-mislabeled-as-html.sub.html` +* Helping document cases where CORB causes observable changes in behavior. + Examples: + * blocking of nosniff images labeled as non-image, CORB-protected + Content-Type - `img-png-mislabeled-as-html-nosniff.tentative.sub.html` + * blocking of CORB-protected documents can prevent triggering + syntax errors in scripts - + `script-html-via-cross-origin-blob-url.tentative.sub.html` +* Helping verify which MIME types are protected by CORB. + +Examples of aspects that WPT tests cannot cover (these aspects have to be +covered in other, browser-specific tests): +* Verifying that CORB doesn't affect things that are only indirectly + observable by the web (like + [prefetch](https://html.spec.whatwg.org/#link-type-prefetch). +* Verifying that CORB strips headers of blocked responses. +* Verifying that CORB blocks responses before they reach the process hosting + a cross-origin execution context. diff --git a/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html b/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html new file mode 100644 index 0000000000..0e75596952 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not CORB-eligible --> +<img src="resources/html-correctly-labeled.html"> diff --git a/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub.html b/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub.html new file mode 100644 index 0000000000..844cd0c927 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-html-correctly-labeled.sub.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<!-- Test verifies that html fed to an <img> tag doesn't have any observable + difference with and without CORB (in both cases the resource body cannot be + rendered as an image - html cannot be rendered as an image and the empty body + from a CORB-blocked response also cannot be rendered as an image). +--> +<meta charset="utf-8"> +<!-- Reference page uses same-origin resources, which are not CORB-eligible. --> +<link rel="match" href="img-html-correctly-labeled.sub-ref.html"> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html"> diff --git a/testing/web-platform/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html b/testing/web-platform/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html new file mode 100644 index 0000000000..e2386de2f2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html @@ -0,0 +1,85 @@ +<!-- Test verifies that cross-origin, nosniff images are 1) blocked when their + MIME type is covered by CORB and 2) allowed otherwise. + + This test is very similar to fetch/nosniff/images.html, except that + 1) it deals with cross-origin images (CORB ignores same-origin fetches), + 2) it focuses on MIME types relevant to CORB. + There are opportunities to unify the test here with nosniff tests *if* + we can also start blocking same-origin (or cors-allowed) images. We + should try to gather data to quantify the impact of such change. +--> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + var passes = [ + // Empty or non-sensical MIME types + null, "", "x", "x/x", + + // MIME-types not protected by CORB + "image/gif", "image/png", "image/png;blah", "image/svg+xml", + "application/javascript", "application/jsonp", + "application/dash+xml", // video format + "image/gif;HI=THERE", + + // Non-image MIME-types that in practice get used for images on the web. + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1302539 + "application/octet-stream", + // https://crbug.com/990853 + "application/x-www-form-urlencoded", + + // MIME types that may seem to be JSON or XML, but really aren't - i.e. + // these MIME types are not covered by: + // - https://mimesniff.spec.whatwg.org/#json-mime-type + // - https://mimesniff.spec.whatwg.org/#xml-mime-type + // - https://tools.ietf.org/html/rfc6839 + // - https://tools.ietf.org/html/rfc7303 + "text/x-json", "text/json+blah", "application/json+blah", + "text/xml+blah", "application/xml+blah", + "application/blahjson", "text/blahxml"] + + var fails = [ + // CORB-protected MIME-types - i.e. ones covered by: + // - https://mimesniff.spec.whatwg.org/#html-mime-type + // - https://mimesniff.spec.whatwg.org/#json-mime-type + // - https://mimesniff.spec.whatwg.org/#xml-mime-type + "text/html", + "text/json", "application/json", "text/xml", "application/xml", + "application/blah+json", "text/blah+json", + "application/blah+xml", "text/blah+xml", + "TEXT/HTML", "TEXT/JSON", "TEXT/BLAH+JSON", "APPLICATION/BLAH+XML", + "text/json;does=it;matter", "text/HTML;NO=it;does=NOT"] + + const get_url = (mime) => { + // www1 is cross-origin, so the HTTP response is CORB-eligible --> + url = "http://{{domains[www1]}}:{{ports[http][0]}}" + url = url + "/fetch/nosniff/resources/image.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + return url + } + + passes.forEach(function(mime) { + async_test(function(t) { + var img = document.createElement("img") + img.onerror = t.unreached_func("Unexpected error event") + img.onload = t.step_func_done(function(){ + assert_equals(img.width, 96) + }) + img.src = get_url(mime) + document.body.appendChild(img) + }, "CORB should allow the response if Content-Type is: '" + mime + "'. ") + }) + + fails.forEach(function(mime) { + async_test(function(t) { + var img = document.createElement("img") + img.onerror = t.step_func_done() + img.onload = t.unreached_func("Unexpected load event") + img.src = get_url(mime) + document.body.appendChild(img) + }, "CORB should block the response if Content-Type is: '" + mime + "'. ") + }) +</script> diff --git a/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html new file mode 100644 index 0000000000..a771ed6a65 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not CORB-eligible --> +<img src="resources/empty-labeled-as-png.png"> diff --git a/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 0000000000..82adc47b0c --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<!-- Test verifies that CORB blocks an image mislabeled as text/html if + sniffing is disabled via `X-Content-Type-Options: nosniff` response header. + This has an observable effect (the image stops rendering), compared to the + behavior with no CORB. +--> +<meta charset="utf-8"> +<!-- Reference page uses same-origin resources, which are not CORB-eligible. --> +<link rel="match" href="img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html"> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html-nosniff.png"> diff --git a/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html new file mode 100644 index 0000000000..ebb337dba8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not CORB-eligible --> +<img src="resources/png-correctly-labeled.png"> diff --git a/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub.html b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub.html new file mode 100644 index 0000000000..1ae4cfcaa7 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-png-mislabeled-as-html.sub.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<!-- Test verifies that CORB won't block an image after sniffing determines + that the text/html Content-Type response header doesn't match the response + body. +--> +<meta charset="utf-8"> +<!-- Reference page uses same-origin resources, which are not CORB-eligible. --> +<link rel="match" href="img-png-mislabeled-as-html.sub-ref.html"> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html.png"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html b/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html new file mode 100644 index 0000000000..3219feda17 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<!-- Verifies CORB/ORB SVG image blocking. + This image has no MIME type and an html DOCTYPE declaration and is + expected to be blocked--> +<meta charset="utf-8"> +<link rel="match" href="img-svg-invalid.sub-ref.html"> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html b/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html new file mode 100644 index 0000000000..efcfaa2737 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<!-- Verifies CORB/ORB SVG image blocking. + This image has an SVG MIME type and an html DOCTYPE declaration and is + expected to load. + + This testcase is distilled from a bugreport and real web page. See: + https://crbug.com/1359788 +--> +<meta charset="utf-8"> +<link rel="match" href="img-svg.sub-ref.html"> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-invalid.sub-ref.html b/testing/web-platform/tests/fetch/corb/img-svg-invalid.sub-ref.html new file mode 100644 index 0000000000..484cd0a4fd --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-invalid.sub-ref.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, but invalid URL. + Serves as reference for tests that expect the image to be blocked. --> +<img src="resources/invalid.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-dash.sub.html b/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-dash.sub.html new file mode 100644 index 0000000000..0578b835fe --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-dash.sub.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<!-- Verifies CORB/ORB SVG image blocking. + This image is served with a DASH MIME type, and is expected to be blocked. --> +<meta charset="utf-8"> +<link rel="match" href="img-svg-invalid.sub-ref.html"> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-labeled-as-dash.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html b/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html new file mode 100644 index 0000000000..30a2eb3246 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<!-- Verifies CORB/ORB SVG image blocking. + This image is served with a proper SVG MIME type and is expected to load. --> +<meta charset="utf-8"> +<link rel="match" href="img-svg.sub-ref.html"> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-labeled-as-svg-xml.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg-xml-decl.sub.html b/testing/web-platform/tests/fetch/corb/img-svg-xml-decl.sub.html new file mode 100644 index 0000000000..0d3aeafb25 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg-xml-decl.sub.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<!-- Verifies CORB/ORB SVG image blocking. + This image has an XML declaration and is expected to load. --> +<meta charset="utf-8"> +<link rel="match" href="img-svg.sub-ref.html"> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/svg-xml-decl.svg"> diff --git a/testing/web-platform/tests/fetch/corb/img-svg.sub-ref.html b/testing/web-platform/tests/fetch/corb/img-svg.sub-ref.html new file mode 100644 index 0000000000..5462f685a0 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/img-svg.sub-ref.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not CORB-eligible. + Serves as reference for cases the image is expected to be loaded. --> +<img src="resources/svg.svg"> diff --git a/testing/web-platform/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html b/testing/web-platform/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 0000000000..cea80f2f89 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<!-- This test verifies observable CORB impact on <link rel="preload"> elements. +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +async_test(function(t) { + // With CORB the link.onerror event will be reached + // (because CORB will block the cross-origin preload). + window.preloadErrorEvent = t.step_func_done(); + + // Without CORB the link.onload event will be reached. + window.preloadLoadEvent = t.unreached_func("link/preload onload event reached."); +}); +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<link rel="preload" as="image" + onerror="window.preloadErrorEvent()" + onload="window.preloadLoadEvent()" + href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/png-mislabeled-as-html-nosniff.png"> diff --git a/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css new file mode 100644 index 0000000000..afd2b92975 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers new file mode 100644 index 0000000000..0f228f94ec --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css new file mode 100644 index 0000000000..afd2b92975 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/css-with-json-parser-breaker.css b/testing/web-platform/tests/fetch/corb/resources/css-with-json-parser-breaker.css new file mode 100644 index 0000000000..7db6f5c6d3 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/css-with-json-parser-breaker.css @@ -0,0 +1,3 @@ +)]}' +{} +#header { color: red; } diff --git a/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png b/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png diff --git a/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png.headers b/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png.headers new file mode 100644 index 0000000000..e7be84a714 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/empty-labeled-as-png.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html b/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html new file mode 100644 index 0000000000..7bad71bfbd --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Page Title</title> + </head> + <body> + <p>Page body</p> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html.headers b/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-correctly-labeled.html.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js new file mode 100644 index 0000000000..db45bb4acc --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js @@ -0,0 +1,9 @@ +<!--/*--><html><body><script type="text/javascript"><!--//*/ + +// This is a regression test for https://crbug.com/839425 +// which found out that some script resources are served +// with text/html content-type and with a body that is +// both a valid html and a valid javascript. +window['html-js-polyglot.js'] = true; + +//--></script></body></html> diff --git a/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js.headers b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js new file mode 100644 index 0000000000..faae1b7682 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js @@ -0,0 +1,10 @@ +<!-- comment --> <script type='text/javascript'> +//<![CDATA[ + +// This is a regression test for https://crbug.com/839945 +// which found out that some script resources are served +// with text/html content-type and with a body that is +// both a valid html and a valid javascript. +window['html-js-polyglot2.js'] = true; + +//]]>--></script> diff --git a/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js.headers b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/html-js-polyglot2.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js new file mode 100644 index 0000000000..a880a5bc72 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers new file mode 100644 index 0000000000..0f228f94ec --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js new file mode 100644 index 0000000000..a880a5bc72 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png b/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png diff --git a/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png.headers b/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png.headers new file mode 100644 index 0000000000..e7be84a714 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-correctly-labeled.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png diff --git a/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers new file mode 100644 index 0000000000..0f228f94ec --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png diff --git a/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js b/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js new file mode 100644 index 0000000000..9c3b87bcbd --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js @@ -0,0 +1 @@ +alert(1); // Arbitrary JavaScript. Details don't matter for the test. diff --git a/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js.headers b/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js.headers new file mode 100644 index 0000000000..0d848b02c2 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/response_block_probe.js.headers @@ -0,0 +1 @@ +Content-Type: text/csv diff --git a/testing/web-platform/tests/fetch/corb/resources/sniffable-resource.py b/testing/web-platform/tests/fetch/corb/resources/sniffable-resource.py new file mode 100644 index 0000000000..f8150936ac --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/sniffable-resource.py @@ -0,0 +1,11 @@ +def main(request, response): + body = request.GET.first(b"body", None) + type = request.GET.first(b"type", None) + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"content-length", len(body)) + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(body) diff --git a/testing/web-platform/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html b/testing/web-platform/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html new file mode 100644 index 0000000000..67b3ad5a60 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> +fetch('html-correctly-labeled.html') + .then(response => response.blob()) + .then(blob => { + let msg = { blob_size: blob.size, + blob_type: blob.type, + blob_url: URL.createObjectURL(blob) }; + window.parent.postMessage(msg, '*'); + }) + .catch(error => { + let msg = { error: error }; + window.parent.postMessage(msg, '*'); + }); +</script> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg new file mode 100644 index 0000000000..fa2d29b3b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers new file mode 100644 index 0000000000..29515ee7d4 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers @@ -0,0 +1 @@ +Content-Type: diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg new file mode 100644 index 0000000000..fa2d29b3b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers new file mode 100644 index 0000000000..070de35fbe --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg new file mode 100644 index 0000000000..2b7d1016b1 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg @@ -0,0 +1,3 @@ +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers new file mode 100644 index 0000000000..43ce612c9f --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers @@ -0,0 +1 @@ +Content-Type: application/dash+xml diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg new file mode 100644 index 0000000000..2b7d1016b1 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg @@ -0,0 +1,3 @@ +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers new file mode 100644 index 0000000000..070de35fbe --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml diff --git a/testing/web-platform/tests/fetch/corb/resources/svg-xml-decl.svg b/testing/web-platform/tests/fetch/corb/resources/svg-xml-decl.svg new file mode 100644 index 0000000000..3b39aff8e5 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg-xml-decl.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg.svg b/testing/web-platform/tests/fetch/corb/resources/svg.svg new file mode 100644 index 0000000000..2b7d1016b1 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg.svg @@ -0,0 +1,3 @@ +<svg width="920" height="625" xmlns="http://www.w3.org/2000/svg" role="img"> +<rect width="100" height="100" style="fill:rgb(255,0,0)"/> +</svg> diff --git a/testing/web-platform/tests/fetch/corb/resources/svg.svg.headers b/testing/web-platform/tests/fetch/corb/resources/svg.svg.headers new file mode 100644 index 0000000000..070de35fbe --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/resources/svg.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml diff --git a/testing/web-platform/tests/fetch/corb/response_block.tentative.https.html b/testing/web-platform/tests/fetch/corb/response_block.tentative.https.html new file mode 100644 index 0000000000..6b116000d4 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/response_block.tentative.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +// A cross-origin response containing JavaScript, labelled as text/csv. +const probeUrl = get_host_info().HTTPS_REMOTE_ORIGIN + + "/fetch/corb/resources/response_block_probe.js"; + +// Test handling of blocked responses in CORB/ORB for <script> elements. +function probe_script() { + // We will cross-origin load a script resource that should get blocked by all + // versions of CORB/ORB. Two things may happen: + // + // 1, An empty response is injected. (What CORB does.) + // 2, An error is injected and script loading aborts. (What ORB does.) + + // Load the probe as a script. + const script = document.createElement("script"); + script.src = probeUrl; + document.body.appendChild(script); + + // Return a promise that will return a string description corresponding to the + // conditions above. + return new Promise((resolve, reject) => { + script.onload = _ => resolve("Resource loaded (expected for CORB)"); + script.onerror = _ => resolve("ORB-style network error"); + }); +} + +// Test handling of blocked responses in CORB/ORB for script-initiated fetches. +function probe_fetch() { + return fetch(probeUrl, {mode: "no-cors"}) + .then(response => response.text()) + .then(text => { + assert_equals(text, ""); + return "Resource loaded (expected for CORB)"; + }) + .catch(_ => "ORB-style network error"); +} + +// These tests check for ORB behaviour. +promise_test(t => probe_script().then( + value => assert_equals(value, "ORB-style network error")), + "ORB: Expect error response from <script> fetch."); +promise_test(t => probe_fetch().then( + value => assert_equals(value, "ORB-style network error")), + "ORB: Expect error response from fetch()."); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/testing/web-platform/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html new file mode 100644 index 0000000000..6d1947cea7 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- Test verifies that html fed to a <script> tag won't report a syntax + error after CORB blocks the response (an empty response body injected + by CORB won't have any JavaScript syntax errors). +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> +<script> +setup({allow_uncaught_exception : true}); +async_test(function(t) { + var script = document.createElement("script") + + // Without CORB, the html document would cause a syntax error when parsed as + // JavaScript, but with CORB there should be no errors (because CORB will + // replace the response body with an empty body). With ORB, the script loading + // itself will error out. + script.onload = t.step_func_done(); + script.onerror = t.step_func_done(); + addEventListener("error",function(e) { + t.step(function() { + assert_unreached("Empty body of a CORB-blocked response shouldn't trigger syntax errors."); + t.done(); + }) + }); + + // www1 is cross-origin, so the HTTP response is CORB-eligible. + script.src = 'http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html'; + document.body.appendChild(script) +}, "CORB-blocked script has no syntax errors"); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-html-js-polyglot.sub.html b/testing/web-platform/tests/fetch/corb/script-html-js-polyglot.sub.html new file mode 100644 index 0000000000..9a272d63ff --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-html-js-polyglot.sub.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- Test verifies that CORB won't block a polyglot script that is + both a valid HTML document and also valid Javascript. +--> +<meta charset="utf-8"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +["html-js-polyglot.js", "html-js-polyglot2.js"].forEach(polyglot_name => { + async_test(function(t) { + window[polyglot_name] = false; + var script = document.createElement("script"); + + script.onload = t.step_func_done(function(){ + // Verify that the script response wasn't blocked - that script + // should have set window[polyglot_name] to true. + assert_true(window[polyglot_name]); + }) + addEventListener("error",function(e) { + t.step(function() { + assert_unreached("No errors are expected with or without CORB."); + t.done(); + }) + }); + + // www1 is cross-origin, so the HTTP response is CORB-eligible. + script.src = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/" + polyglot_name; + document.body.appendChild(script); + }, "CORB cannot block polyglot HTML/JS: " + polyglot_name); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html b/testing/web-platform/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html new file mode 100644 index 0000000000..c8a90c79b3 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<!-- Test verifies that cross-origin blob URIs are blocked both with and + without CORB. +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> +<script> +async_test(function(t) { + function step1_createSubframe() { + addEventListener("message", function(e) { + t.step(function() { step2_processSubframeMsg(e.data); }) + }); + var subframe = document.createElement("iframe") + // www1 is cross-origin, to ensure that the received blob will be cross-origin. + subframe.src = 'http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html'; + document.body.appendChild(subframe); + } + + function step2_processSubframeMsg(msg) { + assert_false(msg.hasOwnProperty('error'), 'unexpected property found: "error"'); + assert_equals(msg.blob_type, 'text/html'); + assert_equals(msg.blob_size, 147); + + // With and without CORB loading of a cross-origin blob should be blocked + // (this is verified by expecting |script.onerror|, but not |script.onload| + // below). + var script = document.createElement("script") + script.src = msg.blob_url; + script.onerror = t.step_func_done(function(){}) + script.onload = t.unreached_func("Unexpected load event") + document.body.appendChild(script) + } + + step1_createSubframe(); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html b/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 0000000000..b6bc90964d --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<!-- Test verifies that script mislabeled as html won't execute with and without CORB + if the nosniff response header is present. + + The expected behavior is covered by the Fetch spec at + https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? + + See also the following tests: + - fetch/nosniff/importscripts.html + - fetch/nosniff/script.html + - fetch/nosniff/worker.html +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +setup({ single_test: true }); +window.has_executed_script = false; +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/js-mislabeled-as-html-nosniff.js"> +</script> + +<script> +// Verify what observable effects the <script> tag above had. +// Assertion should hold with and without CORB: +assert_false(window.has_executed_script, + 'The cross-origin script should not be executed'); +done(); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html.sub.html b/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html.sub.html new file mode 100644 index 0000000000..44cb1f8659 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-js-mislabeled-as-html.sub.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<!-- Test verifies that script mislabeled as html will execute with and without + CORB (CORB should allow the script after sniffing). +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +setup({ single_test: true }); +window.has_executed_script = false; +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/js-mislabeled-as-html.js"> +</script> + +<script> +// Verify what observable effects the <script> tag above had. +// Assertion should hold with and without CORB: +assert_true(window.has_executed_script, + 'The cross-origin script should execute'); +done(); +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/testing/web-platform/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html new file mode 100644 index 0000000000..f0eb1f0ab1 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Test verifies CORB will block responses beginning with a JSON parser + breaker regardless of their MIME type (excluding text/css - see below). + + A JSON parser breaker is a prefix added to resources with sensitive data to + prevent cross-site script inclusion (XSSI) and similar attacks. For example, + it may be included in JSON files to prevent them from leaking data via a + <script> tag, making the response only useful to a fetch or XmlHttpRequest. + See also https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md#Protecting-JSON + + The assumption is that all images, other media, scripts, fonts and other + resources that may be embedded cross-origin will never begin with a JSON + parser breaker. For example an JPEG image should always being with FF D8 FF, + a PNG image with 89 50 4E 47 0D 0A 1A 0A bytes and an SVG image with "<?xml" + substring. + + The assumption above excludes text/css which (as shown by + style-css-with-json-parser-breaker.sub.html) can parse as valid stylesheet + even in presence of a JSON parser breaker. +--> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> +<script> +setup({allow_uncaught_exception : true}); + +// A subset of JSON security prefixes (only ones that are parser breakers). +json_parser_breakers = [ + ")]}'", + "{}&&", + "{} &&", +] + +// JSON parser breaker should trigger CORB blocking for any Content-Type - even +// for resources that claim to be of a MIME type that is normally allowed to be +// embedded in cross-origin documents (like images and/or scripts). +mime_types = [ + // CORB-protected MIME types + "text/html", + "text/xml", + "text/json", + "text/plain", + + // MIME types that normally are allowed by CORB. + "application/javascript", + "image/png", + "image/svg+xml", + + // Other types. + "application/pdf", + "application/zip", +] + +function test(mime_type, body) { + // The test below depends on a global/shared event handler - we need to ensure + // that no tests run in parallel - this is achieved by using `promise_test` + // instead of `async_test`. See also + // https://web-platform-tests.org/writing-tests/testharness-api.html#promise-tests + promise_test(t => new Promise(function(resolve, reject) { + var script = document.createElement("script") + + // Without CORB, the JSON parser breaker would cause a syntax error when + // parsed as JavaScript, but with CORB there should be no errors (because + // CORB will replace the response body with an empty body). With ORB, + // the script loading itself should error out. + script.onload = resolve; + script.onerror = resolve; + addEventListener("error", t.unreached_func( + "Empty body of a CORS-blocked response shouldn't trigger syntax errors.")) + + // www1 is cross-origin, so the HTTP response is CORB-eligible. + var src_prefix = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/sniffable-resource.py"; + script.src = src_prefix + "?type=" + mime_type + "&body=" + encodeURIComponent(body); + document.body.appendChild(script) + }), "CORB-blocks '" + mime_type + "' that starts with the following JSON parser breaker: " + body); +} + +mime_types.forEach(function(type) { + json_parser_breakers.forEach(function(body) { + test(type, body); + }); +}); + +</script> diff --git a/testing/web-platform/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html b/testing/web-platform/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html new file mode 100644 index 0000000000..6d490d55bc --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- Test verifies CORB will block responses with types that do not + require confirmation sniffing. + + We assume that: + 1) it is unlikely that images, other media, scripts, etc. will be mislabelled + as the |protected_mime_types| below, + 2) the |protected_mime_types| below are likely to contain sensitive, + credentialled data. +--> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<div id=log></div> +<script> +setup({allow_uncaught_exception : true, single_test : true}); + +function test(mime_type, is_blocking_expected) { + var action = is_blocking_expected ? "blocks" : "does not block"; + + async_test(function(t) { + var script = document.createElement("script") + var script_has_run_token = "script_has_run" + token(); + + // With and without CORB there should be no error, but without CORB the + // original script body will be preserved and |window.script_has_run| will + // be set. + window[script_has_run_token] = false; + script.onload = t.step_func_done(function(){ + if (is_blocking_expected) { + assert_false(window[script_has_run_token]); + } else { + assert_true(window[script_has_run_token]); + } + }); + addEventListener("error",function(e) { + t.step(function() { + assert_unreached("Unexpected error: " + e); + t.done(); + }) + }); + + // www1 is cross-origin, so the HTTP response is CORB-eligible. + var src_prefix = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/sniffable-resource.py"; + body = `window['${script_has_run_token}'] = true;` + script.src = src_prefix + "?type=" + mime_type + "&body=" + encodeURIComponent(body); + document.body.appendChild(script) + }, "CORB " + action + " '" + mime_type + "'"); +} + +// Some mime types should be protected by CORB without any kind +// of confirmation sniffing. +protected_mime_types = [ + "application/gzip", + "application/pdf", + "application/x-gzip", + "application/x-protobuf", + "application/zip", + "multipart/byteranges", + "multipart/signed", + "text/csv", + "text/event-stream", +] +protected_mime_types.forEach(function(type) { + test(type, true /* is_blocking_expected */); +}); + +// Other mime types. +other_mime_types = [ + // These content types are legitimately allowed in 'no-cors' fetches. + "application/javascript", + + // Confirmation sniffing will fail and prevent CORB from blocking the + // response. + "text/html", + + // Unrecognized content types. + "application/blah" +] +other_mime_types.forEach(function(type) { + test(type, false /* is_blocking_expected */); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html b/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 0000000000..8fef0dc59e --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- Test verifies that a stylesheet mislabeled as html won't execute with and + without CORB if the nosniff response header is present. + + The expected behavior is covered by the Fetch spec at + https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? + + See also the following tests: + - fetch/nosniff/stylesheet.html +--> +<meta charset="utf-8"> +<title>CSS is not applied (because of nosniff + non-text/css headers)</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<!-- Default style that will be applied if the external stylesheet resource + below won't load for any reason. This stylesheet will set h1's + color to green (see |default_color| below). --> +<style> +h1 { color: green; } +</style> + +<!-- This stylesheet (if loaded) should set h1#header's color to red + (see |external_color| below). --> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<link rel="stylesheet" type="text/css" + href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-mislabeled-as-html-nosniff.css"> + +<body> + <h1 id="header">Header example</h1> + <p>Paragraph body</p> +</body> + +<script> +test(() => { + let style = getComputedStyle(document.getElementById('header')); + const external_color = 'rgb(255, 0, 0)'; // red + const default_color = 'rgb(0, 128, 0)'; // green + assert_equals(style.getPropertyValue('color'), default_color); + assert_not_equals(style.getPropertyValue('color'), external_color); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html.sub.html b/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html.sub.html new file mode 100644 index 0000000000..4f0b4c22f5 --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/style-css-mislabeled-as-html.sub.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<!-- Test verifies that CORB won't impact a cross-origin stylesheet mislabeled + as text/html (because even without CORB mislabeled CSS will be rejected). +--> +<meta charset="utf-8"> +<title>CSS is not applied (because of strict content-type enforcement for cross-origin stylesheets)</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<!-- Default style that will be applied if the external stylesheet resource + below won't load for any reason. This stylesheet will set h1's + color to green (see |default_color| below). --> +<style> +h1 { color: green; } +</style> + +<!-- This stylesheet (if loaded) should set h1#header's color to red + (see |external_color| below). --> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<link rel="stylesheet" type="text/css" + href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-mislabeled-as-html.css"> + +<body> + <h1 id="header">Header example</h1> + <p>Paragraph body</p> +</body> + +<script> +test(() => { + let style = getComputedStyle(document.getElementById('header')); + const external_color = 'rgb(255, 0, 0)'; // red + const default_color = 'rgb(0, 128, 0)'; // green + assert_equals(style.getPropertyValue('color'), default_color); + assert_not_equals(style.getPropertyValue('color'), external_color); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html b/testing/web-platform/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html new file mode 100644 index 0000000000..29ed586a4f --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<!-- Test verifies that CORB won't block a stylesheet that + 1) is correctly labeled with text/css Content-Type and parsing fine as text/css + 2) starts with a JSON parser breaker (like )]}') +--> +<meta charset="utf-8"> +<title>CORB doesn't block a stylesheet that has a proper Content-Type and begins with a JSON parser breaker</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<!-- Default style that will be applied if the external stylesheet resource + below won't load for any reason. This stylesheet will set h1's + color to green (see |default_color| below). --> +<style> +h1 { color: green; } +</style> + +<!-- This stylesheet (if loaded) should set h1#header's color to red + (see |external_color| below). --> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<link rel="stylesheet" type="text/css" + href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/css-with-json-parser-breaker.css"> + +<body> + <h1 id="header">Header example</h1> + <p>Paragraph body</p> +</body> + +<script> +test(() => { + // Verify that CSS got applied / did not get blocked by CORB. + let style = getComputedStyle(document.getElementById('header')); + const external_color = 'rgb(255, 0, 0)'; // red + const default_color = 'rgb(0, 128, 0)'; // green + assert_equals(style.getPropertyValue('color'), external_color); + assert_not_equals(style.getPropertyValue('color'), default_color); +}); +</script> diff --git a/testing/web-platform/tests/fetch/corb/style-html-correctly-labeled.sub.html b/testing/web-platform/tests/fetch/corb/style-html-correctly-labeled.sub.html new file mode 100644 index 0000000000..cdefcd2d2c --- /dev/null +++ b/testing/web-platform/tests/fetch/corb/style-html-correctly-labeled.sub.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- Test verifies that using a HTML document as a stylesheet has no observable + differences with and without CORB: + - The cross-origin stylesheet requires a correct text/css Content-Type + and therefore won't render even without CORB. This aspect of this test + is similar to the style-css-mislabeled-as-html.sub.html test. + - Even if the Content-Type requirements were relaxed for cross-origin stylesheets, + the HTML document is unlikely to parse as a stylesheet (unless a polyglot + HTML/CSS document is crafted as part of an attack) and therefore the + observable behavior should be indistinguishable from parsing the empty, + CORB-blocked response as a stylesheet. +--> +<meta charset="utf-8"> +<title>CSS is not applied (because of mismatched Content-Type header)</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<!-- Default style that will be applied if the external stylesheet resource + below won't load for any reason. This stylesheet will set h1's + color to green (see |default_color| below). --> +<style> +h1 { color: green; } +</style> + +<!-- This is not really a stylesheet... --> +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<link rel="stylesheet" type="text/css" + href="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/html-correctly-labeled.html"> + +<body> + <h1 id="header">Header example</h1> + <p>Paragraph body</p> +</body> + +<script> +test(() => { + var style = getComputedStyle(document.getElementById('header')); + const default_color = 'rgb(0, 128, 0)'; // green + assert_equals(style.getPropertyValue('color'), default_color); +}); +</script> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html new file mode 100644 index 0000000000..cc6a3a81bc --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <script> +const host = get_host_info(); +const remoteBaseURL = host.HTTP_REMOTE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; +const localBaseURL = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; + +function with_iframe(url) +{ + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +function loadIFrameAndFetch(iframeURL, fetchURL, expectedFetchResult, title) +{ + promise_test(async () => { + const frame = await with_iframe(iframeURL); + let receiveMessage; + const promise = new Promise((resolve, reject) => { + receiveMessage = (event) => { + if (event.data !== expectedFetchResult) { + reject("Received unexpected message " + event.data); + return; + } + resolve(); + } + window.addEventListener("message", receiveMessage, false); + }); + frame.contentWindow.postMessage(fetchURL, "*"); + return promise.finally(() => { + frame.remove(); + window.removeEventListener("message", receiveMessage, false); + }); + }, title); +} + +// This above data URL should be equivalent to resources/iframeFetch.html +var dataIFrameURL = "data:text/html;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxzY3JpcHQ+CiAgICAgICAgZnVuY3Rpb24gcHJvY2Vzc01lc3NhZ2UoZXZlbnQpCiAgICAgICAgewogICAgICAgICAgICBmZXRjaChldmVudC5kYXRhLCB7IG1vZGU6ICJuby1jb3JzIiB9KS50aGVuKCgpID0+IHsKICAgICAgICAgICAgICAgIHBhcmVudC5wb3N0TWVzc2FnZSgib2siLCAiKiIpOwogICAgICAgICAgICB9LCAoKSA9PiB7CiAgICAgICAgICAgICAgICBwYXJlbnQucG9zdE1lc3NhZ2UoImtvIiwgIioiKTsKICAgICAgICAgICAgfSk7CiAgICAgICAgfQogICAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCJtZXNzYWdlIiwgcHJvY2Vzc01lc3NhZ2UsIGZhbHNlKTsKICAgIDwvc2NyaXB0Pgo8L2hlYWQ+Cjxib2R5PgogICAgPGgzPlRoZSBpZnJhbWUgbWFraW5nIGEgc2FtZSBvcmlnaW4gZmV0Y2ggY2FsbC48L2gzPgo8L2JvZHk+CjwvaHRtbD4K"; + +loadIFrameAndFetch(dataIFrameURL, localBaseURL + "resources/hello.py?corp=same-origin", "ko", + "Cross-origin fetch in a data: iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadIFrameAndFetch(dataIFrameURL, localBaseURL + "resources/hello.py?corp=same-site", "ko", + "Cross-origin fetch in a data: iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadIFrameAndFetch(remoteBaseURL + "resources/iframeFetch.html", localBaseURL + "resources/hello.py?corp=same-origin", "ko", + "Cross-origin fetch in a cross origin iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadIFrameAndFetch(notSameSiteBaseURL + "resources/iframeFetch.html", localBaseURL + "resources/hello.py?corp=same-site", "ko", + "Cross-origin fetch in a cross origin iframe load fails if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadIFrameAndFetch(remoteBaseURL + "resources/iframeFetch.html", remoteBaseURL + "resources/hello.py?corp=same-origin", "ok", + "Same-origin fetch in a cross origin iframe load succeeds if the server blocks cross-origin loads with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.any.js b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.any.js new file mode 100644 index 0000000000..64a7bfeb86 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.any.js @@ -0,0 +1,76 @@ +// META: timeout=long +// META: global=window,dedicatedworker,sharedworker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTP_ORIGIN + path; +const sameSiteBaseURL = "http://" + host.ORIGINAL_HOST + ":" + host.HTTP_PORT2 + path; +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + path; +const httpsBaseURL = host.HTTPS_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-scheme (HTTP to HTTPS) no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const remoteSameSiteURL = sameSiteBaseURL + "resources/hello.py?corp=same-site"; + + await fetch(remoteSameSiteURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(sameSiteBaseURL + "resources/hello.py?corp=same-origin", { mode: "no-cors" })); +}, "Valid cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.https.any.js b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.https.any.js new file mode 100644 index 0000000000..c9b5b7502f --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/fetch.https.any.js @@ -0,0 +1,56 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTPS_ORIGIN + path; +const notSameSiteBaseURL = host.HTTPS_NOTSAMESITE_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/iframe-loads.html b/testing/web-platform/tests/fetch/cross-origin-resource-policy/iframe-loads.html new file mode 100644 index 0000000000..63902c302b --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/iframe-loads.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <script> +const host = get_host_info(); +const remoteBaseURL = host.HTTP_REMOTE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; +const localBaseURL = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; + +function with_iframe(url) { + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +promise_test(async() => { + const url = remoteBaseURL + "resources/iframe.py?corp=same-origin"; + + await new Promise((resolve, reject) => { + return fetch(url, { mode: "no-cors" }).then(reject, resolve); + }); + + const iframe = await with_iframe(url); + return new Promise((resolve, reject) => { + window.addEventListener("message", (event) => { + if (event.data !== "pong") { + reject(event.data); + return; + } + resolve(); + }, false); + iframe.contentWindow.postMessage("ping", "*"); + }).finally(() => { + iframe.remove(); + }); +}, "Load an iframe that has Cross-Origin-Resource-Policy header"); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/image-loads.html b/testing/web-platform/tests/fetch/cross-origin-resource-policy/image-loads.html new file mode 100644 index 0000000000..060b7551ea --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/image-loads.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <div id="testDiv"></div> + <script> +const host = get_host_info(); +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; +const ok = true; +const ko = false; +const noCors = false; + +function loadImage(url, shoudLoad, corsMode, title) +{ + const testDiv = document.getElementById("testDiv"); + promise_test(() => { + const img = new Image(); + if (corsMode) + img.crossOrigin = corsMode; + img.src = url; + return new Promise((resolve, reject) => { + img.onload = shoudLoad ? resolve : reject; + img.onerror = shoudLoad ? reject : resolve; + testDiv.appendChild(img); + }).finally(() => { + testDiv.innerHTML = ""; + }); + }, title); +} + +loadImage("./resources/image.py?corp=same-origin", ok, noCors, + "Same-origin image load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadImage("./resources/image.py?corp=same-site", ok, noCors, + "Same-origin image load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-origin&acao=*", ok, "anonymous", + "Cross-origin cors image load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-site&acao=*", ok, "anonymous", + "Cross-origin cors image load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-origin&acao=*", ko, noCors, + "Cross-origin no-cors image load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadImage(notSameSiteBaseURL + "resources/image.py?corp=same-site&acao=*", ko, noCors, + "Cross-origin no-cors image load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/green.png b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/green.png Binary files differnew file mode 100644 index 0000000000..28a1faab37 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/green.png diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/hello.py b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/hello.py new file mode 100644 index 0000000000..2b1cb84bad --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/hello.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] + if b'origin' in request.headers: + headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin'])) + + return 200, headers, b"hello" diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframe.py b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframe.py new file mode 100644 index 0000000000..815ecf5927 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframe.py @@ -0,0 +1,5 @@ +def main(request, response): + headers = [(b"Content-Type", b"text/html"), + (b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] + return 200, headers, b"<body><h3>The iframe</h3><script>window.onmessage = () => { parent.postMessage('pong', '*'); }</script></body>" + diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html new file mode 100644 index 0000000000..257185805d --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> + <script> + function processMessage(event) + { + fetch(event.data, { mode: "no-cors" }).then(() => { + parent.postMessage("ok", "*"); + }, () => { + parent.postMessage("ko", "*"); + }); + } + window.addEventListener("message", processMessage, false); + </script> +</head> +<body> + <h3>The iframe making a same origin fetch call.</h3> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/image.py b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/image.py new file mode 100644 index 0000000000..2a779cf11b --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/image.py @@ -0,0 +1,22 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + type = request.GET.first(b"type", None) + + body = open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"green.png"), u"rb").read() + + response.add_required_headers = False + response.writer.write_status(200) + + if b'corp' in request.GET: + response.writer.write_header(b"cross-origin-resource-policy", request.GET[b'corp']) + if b'acao' in request.GET: + response.writer.write_header(b"access-control-allow-origin", request.GET[b'acao']) + response.writer.write_header(b"content-length", len(body)) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(body) diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/redirect.py b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/redirect.py new file mode 100644 index 0000000000..0dad4dd923 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/redirect.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [(b"Location", request.GET[b'redirectTo'])] + if b'corp' in request.GET: + headers.append((b'Cross-Origin-Resource-Policy', request.GET[b'corp'])) + + return 302, headers, b"" diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/script.py b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/script.py new file mode 100644 index 0000000000..58f8d34154 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/resources/script.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] + if b'origin' in request.headers: + headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin'])) + + return 200, headers, b"" diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js b/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js new file mode 100644 index 0000000000..8f6338176a --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js @@ -0,0 +1,7 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + return promise_rejects_js(t, + TypeError, + fetch(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=same-site", { mode: "no-cors" })); +}, "Cross-Origin-Resource-Policy: same-site blocks retrieving HTTPS from HTTP"); diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js b/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js new file mode 100644 index 0000000000..4c74571874 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js @@ -0,0 +1,13 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + const img = new Image(); + img.src = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/image.py?corp=same-site"; + return new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + document.body.appendChild(img); + }).finally(() => { + img.remove(); + }); +}, "Cross-Origin-Resource-Policy does not block Mixed Content <img>"); diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/script-loads.html b/testing/web-platform/tests/fetch/cross-origin-resource-policy/script-loads.html new file mode 100644 index 0000000000..a9690fc70b --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/script-loads.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> + <div id="testDiv"></div> + <script> +const host = get_host_info(); +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; +const ok = true; +const ko = false; +const noCors = false; + +function loadScript(url, shoudLoad, corsMode, title) +{ + const testDiv = document.getElementById("testDiv"); + promise_test(() => { + const script = document.createElement("script"); + if (corsMode) + script.crossOrigin = corsMode; + script.src = url; + return new Promise((resolve, reject) => { + script.onload = shoudLoad ? resolve : reject; + script.onerror = shoudLoad ? reject : resolve; + testDiv.appendChild(script); + }); + }, title); +} + +loadScript("./resources/script.py?corp=same-origin", ok, noCors, + "Same-origin script load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadScript("./resources/script.py?corp=same-site", ok, noCors, + "Same-origin script load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-origin&acao=*", ok, "anonymous", + "Cross-origin cors script load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-site&acao=*", ok, "anonymous", + "Cross-origin cors script load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-origin&acao=*", ko, noCors, + "Cross-origin no-cors script load with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +loadScript(notSameSiteBaseURL + "resources/script.py?corp=same-site&acao=*", ko, noCors, + "Cross-origin no-cors script load with a 'Cross-Origin-Resource-Policy: same-site' response header."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/cross-origin-resource-policy/syntax.any.js b/testing/web-platform/tests/fetch/cross-origin-resource-policy/syntax.any.js new file mode 100644 index 0000000000..dc874977a6 --- /dev/null +++ b/testing/web-platform/tests/fetch/cross-origin-resource-policy/syntax.any.js @@ -0,0 +1,19 @@ +// META: script=/common/get-host-info.sub.js + +const crossOriginURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp="; + +[ + "same", + "same, same-origin", + "SAME-ORIGIN", + "Same-Origin", + "same-origin, <>", + "same-origin, same-origin", + "https://www.example.com", // See https://github.com/whatwg/fetch/issues/760 +].forEach(incorrectHeaderValue => { + // Note: an incorrect value results in a successful load, so this test is only meaningful in + // implementations with support for the header. + promise_test(t => { + return fetch(crossOriginURL + encodeURIComponent(incorrectHeaderValue), { mode: "no-cors" }); + }, "Parsing Cross-Origin-Resource-Policy: " + incorrectHeaderValue); +}); diff --git a/testing/web-platform/tests/fetch/data-urls/README.md b/testing/web-platform/tests/fetch/data-urls/README.md new file mode 100644 index 0000000000..1ce5b18b53 --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/README.md @@ -0,0 +1,11 @@ +## data: URLs + +`resources/data-urls.json` contains `data:` URL tests. The tests are encoded as a JSON array. Each value in the array is an array of two or three values. The first value describes the input, the second value describes the expected MIME type, null if the input is expected to fail somehow, or the empty string if the expected value is `text/plain;charset=US-ASCII`. The third value, if present, describes the expected body as an array of integers representing bytes. + +These tests are used for `data:` URLs in this directory (see `processing.any.js`). + +## Forgiving-base64 decode + +`resources/base64.json` contains [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) tests. The tests are encoded as a JSON array. Each value in the array is an array of two values. The first value describes the input, the second value describes the output as an array of integers representing bytes or null if the input cannot be decoded. + +These tests are used for `data:` URLs in this directory (see `base64.any.js`) and `window.atob()` in `../../html/webappapis/atob/base64.html`. diff --git a/testing/web-platform/tests/fetch/data-urls/base64.any.js b/testing/web-platform/tests/fetch/data-urls/base64.any.js new file mode 100644 index 0000000000..83f34db177 --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/base64.any.js @@ -0,0 +1,18 @@ +// META: global=window,worker + +promise_test(() => fetch("resources/base64.json").then(res => res.json()).then(runBase64Tests), "Setup."); +function runBase64Tests(tests) { + for(let i = 0; i < tests.length; i++) { + const input = tests[i][0], + output = tests[i][1], + dataURL = "data:;base64," + input; + promise_test(t => { + if(output === null) { + return promise_rejects_js(t, TypeError, fetch(dataURL)); + } + return fetch(dataURL).then(res => res.arrayBuffer()).then(body => { + assert_array_equals(new Uint8Array(body), output); + }); + }, "data: URL base64 handling: " + format_value(input)); + } +} diff --git a/testing/web-platform/tests/fetch/data-urls/navigate.window.js b/testing/web-platform/tests/fetch/data-urls/navigate.window.js new file mode 100644 index 0000000000..b532a00683 --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/navigate.window.js @@ -0,0 +1,75 @@ +// META: timeout=long +// +// Test some edge cases around navigation to data: URLs to ensure they use the same code path + +[ + { + input: "data:text/html,<script>parent.postMessage(1, '*')</script>", + result: 1, + name: "Nothing fancy", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMiwgJyonKTwvc2NyaXB0Pg==", + result: 2, + name: "base64", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNCwgJyonKTwvc2NyaXB0Pr+/", + result: 4, + name: "base64 with code points that differ from base64url" + }, + { + input: "data:text/html;base64,PHNjcml%09%20%20%0A%0C%0DwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNiwgJyonKTwvc2NyaXB0Pg==", + result: 6, + name: "ASCII whitespace in the input is removed" + } +].forEach(({ input, result, name }) => { + // Use promise_test so they go sequentially + promise_test(async t => { + const event = await new Promise((resolve, reject) => { + self.addEventListener("message", t.step_func(resolve), { once: true }); + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + + // The assumption is that postMessage() is quicker + t.step_timeout(reject, 500); + frame.src = input; + }); + assert_equals(event.data, result); + }, name); +}); + +// Failure cases +[ + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMywgJyonKTwvc2NyaXB0Pg=", + name: "base64 with incorrect padding", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNSwgJyonKTwvc2NyaXB0Pr-_", + name: "base64url is not supported" + }, + { + input: "data:text/html;base64,%0BPHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNywgJyonKTwvc2NyaXB0Pg==", + name: "Vertical tab in the input leads to an error" + } +].forEach(({ input, name }) => { + // Continue to use promise_test so they go sequentially + promise_test(async t => { + const event = await new Promise((resolve, reject) => { + self.addEventListener("message", t.step_func(reject), { once: true }); + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + + // The assumption is that postMessage() is quicker + t.step_timeout(resolve, 500); + frame.src = input; + }); + }, name); +}); + +// I found some of the interesting code point cases above through brute force: +// +// for (i = 0; i < 256; i++) { +// w(btoa("<script>parent.postMessage(5, '*')<\/script>" + String.fromCodePoint(i) + String.fromCodePoint(i))); +// } diff --git a/testing/web-platform/tests/fetch/data-urls/processing.any.js b/testing/web-platform/tests/fetch/data-urls/processing.any.js new file mode 100644 index 0000000000..cec97bd6be --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/processing.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker + +promise_test(() => fetch("resources/data-urls.json").then(res => res.json()).then(runDataURLTests), "Setup."); +function runDataURLTests(tests) { + for(let i = 0; i < tests.length; i++) { + const input = tests[i][0], + expectedMimeType = tests[i][1], + expectedBody = expectedMimeType !== null ? tests[i][2] : null; + promise_test(t => { + if(expectedMimeType === null) { + return promise_rejects_js(t, TypeError, fetch(input)); + } else { + return fetch(input).then(res => { + return res.arrayBuffer().then(body => { + assert_array_equals(new Uint8Array(body), expectedBody); + assert_equals(res.headers.get("content-type"), expectedMimeType); // We could assert this earlier, but this fails often + }); + }); + } + }, format_value(input)); + } +} diff --git a/testing/web-platform/tests/fetch/data-urls/resources/base64.json b/testing/web-platform/tests/fetch/data-urls/resources/base64.json new file mode 100644 index 0000000000..01f981a650 --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/resources/base64.json @@ -0,0 +1,82 @@ +[ + ["", []], + ["abcd", [105, 183, 29]], + [" abcd", [105, 183, 29]], + ["abcd ", [105, 183, 29]], + [" abcd===", null], + ["abcd=== ", null], + ["abcd ===", null], + ["a", null], + ["ab", [105]], + ["abc", [105, 183]], + ["abcde", null], + ["𐀀", null], + ["=", null], + ["==", null], + ["===", null], + ["====", null], + ["=====", null], + ["a=", null], + ["a==", null], + ["a===", null], + ["a====", null], + ["a=====", null], + ["ab=", null], + ["ab==", [105]], + ["ab===", null], + ["ab====", null], + ["ab=====", null], + ["abc=", [105, 183]], + ["abc==", null], + ["abc===", null], + ["abc====", null], + ["abc=====", null], + ["abcd=", null], + ["abcd==", null], + ["abcd===", null], + ["abcd====", null], + ["abcd=====", null], + ["abcde=", null], + ["abcde==", null], + ["abcde===", null], + ["abcde====", null], + ["abcde=====", null], + ["=a", null], + ["=a=", null], + ["a=b", null], + ["a=b=", null], + ["ab=c", null], + ["ab=c=", null], + ["abc=d", null], + ["abc=d=", null], + ["ab\u000Bcd", null], + ["ab\u3000cd", null], + ["ab\u3001cd", null], + ["ab\tcd", [105, 183, 29]], + ["ab\ncd", [105, 183, 29]], + ["ab\fcd", [105, 183, 29]], + ["ab\rcd", [105, 183, 29]], + ["ab cd", [105, 183, 29]], + ["ab\u00a0cd", null], + ["ab\t\n\f\r cd", [105, 183, 29]], + [" \t\n\f\r ab\t\n\f\r cd\t\n\f\r ", [105, 183, 29]], + ["ab\t\n\f\r =\t\n\f\r =\t\n\f\r ", [105]], + ["A", null], + ["/A", [252]], + ["//A", [255, 240]], + ["///A", [255, 255, 192]], + ["////A", null], + ["/", null], + ["A/", [3]], + ["AA/", [0, 15]], + ["AAAA/", null], + ["AAA/", [0, 0, 63]], + ["\u0000nonsense", null], + ["abcd\u0000nonsense", null], + ["YQ", [97]], + ["YR", [97]], + ["~~", null], + ["..", null], + ["--", null], + ["__", null] +] diff --git a/testing/web-platform/tests/fetch/data-urls/resources/data-urls.json b/testing/web-platform/tests/fetch/data-urls/resources/data-urls.json new file mode 100644 index 0000000000..f318d1f3e5 --- /dev/null +++ b/testing/web-platform/tests/fetch/data-urls/resources/data-urls.json @@ -0,0 +1,214 @@ +[ + ["data://test/,X", + "text/plain;charset=US-ASCII", + [88]], + ["data://test:test/,X", + null], + ["data:,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:", + null], + ["data:text/html", + null], + ["data:text/html ;charset=x ", + null], + ["data:,", + "text/plain;charset=US-ASCII", + []], + ["data:,X#X", + "text/plain;charset=US-ASCII", + [88]], + ["data:,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/plain,X", + "text/plain", + [88]], + ["data:text/plain ,X", + "text/plain", + [88]], + ["data:text/plain%20,X", + "text/plain%20", + [88]], + ["data:text/plain\f,X", + "text/plain%0c", + [88]], + ["data:text/plain%0C,X", + "text/plain%0c", + [88]], + ["data:text/plain;,X", + "text/plain", + [88]], + ["data:;x=x;charset=x,X", + "text/plain;x=x;charset=x", + [88]], + ["data:;x=x,X", + "text/plain;x=x", + [88]], + ["data:text/plain;charset=windows-1252,%C2%B1", + "text/plain;charset=windows-1252", + [194, 177]], + ["data:text/plain;Charset=UTF-8,%C2%B1", + "text/plain;charset=UTF-8", + [194, 177]], + ["data:text/plain;charset=windows-1252,áñçə💩", + "text/plain;charset=windows-1252", + [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]], + ["data:text/plain;charset=UTF-8,áñçə💩", + "text/plain;charset=UTF-8", + [195, 161, 195, 177, 195, 167, 201, 153, 240, 159, 146, 169]], + ["data:image/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif,%C2%B1", + "image/gif", + [194, 177]], + ["data:IMAGE/gif;hi=x,%C2%B1", + "image/gif;hi=x", + [194, 177]], + ["data:IMAGE/gif;CHARSET=x,%C2%B1", + "image/gif;charset=x", + [194, 177]], + ["data: ,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%20,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\f,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%1F,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:\u0000,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:%00,%FF", + "text/plain;charset=US-ASCII", + [255]], + ["data:text/html ,X", + "text/html", + [88]], + ["data:text / html,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:†/†,X", + "%e2%80%a0/%e2%80%a0", + [88]], + ["data:X,X", + "text/plain;charset=US-ASCII", + [88]], + ["data:image/png,X X", + "image/png", + [88, 32, 88]], + ["data:application/javascript,X X", + "application/javascript", + [88, 32, 88]], + ["data:application/xml,X X", + "application/xml", + [88, 32, 88]], + ["data:text/javascript,X X", + "text/javascript", + [88, 32, 88]], + ["data:text/plain,X X", + "text/plain", + [88, 32, 88]], + ["data:unknown/unknown,X X", + "unknown/unknown", + [88, 32, 88]], + ["data:text/plain;a=\",\",X", + "text/plain;a=\"\"", + [34, 44, 88]], + ["data:text/plain;a=%2C,X", + "text/plain;a=%2C", + [88]], + ["data:;base64;base64,WA", + "text/plain", + [88]], + ["data:x/x;base64;base64,WA", + "x/x", + [88]], + ["data:x/x;base64;charset=x,WA", + "x/x;charset=x", + [87, 65]], + ["data:x/x;base64;charset=x;base64,WA", + "x/x;charset=x", + [88]], + ["data:x/x;base64;base64x,WA", + "x/x", + [87, 65]], + ["data:;base64,W%20A", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64,W%0CA", + "text/plain;charset=US-ASCII", + [88]], + ["data:x;base64x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64;x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:x;base64=x,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:; base64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data: ;charset=x ; base64,WA", + "text/plain;charset=x", + [88]], + ["data:;base64;,WA", + "text/plain", + [87, 65]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base64 ,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;base 64,WA", + "text/plain", + [87, 65]], + ["data:;BASe64,WA", + "text/plain;charset=US-ASCII", + [88]], + ["data:;%62ase64,WA", + "text/plain", + [87, 65]], + ["data:%3Bbase64,WA", + "text/plain;charset=US-ASCII", + [87, 65]], + ["data:;charset=x,X", + "text/plain;charset=x", + [88]], + ["data:; charset=x,X", + "text/plain;charset=x", + [88]], + ["data:;charset =x,X", + "text/plain", + [88]], + ["data:;charset= x,X", + "text/plain;charset=\" x\"", + [88]], + ["data:;charset=,X", + "text/plain", + [88]], + ["data:;charset,X", + "text/plain", + [88]], + ["data:;charset=\"x\",X", + "text/plain;charset=x", + [88]], + ["data:;CHARSET=\"X\",X", + "text/plain;charset=X", + [88]] +] diff --git a/testing/web-platform/tests/fetch/fetch-later/META.yml b/testing/web-platform/tests/fetch/fetch-later/META.yml new file mode 100644 index 0000000000..f8fd46bec3 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/META.yml @@ -0,0 +1,3 @@ +spec: https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method +suggested_reviewers: + - mingyc diff --git a/testing/web-platform/tests/fetch/fetch-later/README.md b/testing/web-platform/tests/fetch/fetch-later/README.md new file mode 100644 index 0000000000..661e2b9184 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/README.md @@ -0,0 +1,3 @@ +# FetchLater Tests + +These tests cover [FetchLater method](https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method) related behaviors. diff --git a/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js new file mode 100644 index 0000000000..18b368066b --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js @@ -0,0 +1,53 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads an iframe that creates a fetchLater request w/ short timeout. + const iframe = await loadScriptAsIframe(` + fetchLater("${url}", {activateAfter: 1000}); // 1s + `); + // Deletes the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should have sent all requests. + await expectBeacon(uuid, {count: 1}); +}, 'fetchLater() sends out based on activateAfter.'); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document enters BFCache. + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Creates a fetchLater request with short timeout. It should be sent out + // even if the document is then put into BFCache. + await rc1.executeScript(url => { + fetchLater(url, {activateAfter: 1000}); // 1s. + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to let page enter BFCache. + const rc2 = await rc1.navigateToNew(); + // Navigate back. + await rc2.historyBack(); + // Verify that the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + await expectBeacon(uuid, {count: 1}); +}, 'fetchLater() sends out based on activateAfter, even if document is in BFCache.'); diff --git a/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js new file mode 100644 index 0000000000..37f72ab89e --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js @@ -0,0 +1,46 @@ +'use strict'; + +test(() => { + assert_throws_js(TypeError, () => fetchLater()); +}, `fetchLater() cannot be called without request.`); + +test(() => { + assert_throws_js(TypeError, () => fetchLater('http://www.google.com')); + assert_throws_js(TypeError, () => fetchLater('file://tmp')); + assert_throws_js(TypeError, () => fetchLater('ssh://example.com')); + assert_throws_js(TypeError, () => fetchLater('wss://example.com')); + assert_throws_js(TypeError, () => fetchLater('about:blank')); + assert_throws_js(TypeError, () => fetchLater(`javascript:alert('');`)); +}, `fetchLater() throws TypeError on non-HTTPS URL.`); + +test(() => { + assert_throws_js( + RangeError, + () => fetchLater('https://www.google.com', {activateAfter: -1})); +}, `fetchLater() throws RangeError on negative activateAfter.`); + +test(() => { + const result = fetchLater('/'); + assert_false(result.activated); +}, `fetchLater()'s return tells the deferred request is not yet sent.`); + +test(() => { + const result = fetchLater('/'); + assert_throws_js(TypeError, () => result.activated = true); +}, `fetchLater() throws TypeError when mutating its returned state.`); + +test(() => { + const controller = new AbortController(); + // Immediately aborts the controller. + controller.abort(); + assert_throws_dom( + 'AbortError', () => fetchLater('/', {signal: controller.signal})); +}, `fetchLater() throws AbortError when its initial abort signal is aborted.`); + +test(() => { + const controller = new AbortController(); + const result = fetchLater('/', {signal: controller.signal}); + assert_false(result.activated); + controller.abort(); + assert_false(result.activated); +}, `fetchLater() does not throw error when it is aborted before sending.`); diff --git a/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js new file mode 100644 index 0000000000..17240db354 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js @@ -0,0 +1,6 @@ +importScripts('/resources/testharness.js'); + +test(() => { + assert_false('fetchLater' in self); +}, `fetchLater() is not supported in worker.`); +done(); diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html new file mode 100644 index 0000000000..38eada4513 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html @@ -0,0 +1,23 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: No Referrer When Downgrade Policy</title> +<meta name='referrer' content='no-referrer-when-downgrade'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_ORIGIN, +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html new file mode 100644 index 0000000000..75e9ece7ba --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: No Referrer Policy</title> +<meta name='referrer' content='no-referrer'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +testReferrerHeader(token(), /*host=*/'', /*expectedReferer=*/""); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html new file mode 100644 index 0000000000..b9f14171ba --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html @@ -0,0 +1,25 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Origin When Cross Origin Policy</title> +<meta name='referrer' content='origin-when-cross-origin'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_ORIGIN, + HTTPS_REMOTE_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL); +testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html new file mode 100644 index 0000000000..ce7abf9203 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html @@ -0,0 +1,23 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Origin Policy</title> +<meta name='referrer' content='origin'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_REMOTE_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html new file mode 100644 index 0000000000..264beddc03 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Same Origin Policy</title> +<meta name='referrer' content='same-origin'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_REMOTE_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), /*host=*/'', REFERRER_URL); +testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, /*expectedReferrer=*/''); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html new file mode 100644 index 0000000000..9133f2496f --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Strict Origin Policy</title> +<meta name='referrer' content='strict-origin'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_REMOTE_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN); +// Note: FetchLater cannot be used for non-secure URL. + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html new file mode 100644 index 0000000000..943d70bbc5 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Strict Origin Policy</title> +<meta name='referrer' content='strict-origin'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_ORIGIN); +// Note: FetchLater cannot be used for non-secure URL. + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html new file mode 100644 index 0000000000..a602e0003a --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<title>FetchLater Referrer Header: Unsafe Url Policy</title> +<meta name='referrer' content='unsafe-url'> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script> +</head> +<body> +<script> + +const { + HTTPS_ORIGIN +} = get_host_info(); + +testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL); +// Note: FetchLater cannot be used for non-secure URL. + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js new file mode 100644 index 0000000000..1e9fed1117 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js @@ -0,0 +1,63 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const { + HTTPS_ORIGIN, + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +async function loadElement(el) { + const loaded = new Promise(resolve => el.onload = resolve); + document.body.appendChild(el); + await loaded; +} + +// `host` may be cross-origin +async function loadFetchLaterIframe(host, targetUrl) { + const url = `${host}/fetch/fetch-later/resources/fetch-later.html?url=${ + encodeURIComponent(targetUrl)}`; + const iframe = document.createElement('iframe'); + iframe.src = url; + await loadElement(iframe); + return iframe; +} + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a blank iframe that fires a fetchLater request. + const iframe = document.createElement('iframe'); + iframe.addEventListener('load', () => { + fetchLater(url, {activateAfter: 0}); + }); + await loadElement(iframe); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A blank iframe can trigger fetchLater.'); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a same-origin iframe that fires a fetchLater request. + await loadFetchLaterIframe(HTTPS_ORIGIN, url); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A same-origin iframe can trigger fetchLater.'); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a same-origin iframe that fires a fetchLater request. + await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, url); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A cross-origin iframe can trigger fetchLater.'); diff --git a/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js new file mode 100644 index 0000000000..93705418f2 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js @@ -0,0 +1,75 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const { + HTTPS_ORIGIN, + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +function fetchLaterPopupUrl(host, targetUrl) { + return `${host}/fetch/fetch-later/resources/fetch-later.html?url=${ + encodeURIComponent(targetUrl)}`; +} + +for (const target of ['', '_blank']) { + for (const features in ['', 'popup', 'popup,noopener']) { + parallelPromiseTest( + async t => { + const uuid = token(); + const url = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + + // Opens a blank popup window that fires a fetchLater request. + const w = window.open( + `javascript: fetchLater("${url}", {activateAfter: 0})`, target, + features); + await new Promise(resolve => w.addEventListener('load', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A blank window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + + parallelPromiseTest( + async t => { + const uuid = token(); + const popupUrl = + fetchLaterPopupUrl(HTTPS_ORIGIN, generateSetBeaconURL(uuid)); + + // Opens a same-origin popup that fires a fetchLater request. + const w = window.open(popupUrl, target, features); + await new Promise(resolve => w.addEventListener('load', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A same-origin window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + + parallelPromiseTest( + async t => { + const uuid = token(); + const popupUrl = fetchLaterPopupUrl( + HTTPS_NOTSAMESITE_ORIGIN, generateSetBeaconURL(uuid)); + + // Opens a cross-origin popup that fires a fetchLater request. + const w = window.open(popupUrl, target, features); + // As events from cross-origin window is not accessible, waiting for + // its message instead. + await new Promise( + resolve => window.addEventListener('message', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A cross-origin window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + } +} diff --git a/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js b/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js new file mode 100644 index 0000000000..c13932e353 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js @@ -0,0 +1,5 @@ +'use strict'; + +test(() => { + assert_false(window.hasOwnProperty('fetchLater')); +}, `fetchLater() is not supported in non-secure context.`); diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js new file mode 100644 index 0000000000..60730e0242 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js @@ -0,0 +1,26 @@ +// META: title=FetchLater: allowed by CSP +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests allowed by Content Security Policy. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', `connect-src 'self' ${HTTPS_NOTSAMESITE_ORIGIN}`); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + fetchLater(url, {activateAfter: 0}); + + await expectBeacon(uuid, {count: 1}); + t.done(); +}, 'FetchLater allowed by CSP should succeed'); diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js new file mode 100644 index 0000000000..b32ddaecfc --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js @@ -0,0 +1,31 @@ +// META: title=FetchLater: blocked by CSP +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests blocked by Content Security Policy are rejected. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', 'connect-src \'self\''); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const cspViolationUrl = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + fetchLater(cspViolationUrl, {activateAfter: 0}); + + await new Promise( + resolve => window.addEventListener('securitypolicyviolation', e => { + assert_equals(e.violatedDirective, 'connect-src'); + resolve(); + })); + t.done(); +}, 'FetchLater blocked by CSP should reject'); diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js new file mode 100644 index 0000000000..3c18727156 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js @@ -0,0 +1,33 @@ +// META: title=FetchLater: redirect blocked by CSP +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long + +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests redirect to URL blocked by Content Security Policy. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', 'connect-src \'self\''); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const cspViolationUrl = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + const url = + `/common/redirect.py?location=${encodeURIComponent(cspViolationUrl)}`; + fetchLater(url, {activateAfter: 0}); + + // TODO(crbug.com/1465781): redirect csp check is handled in browser, of which + // result cannot be populated to renderer at this moment. + await expectBeacon(uuid, {count: 0}); + t.done(); +}, 'FetchLater redirect blocked by CSP should reject'); diff --git a/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js new file mode 100644 index 0000000000..1b5b85563d --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js @@ -0,0 +1,128 @@ +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const kQuotaPerOrigin = 64 * 1024; // 64 kilobytes per spec. +const {ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); + +// Runs a test case that cover a single fetchLater() call with `body` in its +// request payload. The call is not expected to throw any errors. +function fetchLaterPostTest(body, description) { + test(() => { + const controller = new AbortController(); + const result = fetchLater( + '/fetch-later', + {method: 'POST', signal: controller.signal, body: body}); + assert_false(result.activated); + // Release quota taken by the pending request for subsequent tests. + controller.abort(); + }, description); +} + +// Test small payload for each supported data types. +for (const [dataType, skipCharset] of Object.entries( + BeaconDataTypeToSkipCharset)) { + fetchLaterPostTest( + makeBeaconData(generateSequentialData(0, 1024, skipCharset), dataType), + `A fetchLater() call accept small data in POST request of ${dataType}.`); +} + +// Test various size of payloads for the same origin. +for (const dataType in BeaconDataType) { + if (dataType !== BeaconDataType.FormData && + dataType !== BeaconDataType.URLSearchParams) { + // Skips FormData & URLSearchParams, as browser adds extra bytes to them + // in addition to the user-provided content. It is difficult to test a + // request right at the quota limit. + fetchLaterPostTest( + // Generates data that is exactly 64 kilobytes. + makeBeaconData(generatePayload(kQuotaPerOrigin), dataType), + `A single fetchLater() call takes up the per-origin quota for its ` + + `body of ${dataType}.`); + } +} + +// Test empty payload. +for (const dataType in BeaconDataType) { + test( + () => { + assert_throws_js( + TypeError, () => fetchLater('/', {method: 'POST', body: ''})); + }, + `A single fetchLater() call does not accept empty data in POST request ` + + `of ${dataType}.`); +} + +// Test oversized payload. +for (const dataType in BeaconDataType) { + test( + () => { + assert_throws_dom( + 'QuotaExceededError', + () => fetchLater('/fetch-later', { + method: 'POST', + // Generates data that exceeds 64 kilobytes. + body: + makeBeaconData(generatePayload(kQuotaPerOrigin + 1), dataType) + })); + }, + `A single fetchLater() call is not allowed to exceed per-origin quota ` + + `for its body of ${dataType}.`); +} + +// Test accumulated oversized request. +for (const dataType in BeaconDataType) { + test( + () => { + const controller = new AbortController(); + // Makes the 1st call that sends only half of allowed quota. + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType) + }); + + // Makes the 2nd call that sends half+1 of allowed quota. + assert_throws_dom('QuotaExceededError', () => { + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData( + generatePayload(kQuotaPerOrigin / 2 + 1), dataType) + }); + }); + // Release quota taken by the pending requests for subsequent tests. + controller.abort(); + }, + `The 2nd fetchLater() call is not allowed to exceed per-origin quota ` + + `for its body of ${dataType}.`); +} + +// Test various size of payloads across different origins. +for (const dataType in BeaconDataType) { + test( + () => { + const controller = new AbortController(); + // Makes the 1st call that sends only half of allowed quota. + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType) + }); + + // Makes the 2nd call that sends half+1 of allowed quota, but to a + // different origin. + fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/fetch-later`, { + method: 'POST', + signal: controller.signal, + body: + makeBeaconData(generatePayload(kQuotaPerOrigin / 2 + 1), dataType) + }); + // Release quota taken by the pending requests for subsequent tests. + controller.abort(); + }, + `The 2nd fetchLater() call to another origin does not exceed per-origin` + + ` quota for its body of ${dataType}.`); +} diff --git a/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html b/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html new file mode 100644 index 0000000000..b569e1a076 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<head> +</head> +<body> + <script> + const PARAMS = new URL(location.href).searchParams; + const TARGET_URL= decodeURIComponent(PARAMS.get('url')) || ''; + + fetchLater(TARGET_URL, {activateAfter: 0}); + if (window.opener) { + window.opener.postMessage("done", "*"); + } + </script> +</body> diff --git a/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js b/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js new file mode 100644 index 0000000000..374097614a --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js @@ -0,0 +1,39 @@ +'use strict'; + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +const REFERRER_ORIGIN = self.location.origin + '/'; +const REFERRER_URL = self.location.href; + +function testReferrerHeader(id, host, expectedReferrer) { + const url = `${ + host}/beacon/resources/inspect-header.py?header=referer&cmd=put&id=${id}`; + + promise_test(t => { + fetchLater(url, {activateAfter: 0}); + return pollResult(expectedReferrer, id).then(result => { + assert_equals(result, expectedReferrer, 'Correct referrer header result'); + }); + }, `Test referer header ${host}`); +} + +function pollResult(expectedReferrer, id) { + const checkUrl = + `/beacon/resources/inspect-header.py?header=referer&cmd=get&id=${id}`; + + return new Promise(resolve => { + function checkResult() { + fetch(checkUrl).then(response => { + assert_equals( + response.status, 200, 'Inspect header response\'s status is 200'); + let result = response.headers.get('x-request-referer'); + + if (result != undefined) { + resolve(result); + } else { + step_timeout(checkResult.bind(this), 100); + } + }); + } + checkResult(); + }); +} diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js new file mode 100644 index 0000000000..d91c73580a --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js @@ -0,0 +1,183 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +// NOTE: Due to the restriction of WPT runner, the following tests are all run +// with BackgroundSync off, which is different from some browsers, +// e.g. Chrome, default behavior, as the testing infra does not support enabling +// it. + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document enters BFCache. + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Creates a fetchLater request with default config in remote, which should + // only be sent on page discarded (not on entering BFCache). + await rc1.executeScript(url => { + fetchLater(url); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to let page enter BFCache. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // Theoretically, the request should still be pending thus 0 request received. + // However, 1 request is sent, as by default the WPT test runner, e.g. + // content_shell in Chromium, does not enable BackgroundSync permission, + // resulting in forcing request sending on every navigation. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() sends on page entering BFCache if BackgroundSync is off.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // When the remote is put into BFCached, creates a fetchLater request w/ + // activateAfter = 0s. It should be sent out immediately. + await rc1.executeScript(url => { + window.addEventListener('pagehide', e => { + if (e.persisted) { + fetchLater(url, {activateAfter: 0}); + } + }); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `Call fetchLater() when BFCached with activateAfter=0 sends immediately.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document gets discarded + // on navigated away. + const helper = new RemoteContextHelper(); + // Opens a window without BFCache. + const rc1 = await helper.addWindow(); + + // Creates a fetchLater request in remote which should only be sent on + // navigating away. + await rc1.executeScript(url => { + fetchLater(url); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was NOT BFCached. + assert_equals(undefined, await rc1.executeScript(() => { + return window.pageshowEvent; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() sends on navigating away a page w/o BFCache.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document gets discarded + // on navigated away. + const helper = new RemoteContextHelper(); + // Opens a window without BFCache. + const rc1 = await helper.addWindow(); + + // Creates 2 fetchLater requests in remote, and one of them is aborted + // immediately. The other one should only be sent right on navigating away. + await rc1.executeScript(url => { + const controller = new AbortController(); + fetchLater(url, {signal: controller.signal}); + fetchLater(url); + controller.abort(); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was NOT BFCached. + assert_equals(undefined, await rc1.executeScript(() => { + return window.pageshowEvent; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() does not send aborted request on navigating away a page w/o BFCache.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const options = {activateAfter: 60000}; + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Creates a fetchLater request in remote which should only be sent on + // navigating away. + await rc1.executeScript((url) => { + // Sets activateAfter = 1m to indicate the request should NOT be sent out + // immediately. + fetchLater(url, {activateAfter: 60000}); + // Adds a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // Theoretically, the request should still be pending thus 0 request received. + // However, 1 request is sent, as by default the WPT test runner, e.g. + // content_shell in Chromium, does not enable BackgroundSync permission, + // resulting in forcing request sending on every navigation, even if page is + // put into BFCache. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.`); diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js new file mode 100644 index 0000000000..ff8d9520e0 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js @@ -0,0 +1,23 @@ +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads an iframe that creates 2 fetchLater requests. One of them is aborted. + const iframe = await loadScriptAsIframe(` + const url = '${url}'; + const controller = new AbortController(); + fetchLater(url, {signal: controller.signal}); + fetchLater(url, {method: 'POST'}); + controller.abort(); + `); + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should not send the aborted request. + await expectBeacon(uuid, {count: 1}); +}, 'A discarded document does not send an already aborted fetchLater request.'); diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js new file mode 100644 index 0000000000..11e85b31a7 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js @@ -0,0 +1,30 @@ +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const numPerMethod = 20; + const total = numPerMethod * 2; + + // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. + const iframe = await loadScriptAsIframe(` + const url = '${url}'; + for (let i = 0; i < ${numPerMethod}; i++) { + // Changing the URL of each request to avoid HTTP Cache issue. + // See crbug.com/1498203#c17. + fetchLater(url + "&method=GET&i=" + i, + {method: 'GET', activateAfter: 10000}); // 10s + fetchLater(url + "&method=POST&i=" + i, + {method: 'POST', activateAfter: 8000}); // 8s + } + `); + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should have sent all requests. + await expectBeacon(uuid, {count: total}); +}, 'A discarded document sends all its fetchLater requests, no matter how much their activateAfter timeout remain.'); diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js new file mode 100644 index 0000000000..df34ec9ac0 --- /dev/null +++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js @@ -0,0 +1,28 @@ +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const numPerMethod = 20; + const total = numPerMethod * 2; + + // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. + const iframe = await loadScriptAsIframe(` + const url = '${url}'; + for (let i = 0; i < ${numPerMethod}; i++) { + // Changing the URL of each request to avoid HTTP Cache issue. + // See crbug.com/1498203#c17. + fetchLater(url + "&method=GET&i=" + i); + fetchLater(url + "&method=POST&i=" + i, {method: 'POST'}); + } + `); + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should have sent all requests. + await expectBeacon(uuid, {count: total}); +}, 'A discarded document sends all its fetchLater requests.'); diff --git a/testing/web-platform/tests/fetch/h1-parsing/README.md b/testing/web-platform/tests/fetch/h1-parsing/README.md new file mode 100644 index 0000000000..487a892dcf --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/README.md @@ -0,0 +1,5 @@ +This directory tries to document "rough consensus" on where HTTP/1 parsing should end up between browsers. + +Any tests that browsers currently fail should have associated bug reports. + +[whatwg/fetch issue #1156](https://github.com/whatwg/fetch/issues/1156) provides context for this effort and pointers to the various issues, pull requests, and bug reports that are associated with it. diff --git a/testing/web-platform/tests/fetch/h1-parsing/lone-cr.window.js b/testing/web-platform/tests/fetch/h1-parsing/lone-cr.window.js new file mode 100644 index 0000000000..6b46ed632f --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/lone-cr.window.js @@ -0,0 +1,23 @@ +// These tests expect that a network error is returned if there's a CR that is not immediately +// followed by LF before reaching message-body. +// +// No browser does this currently, but Firefox does treat it equivalently to a space which gives +// hope. + +[ + "HTTP/1.1\r200 OK\n\nBODY", + "HTTP/1.1 200\rOK\n\nBODY", + "HTTP/1.1 200 OK\n\rHeader: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader\r: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader:\r Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\r\n\nBody", + "HTTP/1.1 200 OK\nHeader: Value\r\r\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\rHeader2: Value2\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\rBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\r" +].forEach(input => { + promise_test(t => { + const message = encodeURIComponent(input); + return promise_rejects_js(t, TypeError, fetch(`resources/message.py?message=${message}`)); + }, `Parsing response with a lone CR before message-body (${input})`); +}); diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js b/testing/web-platform/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js new file mode 100644 index 0000000000..37a61c12b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js @@ -0,0 +1,31 @@ +async_test(t => { + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + script.src = "resources/script-with-0x00-in-header.py"; + script.onerror = t.step_func_done(); + script.onload = t.unreached_func(); + document.body.append(script); +}, "Expect network error for script with 0x00 in a header"); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "resources/document-with-0x00-in-header.py"; + // If network errors result in load events for frames per + // https://github.com/whatwg/html/issues/125 and https://github.com/whatwg/html/issues/1230 this + // should be changed to use the load event instead. + t.step_timeout(() => { + assert_equals(window.frameLoaded, undefined); + t.done(); + }, 1000); + document.body.append(frame); +}, "Expect network error for frame navigation to resource with 0x00 in a header"); + +async_test(t => { + const img = document.createElement("img"); + t.add_cleanup(() => img.remove()); + img.src = "resources/blue-with-0x00-in-a-header.asis"; + img.onerror = t.step_func_done(); + img.onload = t.unreached_func(); + document.body.append(img); +}, "Expect network error for image with 0x00 in a header"); diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/README.md b/testing/web-platform/tests/fetch/h1-parsing/resources/README.md new file mode 100644 index 0000000000..2175d27408 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/README.md @@ -0,0 +1,6 @@ +`blue-with-0x00-in-a-header.asis` is a copy from `../../images/blue.png` with the following prepended using Control Pictures to signify actual newlines and 0x00: +``` +HTTP/1.1 200 AN IMAGE␍␊ +Content-Type: image/png␍␊ +Custom: ␀␍␊␍␊ +``` diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis b/testing/web-platform/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis Binary files differnew file mode 100644 index 0000000000..102340a631 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py b/testing/web-platform/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py new file mode 100644 index 0000000000..d91998b998 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py @@ -0,0 +1,4 @@ +def main(request, response): + response.headers.set(b"Content-Type", b"text/html") + response.headers.set(b"Custom", b"\0") + return b"<!doctype html><script>top.frameLoaded=true</script><b>This is a document.</b>" diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/message.py b/testing/web-platform/tests/fetch/h1-parsing/resources/message.py new file mode 100644 index 0000000000..640080c18b --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/message.py @@ -0,0 +1,3 @@ +def main(request, response): + response.writer.write(request.GET.first(b"message")) + response.close_connection = True diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py b/testing/web-platform/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py new file mode 100644 index 0000000000..39f58d8270 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py @@ -0,0 +1,4 @@ +def main(request, response): + response.headers.set(b"Content-Type", b"text/javascript") + response.headers.set(b"Custom", b"\0") + return b"var thisIsJavaScript = 0" diff --git a/testing/web-platform/tests/fetch/h1-parsing/resources/status-code.py b/testing/web-platform/tests/fetch/h1-parsing/resources/status-code.py new file mode 100644 index 0000000000..5421893b26 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/resources/status-code.py @@ -0,0 +1,6 @@ +def main(request, response): + output = b"HTTP/1.1 " + output += request.GET.first(b"input") + output += b"\nheader-parsing: is sad\n" + response.writer.write(output) + response.close_connection = True diff --git a/testing/web-platform/tests/fetch/h1-parsing/status-code.window.js b/testing/web-platform/tests/fetch/h1-parsing/status-code.window.js new file mode 100644 index 0000000000..5776cf4050 --- /dev/null +++ b/testing/web-platform/tests/fetch/h1-parsing/status-code.window.js @@ -0,0 +1,98 @@ +[ + { + input: "", + expected: null + }, + { + input: "BLAH", + expected: null + }, + { + input: "0 OK", + expected: { + status: 0, + statusText: "OK" + } + }, + { + input: "1 OK", + expected: { + status: 1, + statusText: "OK" + } + }, + { + input: "99 NOT OK", + expected: { + status: 99, + statusText: "NOT OK" + } + }, + { + input: "077 77", + expected: { + status: 77, + statusText: "77" + } + }, + { + input: "099 HELLO", + expected: { + status: 99, + statusText: "HELLO" + } + }, + { + input: "200", + expected: { + status: 200, + statusText: "" + } + }, + { + input: "999 DOES IT MATTER", + expected: { + status: 999, + statusText: "DOES IT MATTER" + } + }, + { + input: "1000 BOO", + expected: null + }, + { + input: "0200 BOO", + expected: null + }, + { + input: "65736 NOT 200 OR SOME SUCH", + expected: null + }, + { + input: "131072 HI", + expected: null + }, + { + input: "-200 TEST", + expected: null + }, + { + input: "0xA", + expected: null + }, + { + input: "C8", + expected: null + } +].forEach(({ description, input, expected }) => { + promise_test(async t => { + if (expected !== null) { + const response = await fetch("resources/status-code.py?input=" + input); + assert_equals(response.status, expected.status); + assert_equals(response.statusText, expected.statusText); + assert_equals(response.headers.get("header-parsing"), "is sad"); + } else { + await promise_rejects_js(t, TypeError, fetch("resources/status-code.py?input=" + input)); + } + }, `HTTP/1.1 ${input} ${expected === null ? "(network error)" : ""}`); +}); diff --git a/testing/web-platform/tests/fetch/http-cache/304-update.any.js b/testing/web-platform/tests/fetch/http-cache/304-update.any.js new file mode 100644 index 0000000000..15484f01eb --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/304-update.any.js @@ -0,0 +1,146 @@ +// META: global=window,worker +// META: title=HTTP Cache - 304 Updates +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache updates returned headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates returned headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "ABC"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["ETag", "ABC"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "DEF"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "DEF"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "Content-* header", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "GHI"], + ["Content-Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "GHI"], + ["Content-Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/README.md b/testing/web-platform/tests/fetch/http-cache/README.md new file mode 100644 index 0000000000..512c422e10 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/README.md @@ -0,0 +1,72 @@ +## HTTP Caching Tests + +These tests cover HTTP-specified behaviours for caches, primarily from +[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html), but as seen through the +lens of Fetch. + +A few notes: + +* By its nature, [caching is entirely optional]( + https://www.rfc-editor.org/rfc/rfc9111.html#section-2-2); + some tests expecting a response to be + cached might fail because the client chose not to cache it, or chose to + race the cache with a network request. + +* Likewise, some tests might fail because there is a separate document-level + cache that's not well defined; see [this + issue](https://github.com/whatwg/fetch/issues/354). + +* [Partial content tests](partial.any.js) (a.k.a. Range requests) are not specified + in Fetch; tests are included here for interest only. + +* Some browser caches will behave differently when reloading / + shift-reloading, despite the `cache mode` staying the same. + +* [cache-tests.fyi](https://cache-tests.fyi/) is another test suite of HTTP caching + which also caters to server/CDN implementations. + +## Test Format + +Each test run gets its own URL and randomized content and operates independently. + +Each test is an an array of objects, with the following members: + +- `name` - The name of the test. +- `requests` - a list of request objects (see below). + +Possible members of a request object: + +- template - A template object for the request, by name. +- request_method - A string containing the HTTP method to be used. +- request_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the request. +- request_body - A string to use as the request body. +- mode - The mode string to pass to `fetch()`. +- credentials - The credentials string to pass to `fetch()`. +- cache - The cache string to pass to `fetch()`. +- pause_after - Boolean controlling a 3-second pause after the request completes. +- response_status - A `[number, string]` array containing the HTTP status code + and phrase to return. +- response_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the response. These values will also be checked like + expected_response_headers, unless there is a third value that is + `false`. See below for special handling considerations. +- response_body - String to send as the response body. If not set, it will contain + the test identifier. +- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]` +- expected_status - A number representing a HTTP status code to check the response for. + If not set, the value of `response_status[0]` will be used; if that + is not set, 200 will be used. +- expected_request_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the request for. +- expected_response_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the response for. See also response_headers. +- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking. + +Some headers in `response_headers` are treated specially: + +* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server. +* For URL-carrying headers, the value will be appended as a query parameter for `target`. + +See the source for exact details. + diff --git a/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test-ref.html b/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test-ref.html new file mode 100644 index 0000000000..905facdc88 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test-ref.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <meta charset="utf-8"> + <img src="/images/green.png"> + <img src="/images/green.png"> +</html> diff --git a/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test.html b/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test.html new file mode 100644 index 0000000000..a8979baf54 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/basic-auth-cache-test.html @@ -0,0 +1,27 @@ +<!doctype html> +<html id="doc" class="reftest-wait"> + <meta charset="utf-8"> + <link rel="match" href="basic-auth-cache-test-ref.html"> + + <img id="auth" onload="loadNoAuth()"> + <img id="noauth" onload="removeWait()"> + + + <script type="text/javascript"> + function loadAuth() { + var authUrl = 'http://testuser:testpass@' + window.location.host + '/fetch/http-cache/resources/securedimage.py'; + document.getElementById('auth').src = authUrl; + } + + function loadNoAuth() { + var noAuthUrl = 'http://' + window.location.host + '/fetch/http-cache/resources/securedimage.py'; + document.getElementById('noauth').src = noAuthUrl; + } + + function removeWait() { + document.getElementById('doc').className = ""; + } + + window.onload = loadAuth; + </script> +</html> diff --git a/testing/web-platform/tests/fetch/http-cache/cache-mode.any.js b/testing/web-platform/tests/fetch/http-cache/cache-mode.any.js new file mode 100644 index 0000000000..8f406d5a6a --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/cache-mode.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Fetch - Cache Mode +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache", + requests: [ + { + cache: "no-cache", + expected_request_headers: [['cache-control', 'max-age=0']] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present", + requests: [ + { + cache: "no-cache", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store", + requests: [ + { + cache: "no-store", + expected_request_headers: [ + ['cache-control', 'no-cache'], + ['pragma', 'no-cache'] + ] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present", + requests: [ + { + cache: "no-store", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present", + requests: [ + { + cache: "no-store", + request_headers: [['pragma', 'foo']], + expected_request_headers: [['pragma', 'foo']] + } + ] + } +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/cc-request.any.js b/testing/web-platform/tests/fetch/http-cache/cc-request.any.js new file mode 100644 index 0000000000..d556566841 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/cc-request.any.js @@ -0,0 +1,202 @@ +// META: global=window,worker +// META: title=HTTP Cache - Cache-Control Request Directives +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=0"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=1"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "1800"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-age=600"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1"] + ], + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "2000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=2000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "1000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=1000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Last-Modified", -10000] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "lm_validate" + } + ] + }, + { + name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["ETag", http_content("abc")] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "etag_validate" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-store"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached", + requests: [ + { + request_headers: [ + ["Cache-Control", "only-if-cached"] + ], + expected_status: 504, + expected_response_text: null + } + ] + } +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/credentials.tentative.any.js b/testing/web-platform/tests/fetch/http-cache/credentials.tentative.any.js new file mode 100644 index 0000000000..31770925cd --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/credentials.tentative.any.js @@ -0,0 +1,62 @@ +// META: global=window,worker +// META: title=HTTP Cache - Content +// META: timeout=long +// META: script=/common/utils.js +// META: script=http-cache.js + +// This is a tentative test. +// Firefox behavior is used as expectations. +// +// whatwg/fetch issue: +// https://github.com/whatwg/fetch/issues/1253 +// +// Chrome design doc: +// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U/edit# + +const request_cacheable = { + request_headers: [], + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ], + // TODO(arthursonzogni): The behavior is tested only for same-origin requests. + // It must behave similarly for cross-site and cross-origin requests. The + // problems is the http-cache.js infrastructure returns the + // "Server-Request-Count" as HTTP response headers, which aren't readable for + // CORS requests. + base_url: location.href.replace(/\/[^\/]*$/, '/'), +}; + +const request_credentialled = { ...request_cacheable, credentials: 'include', }; +const request_anonymous = { ...request_cacheable, credentials: 'omit', }; + +const responseIndex = count => { + return { + expected_response_headers: [ + ['Server-Request-Count', count.toString()], + ], + } +}; + +var tests = [ + { + name: 'same-origin: 2xAnonymous, 2xCredentialled, 1xAnonymous', + requests: [ + { ...request_anonymous , ...responseIndex(1)} , + { ...request_anonymous , ...responseIndex(1)} , + { ...request_credentialled , ...responseIndex(2)} , + { ...request_credentialled , ...responseIndex(2)} , + { ...request_anonymous , ...responseIndex(1)} , + ] + }, + { + name: 'same-origin: 2xCredentialled, 2xAnonymous, 1xCredentialled', + requests: [ + { ...request_credentialled , ...responseIndex(1)} , + { ...request_credentialled , ...responseIndex(1)} , + { ...request_anonymous , ...responseIndex(2)} , + { ...request_anonymous , ...responseIndex(2)} , + { ...request_credentialled , ...responseIndex(1)} , + ] + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/freshness.any.js b/testing/web-platform/tests/fetch/http-cache/freshness.any.js new file mode 100644 index 0000000000..86c2620aa6 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/freshness.any.js @@ -0,0 +1,243 @@ +// META: global=window,worker +// META: title=HTTP Cache - Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + // response directives + { + name: "HTTP cache reuses a response with a future Expires", + requests: [ + { + response_headers: [ + ["Expires", (30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a past Expires", + requests: [ + { + response_headers: [ + ["Expires", (-30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a present Expires", + requests: [ + { + response_headers: [ + ["Expires", 0] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with an invalid Expires", + requests: [ + { + response_headers: [ + ["Expires", "0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with an invalid Expires with Last-Modified now", + requests: [ + { + response_headers: [ + ["Expires", "0"], + ['Last-Modified', 0] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with an invalid Expires with past Last-Modified", + requests: [ + { + response_headers: [ + ["Expires", "0"], + ['Last-Modified', -100000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", -10000] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", "0"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1, s-maxage=3600"] + ], + pause_after: true, + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "12000"] + ], + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-store"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-store"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-cache"], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-cache"], + ["Expires", 10000], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/heuristic.any.js b/testing/web-platform/tests/fetch/http-cache/heuristic.any.js new file mode 100644 index 0000000000..d846131888 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/heuristic.any.js @@ -0,0 +1,93 @@ +// META: global=window,worker +// META: title=HTTP Cache - Heuristic Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)], + ["Cache-Control", "public"] + ], + }, + { + expected_type: "cached", + response_status: [299, "Whatever"] + } + ] + }, + { + name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + }, + { + expected_type: "not_cached" + } + ] + } +]; + +function check_status(status) { + var succeed = status[0]; + var code = status[1]; + var phrase = status[2]; + var body = status[3]; + if (body === undefined) { + body = http_content(code); + } + var expected_type = "not_cached"; + var desired = "does not use" + if (succeed === true) { + expected_type = "cached"; + desired = "reuses"; + } + tests.push( + { + name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness", + requests: [ + { + response_status: [code, phrase], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + response_body: body + }, + { + expected_type: expected_type, + response_status: [code, phrase], + response_body: body + } + ] + } + ) +} +[ + [true, 200, "OK"], + [true, 203, "Non-Authoritative Information"], + [true, 204, "No Content", ""], + [true, 404, "Not Found"], + [true, 405, "Method Not Allowed"], + [true, 410, "Gone"], + [true, 414, "URI Too Long"], + [true, 501, "Not Implemented"] +].forEach(check_status); +[ + [false, 201, "Created"], + [false, 202, "Accepted"], + [false, 403, "Forbidden"], + [false, 502, "Bad Gateway"], + [false, 503, "Service Unavailable"], + [false, 504, "Gateway Timeout"], +].forEach(check_status); +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/http-cache.js b/testing/web-platform/tests/fetch/http-cache/http-cache.js new file mode 100644 index 0000000000..19f1ca9b2b --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/http-cache.js @@ -0,0 +1,274 @@ +/* global btoa fetch token promise_test step_timeout */ +/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */ + +const templates = { + 'fresh': { + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'stale': { + 'response_headers': [ + ['Expires', -5000], + ['Last-Modified', -100000] + ] + }, + 'lcl_response': { + 'response_headers': [ + ['Location', 'location_target'], + ['Content-Location', 'content_location_target'] + ] + }, + 'location': { + 'query_arg': 'location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'content_location': { + 'query_arg': 'content_location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + } +} + +const noBodyStatus = new Set([204, 304]) + +function makeTest (test) { + return function () { + var uuid = token() + var requests = expandTemplates(test) + var fetchFunctions = makeFetchFunctions(requests, uuid) + return runTest(fetchFunctions, requests, uuid) + } +} + +function makeFetchFunctions(requests, uuid) { + var fetchFunctions = [] + for (let i = 0; i < requests.length; ++i) { + fetchFunctions.push({ + code: function (idx) { + var config = requests[idx] + var url = makeTestUrl(uuid, config) + var init = fetchInit(requests, config) + return fetch(url, init) + .then(makeCheckResponse(idx, config)) + .then(makeCheckResponseBody(config, uuid), function (reason) { + if ('expected_type' in config && config.expected_type === 'error') { + assert_throws_js(TypeError, function () { throw reason }) + } else { + throw reason + } + }) + }, + pauseAfter: 'pause_after' in requests[i] + }) + } + return fetchFunctions +} + +function runTest(fetchFunctions, requests, uuid) { + var idx = 0 + function runNextStep () { + if (fetchFunctions.length) { + var nextFetchFunction = fetchFunctions.shift() + if (nextFetchFunction.pauseAfter === true) { + return nextFetchFunction.code(idx++) + .then(pause) + .then(runNextStep) + } else { + return nextFetchFunction.code(idx++) + .then(runNextStep) + } + } else { + return Promise.resolve() + } + } + + return runNextStep() + .then(function () { + return getServerState(uuid) + }).then(function (testState) { + checkRequests(requests, testState) + return Promise.resolve() + }) +} + +function expandTemplates (test) { + var rawRequests = test.requests + var requests = [] + for (let i = 0; i < rawRequests.length; i++) { + var request = rawRequests[i] + request.name = test.name + if ('template' in request) { + var template = templates[request['template']] + for (let member in template) { + if (!request.hasOwnProperty(member)) { + request[member] = template[member] + } + } + } + requests.push(request) + } + return requests +} + +function fetchInit (requests, config) { + var init = { + 'headers': [] + } + if ('request_method' in config) init.method = config['request_method'] + // Note: init.headers must be a copy of config['request_headers'] array, + // because new elements are added later. + if ('request_headers' in config) init.headers = [...config['request_headers']]; + if ('name' in config) init.headers.push(['Test-Name', config.name]) + if ('request_body' in config) init.body = config['request_body'] + if ('mode' in config) init.mode = config['mode'] + if ('credentials' in config) init.credentials = config['credentials'] + if ('cache' in config) init.cache = config['cache'] + init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))]) + return init +} + +function makeCheckResponse (idx, config) { + return function checkResponse (response) { + var reqNum = idx + 1 + var resNum = parseInt(response.headers.get('Server-Request-Count')) + if ('expected_type' in config) { + if (config.expected_type === 'error') { + assert_true(false, `Request ${reqNum} doesn't throw an error`) + return response.text() + } + if (config.expected_type === 'cached') { + assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`) + } + if (config.expected_type === 'not_cached') { + assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`) + } + } + if ('expected_status' in config) { + assert_equals(response.status, config.expected_status, + `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`) + } else if ('response_status' in config) { + assert_equals(response.status, config.response_status[0], + `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`) + } else { + assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`) + } + if ('response_headers' in config) { + config.response_headers.forEach(function (header) { + if (header.len < 3 || header[2] === true) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + } + }) + } + if ('expected_response_headers' in config) { + config.expected_response_headers.forEach(function (header) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + }) + } + return response.text() + } +} + +function makeCheckResponseBody (config, uuid) { + return function checkResponseBody (resBody) { + var statusCode = 200 + if ('response_status' in config) { + statusCode = config.response_status[0] + } + if ('expected_response_text' in config) { + if (config.expected_response_text !== null) { + assert_equals(resBody, config.expected_response_text, + `Response body is "${resBody}", not expected "${config.expected_response_text}"`) + } + } else if ('response_body' in config && config.response_body !== null) { + assert_equals(resBody, config.response_body, + `Response body is "${resBody}", not sent "${config.response_body}"`) + } else if (!noBodyStatus.has(statusCode)) { + assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`) + } + } +} + +function checkRequests (requests, testState) { + var testIdx = 0 + for (let i = 0; i < requests.length; ++i) { + var expectedValidatingHeaders = [] + var config = requests[i] + var serverRequest = testState[testIdx] + var reqNum = i + 1 + if ('expected_type' in config) { + if (config.expected_type === 'cached') continue // the server will not see the request + if (config.expected_type === 'etag_validated') { + expectedValidatingHeaders.push('if-none-match') + } + if (config.expected_type === 'lm_validated') { + expectedValidatingHeaders.push('if-modified-since') + } + } + testIdx++ + expectedValidatingHeaders.forEach(vhdr => { + assert_own_property(serverRequest.request_headers, vhdr, + `request ${reqNum} doesn't have ${vhdr} header`) + }) + if ('expected_request_headers' in config) { + config.expected_request_headers.forEach(expectedHdr => { + assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1], + `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`) + }) + } + } +} + +function pause () { + return new Promise(function (resolve, reject) { + step_timeout(function () { + return resolve() + }, 3000) + }) +} + +function makeTestUrl (uuid, config) { + var arg = '' + var base_url = '' + if ('base_url' in config) { + base_url = config.base_url + } + if ('query_arg' in config) { + arg = `&target=${config.query_arg}` + } + return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}` +} + +function getServerState (uuid) { + return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`) + .then(function (response) { + return response.text() + }).then(function (text) { + return JSON.parse(text) || [] + }) +} + +function run_tests (tests) { + tests.forEach(function (test) { + promise_test(makeTest(test), test.name) + }) +} + +var contentStore = {} +function http_content (csKey) { + if (csKey in contentStore) { + return contentStore[csKey] + } else { + var content = btoa(Math.random() * Date.now()) + contentStore[csKey] = content + return content + } +} diff --git a/testing/web-platform/tests/fetch/http-cache/invalidate.any.js b/testing/web-platform/tests/fetch/http-cache/invalidate.any.js new file mode 100644 index 0000000000..9f8090ace6 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/invalidate.any.js @@ -0,0 +1,235 @@ +// META: global=window,worker +// META: title=HTTP Cache - Invalidation +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: 'HTTP cache invalidates after a successful response from a POST', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate after a failed response from an unsafe request', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a PUT', + requests: [ + { + template: "fresh" + }, { + template: "fresh", + request_method: "PUT", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a DELETE', + requests: [ + { + template: "fresh" + }, { + request_method: "DELETE", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from an unknown method', + requests: [ + { + template: "fresh" + }, { + request_method: "FOO", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + + + { + name: 'HTTP cache invalidates Location URL after a successful response from a POST', + requests: [ + { + template: "location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a PUT', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a DELETE', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from an unknown method', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + + + + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST', + requests: [ + { + template: "content_location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "content_location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + } + +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/partial.any.js b/testing/web-platform/tests/fetch/http-cache/partial.any.js new file mode 100644 index 0000000000..3f23b5930f --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/partial.any.js @@ -0,0 +1,208 @@ +// META: global=window,worker +// META: title=HTTP Cache - Partial Content +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache stores partial content and reuses it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234", + expected_request_headers: [ + ["Range", "bytes=-5"] + ] + }, + { + request_headers: [ + ["Range", "bytes=-5"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01234" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=0-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01" + }, + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=1-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "1234567890" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "0123456789A" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "A" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it with only-if-cached", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=0-1"] + ], + mode: "same-origin", + cache: "only-if-cached", + expected_type: "cached", + expected_status: 206, + expected_response_text: "01" + }, + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=6-8"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ["Range", "bytes=6-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "4" + } + ] + }, + { + name: "HTTP cache stores partial content and completes it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 0-4/10"] + ], + response_body: "01234" + }, + { + expected_request_headers: [ + ["range", "bytes=5-"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/post-patch.any.js b/testing/web-platform/tests/fetch/http-cache/post-patch.any.js new file mode 100644 index 0000000000..0a69baa5c6 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/post-patch.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: title=HTTP Cache - Caching POST and PATCH responses +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "PATCH", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "POST", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + } +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/resources/http-cache.py b/testing/web-platform/tests/fetch/http-cache/resources/http-cache.py new file mode 100644 index 0000000000..3ab610dd14 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/resources/http-cache.py @@ -0,0 +1,124 @@ +import datetime +import json +import time +from base64 import b64decode + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +NOTEHDRS = set([u'content-type', u'access-control-allow-origin', u'last-modified', u'etag']) +NOBODYSTATUS = set([204, 304]) +LOCATIONHDRS = set([u'location', u'content-location']) +DATEHDRS = set([u'date', u'expires', u'last-modified']) + +def main(request, response): + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + + if request.method == u"OPTIONS": + return handle_preflight(uuid, request, response) + if not uuid: + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"UUID not found" + if dispatch == b'test': + return handle_test(uuid, request, response) + elif dispatch == b'state': + return handle_state(uuid, request, response) + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"Fallthrough" + +def handle_preflight(uuid, request, response): + response.status = (200, b"OK") + response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*') + response.headers.set(b"Access-Control-Allow-Methods", b"GET") + response.headers.set(b"Access-Control-Allow-Headers", request.headers.get(b"Access-Control-Request-Headers") or "*") + response.headers.set(b"Access-Control-Max-Age", b"86400") + return b"Preflight request" + +def handle_state(uuid, request, response): + response.headers.set(b"Content-Type", b"text/plain") + return json.dumps(request.server.stash.take(uuid)) + +def handle_test(uuid, request, response): + server_state = request.server.stash.take(uuid) or [] + try: + requests = json.loads(b64decode(request.headers.get(b'Test-Requests', b""))) + except: + response.status = (400, b"Bad Request") + response.headers.set(b"Content-Type", b"text/plain") + return b"No or bad Test-Requests request header" + config = requests[len(server_state)] + if not config: + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"Config not found" + noted_headers = {} + now = time.time() + for header in config.get(u'response_headers', []): + if header[0].lower() in LOCATIONHDRS: # magic locations + if (len(header[1]) > 0): + header[1] = u"%s&target=%s" % (request.url, header[1]) + else: + header[1] = request.url + if header[0].lower() in DATEHDRS and isinstance(header[1], int): # magic dates + header[1] = http_date(now, header[1]) + response.headers.set(isomorphic_encode(header[0]), isomorphic_encode(header[1])) + if header[0].lower() in NOTEHDRS: + noted_headers[header[0].lower()] = header[1] + state = { + u'now': now, + u'request_method': request.method, + u'request_headers': dict([[isomorphic_decode(h.lower()), isomorphic_decode(request.headers[h])] for h in request.headers]), + u'response_headers': noted_headers + } + server_state.append(state) + request.server.stash.put(uuid, server_state) + + if u"access-control-allow-origin" not in noted_headers: + response.headers.set(b"Access-Control-Allow-Origin", b"*") + if u"content-type" not in noted_headers: + response.headers.set(b"Content-Type", b"text/plain") + response.headers.set(b"Server-Request-Count", len(server_state)) + + code, phrase = config.get(u"response_status", [200, b"OK"]) + if config.get(u"expected_type", u"").endswith(u'validated'): + ref_hdrs = server_state[0][u'response_headers'] + previous_lm = ref_hdrs.get(u'last-modified', False) + if previous_lm and request.headers.get(b"If-Modified-Since", False) == isomorphic_encode(previous_lm): + code, phrase = [304, b"Not Modified"] + previous_etag = ref_hdrs.get(u'etag', False) + if previous_etag and request.headers.get(b"If-None-Match", False) == isomorphic_encode(previous_etag): + code, phrase = [304, b"Not Modified"] + if code != 304: + code, phrase = [999, b'304 Not Generated'] + response.status = (code, phrase) + + content = config.get(u"response_body", uuid) + if code in NOBODYSTATUS: + return b"" + return content + + +def get_header(headers, header_name): + result = None + for header in headers: + if header[0].lower() == header_name.lower(): + result = header[1] + return result + +WEEKDAYS = [u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun'] +MONTHS = [None, u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', + u'Aug', u'Sep', u'Oct', u'Nov', u'Dec'] + +def http_date(now, delta_secs=0): + date = datetime.datetime.utcfromtimestamp(now + delta_secs) + return u"%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT" % ( + WEEKDAYS[date.weekday()], + date.day, + MONTHS[date.month], + date.year, + date.hour, + date.minute, + date.second) diff --git a/testing/web-platform/tests/fetch/http-cache/resources/securedimage.py b/testing/web-platform/tests/fetch/http-cache/resources/securedimage.py new file mode 100644 index 0000000000..cac9cfedd2 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/resources/securedimage.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 - + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + image_url = str.replace(request.url, u"fetch/http-cache/resources/securedimage.py", u"images/green.png") + + if b"authorization" not in request.headers: + response.status = 401 + response.headers.set(b"WWW-Authenticate", b"Basic") + return + else: + auth = request.headers.get(b"Authorization") + if auth != b"Basic dGVzdHVzZXI6dGVzdHBhc3M=": + response.set_error(403, u"Invalid username or password - " + isomorphic_decode(auth)) + return + + response.status = 301 + response.headers.set(b"Location", isomorphic_encode(image_url)) diff --git a/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html new file mode 100644 index 0000000000..48b16180cf --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html @@ -0,0 +1,34 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>HTTP Cache - helper</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions"> + <meta name="timeout" content="normal"> + <script src="/resources/testharness.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<script> + const host = get_host_info(); + + // Create iframe that is same-origin to the opener. + var iframe = document.createElement("iframe"); + iframe.src = host.HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') + "split-cache-popup.html"; + document.body.appendChild(iframe); + + window.addEventListener("message", function listener(event) { + if (event.origin !== host.HTTP_ORIGIN) { + // Ignore messages not from the iframe or opener + return; + } else if (typeof(event.data) === "object") { + // This message came from the opener, pass it on to the iframe + iframe.contentWindow.postMessage(event.data, host.HTTP_ORIGIN); + } else if (typeof(event.data) === "string") { + // This message came from the iframe, pass it on to the opener + window.opener.postMessage(event.data, host.HTTP_ORIGIN); + } + }) +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup.html b/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup.html new file mode 100644 index 0000000000..edb5794794 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/resources/split-cache-popup.html @@ -0,0 +1,28 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>HTTP Cache - helper</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions"> + <meta name="timeout" content="normal"> + <script src="/resources/testharness.js"></script> + <script src="../http-cache.js"></script> +</head> +<body> +<script> + window.addEventListener("message", function listener(event) { + window.removeEventListener("message", listener) + + var fetchFunction = makeFetchFunctions(event.data.requests, event.data.uuid)[event.data.index] + fetchFunction.code(event.data.index).then( + function(response) { + event.source.postMessage("success", event.origin) + }, + function(response) { + event.source.postMessage("error", event.origin) + } + ) + }) +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/http-cache/split-cache.html b/testing/web-platform/tests/fetch/http-cache/split-cache.html new file mode 100644 index 0000000000..fe93d2e340 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/split-cache.html @@ -0,0 +1,158 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>HTTP Cache - Partioning by site</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#http-cache-partitions"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/utils.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="http-cache.js"></script> +</head> +<body> +<script> +const host = get_host_info(); + +// We run this entire test four times, varying the following two booleans: +// - is_cross_site_test, which controls whether the popup is cross-site. +// - load_resource_in_iframe, which controls whether the popup loads the +// resource in an iframe or the top-level frame. Note that the iframe is +// always same-site to the opener. +function performFullTest(is_cross_site_test, load_resource_in_iframe, name) { + const POPUP_HTTP_ORIGIN = is_cross_site_test ? host.HTTP_NOTSAMESITE_ORIGIN : host.HTTP_ORIGIN + const LOCAL_HTTP_ORIGIN = host.HTTP_ORIGIN + + const popupBaseURL = POPUP_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; + const localBaseURL = LOCAL_HTTP_ORIGIN + window.location.pathname.replace(/\/[^\/]*$/, '/') ; + + // Note: Navigation requests are requested with credentials. Make sure the + // fetch requests are also requested with credentials. This ensures passing + // this test is not simply the consequence of discriminating anonymous and + // credentialled request in the HTTP cache. + // + // See https://github.com/whatwg/fetch/issues/1253 + var test = { + requests: [ + { + response_headers: [ + ["Expires", (30 * 24 * 60 * 60)], + ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN], + ], + base_url: localBaseURL, + credentials: "include", + }, + { + response_headers: [ + ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN], + ], + base_url: localBaseURL, + credentials: "include", + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + response_headers: [ + ["Access-Control-Allow-Origin", POPUP_HTTP_ORIGIN], + ], + // If the popup's request was a cache hit, we would only expect 2 + // requests to the server. If it was a cache miss, we would expect 3. + // load_resource_in_iframe does not affect the expectation as, even + // though the iframe (if present) is same-site, we expect a cache miss + // when the popup's top-level frame is a different site. + expected_response_headers: [ + ["server-request-count", is_cross_site_test ? "3" : "2"] + ], + base_url: localBaseURL, + credentials: "include", + } + ] + } + + var uuid = token() + var local_requests = expandTemplates(test) + var fetchFns = makeFetchFunctions(local_requests, uuid) + + var popup_requests = expandTemplates(test) + + // Request the resource with a long cache expiry + function local_fetch() { + return fetchFns[0].code(0) + } + + function popup_fetch() { + return new Promise(function(resolve, reject) { + var relativeUrl = load_resource_in_iframe + ? "resources/split-cache-popup-with-iframe.html" + : "resources/split-cache-popup.html"; + var win = window.open(popupBaseURL + relativeUrl); + + // Post a message to initiate the popup's request and give the necessary + // information. Posted repeatedly to account for dropped messages as the + // popup is loading. + function postMessage(event) { + var payload = { + index: 1, + requests: popup_requests, + uuid: uuid + } + win.postMessage(payload, POPUP_HTTP_ORIGIN) + } + var messagePoster = setInterval(postMessage, 100) + + // Listen for the result + function messageListener(event) { + if (event.origin !== POPUP_HTTP_ORIGIN) { + reject("Unknown error") + } else if (event.data === "success") { + resolve() + } else if (event.data === "error") { + reject("Error in popup") + } else { + return; // Ignore testharness.js internal messages + } + window.removeEventListener("message", messageListener) + clearInterval(messagePoster) + win.close() + } + window.addEventListener("message", messageListener) + }) + } + + function local_fetch2() { + return fetchFns[2].code(2) + } + + // Final checks. + function check_server_info() { + return getServerState(uuid) + .then(function (testState) { + checkRequests(local_requests, testState) + return Promise.resolve() + }) + } + + promise_test(() => { + return local_fetch() + .then(popup_fetch) + .then(local_fetch2) + .then(check_server_info) + }, name) +} + +performFullTest( + false /* is_cross_site_test */, false /* load_resource_in_iframe */, + "HTTP cache is shared between same-site top-level frames"); +performFullTest( + true /* is_cross_site_test */, false /* load_resource_in_iframe */, + "HTTP cache is not shared between cross-site top-level frames"); +performFullTest( + false /* is_cross_site_test */, true /* load_resource_in_iframe */, + "HTTP cache is shared between same-site frames with same-site top-level frames"); +performFullTest( + true /* is_cross_site_test */, true /* load_resource_in_iframe */, + "HTTP cache is not shared between same-site frames with cross-site top-level frames"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/http-cache/status.any.js b/testing/web-platform/tests/fetch/http-cache/status.any.js new file mode 100644 index 0000000000..10c83a25a2 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/status.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker +// META: title=HTTP Cache - Status Codes +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = []; +function check_status(status) { + var code = status[0]; + var phrase = status[1]; + var body = status[2]; + if (body === undefined) { + body = http_content(code); + } + tests.push({ + name: "HTTP cache goes to the network if it has a stale " + code + " response", + requests: [ + { + template: "stale", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "not_cached", + response_status: [code, phrase], + response_body: body + } + ] + }) + tests.push({ + name: "HTTP cache avoids going to the network if it has a fresh " + code + " response", + requests: [ + { + template: "fresh", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "cached", + response_status: [code, phrase], + response_body: body + } + ] + }) +} +[ + [200, "OK"], + [203, "Non-Authoritative Information"], + [204, "No Content", null], + [299, "Whatever"], + [400, "Bad Request"], + [404, "Not Found"], + [410, "Gone"], + [499, "Whatever"], + [500, "Internal Server Error"], + [502, "Bad Gateway"], + [503, "Service Unavailable"], + [504, "Gateway Timeout"], + [599, "Whatever"] +].forEach(check_status); +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/http-cache/vary.any.js b/testing/web-platform/tests/fetch/http-cache/vary.any.js new file mode 100644 index 0000000000..2cfd226af8 --- /dev/null +++ b/testing/web-platform/tests/fetch/http-cache/vary.any.js @@ -0,0 +1,313 @@ +// META: global=window,worker +// META: title=HTTP Cache - Vary +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "1"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "2"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't invalidate existing Vary response", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + response_body: http_content('foo_1') + }, + { + request_headers: [ + ["Foo", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + expected_type: "not_cached", + response_body: http_content('foo_2'), + }, + { + request_headers: [ + ["Foo", "1"] + ], + response_body: http_content('foo_1'), + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't pay attention to headers not listed in Vary", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Other", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + }, + { + request_headers: [ + ["Foo", "1"], + ["Other", "3"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses two-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses three-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc4"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response with a field value of '*'", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "*"] + ] + }, + { + request_headers: [ + ["*", "1"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + } +]; +run_tests(tests); diff --git a/testing/web-platform/tests/fetch/images/canvas-remote-read-remote-image-redirect.html b/testing/web-platform/tests/fetch/images/canvas-remote-read-remote-image-redirect.html new file mode 100644 index 0000000000..4a887f3d33 --- /dev/null +++ b/testing/web-platform/tests/fetch/images/canvas-remote-read-remote-image-redirect.html @@ -0,0 +1,28 @@ +<!doctype html> +<meta charset=utf-8> +<title>Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +setup({ single_test: true }); +var image = new Image(); +image.onload = function() { + const canvas = document.createElement("canvas"); + canvas.width = 100; + canvas.height = 100; + + const context = canvas.getContext("2d"); + context.drawImage(image, 0, 0, 100, 100); + + assert_throws_dom("SecurityError", () => { + context.getImageData(0, 0, 100, 100); + }); + done(); +} + +const info = get_host_info(); +const finalURL = get_host_info().HTTP_ORIGIN + "/images/apng.png"; +const intermediateURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?location=" + finalURL; +image.src = "/fetch/api/resources/redirect.py?location=" + encodeURIComponent(intermediateURL); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/META.yml b/testing/web-platform/tests/fetch/metadata/META.yml new file mode 100644 index 0000000000..85f0a7d2ee --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/META.yml @@ -0,0 +1,4 @@ +spec: https://w3c.github.io/webappsec-fetch-metadata/ +suggested_reviewers: + - mikewest + - iVanlIsh diff --git a/testing/web-platform/tests/fetch/metadata/README.md b/testing/web-platform/tests/fetch/metadata/README.md new file mode 100644 index 0000000000..34864d4a4b --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/README.md @@ -0,0 +1,9 @@ +Fetch Metadata Tests +==================== + +This directory contains tests related to the Fetch Metadata proposal: + +: Explainer +:: <https://github.com/w3c/webappsec-fetch-metadata> +: "Spec" +:: <https://w3c.github.io/webappsec-fetch-metadata/> diff --git a/testing/web-platform/tests/fetch/metadata/audio-worklet.https.html b/testing/web-platform/tests/fetch/metadata/audio-worklet.https.html new file mode 100644 index 0000000000..3b768ef0b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/audio-worklet.https.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> + +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<script> + + promise_test(async t => { + const nonce = token(); + const key = "worklet-destination" + nonce; + const context = new AudioContext(); + + await context.audioWorklet.addModule("/fetch/metadata/resources/record-header.py?file=" + key); + const expected = {"site": "same-origin", "user": "", "mode": "cors", "dest": "audioworklet"}; + await validate_expectations(key, expected); + }, "The fetch metadata for audio worklet"); + +</script> +<body></body> diff --git a/testing/web-platform/tests/fetch/metadata/embed.https.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/embed.https.sub.tentative.html new file mode 100644 index 0000000000..1900dbdf08 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/embed.https.sub.tentative.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body> + +<p>Relevant issue: <a href="https://github.com/whatwg/html/issues/513"> +<embed> should support loading random HTML documents, like <object> +</a></p> + +<script> + const nonce = token(); + + const origins = { + "same-origin": "https://{{host}}:{{ports[https][0]}}", + "same-site": "https://{{hosts[][www]}}:{{ports[https][0]}}", + "cross-site": "https://{{hosts[alt][www]}}:{{ports[https][0]}}", + }; + + for (let site in origins) { + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "embed-" + site + "-" + nonce; + + let el = document.createElement('embed'); + el.src = origins[site] + "/fetch/metadata/resources/record-header.py?file=" + key; + el.onload = _ => { + let expected = {"dest": "embed", "site": site, "user": "", "mode": "navigate"}; + validate_expectations(key, expected, site + " embed") + .then(resolve) + .catch(reject); + }; + + document.body.appendChild(el); + }) + }, "Wrapper: " + site + " embed"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "post-embed-" + site + "-" + nonce; + + let el = document.createElement('embed'); + el.src = "/common/blank.html"; + el.addEventListener("load", _ => { + el.addEventListener("load", _ => { + let expected = {"dest": "embed", "site": site, "user":"", "mode":"navigate"}; + validate_expectations(key, expected, "Navigate to " + site + " embed") + .then(resolve) + .catch(reject); + }, { once: true }); + + // Navigate the existing `<embed>` + window.frames[window.length - 1].location = origins[site] + "/fetch/metadata/resources/record-header.py?file=" + key; + }, { once: true }); + + document.body.appendChild(el); + }) + }, "Wrapper: Navigate to " + site + " embed"); + } +</script> diff --git a/testing/web-platform/tests/fetch/metadata/fetch-preflight.https.sub.any.js b/testing/web-platform/tests/fetch/metadata/fetch-preflight.https.sub.any.js new file mode 100644 index 0000000000..d52474353b --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/fetch-preflight.https.sub.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch with preflight"); +}, "Same-site fetch with preflight"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch with preflight"); +}, "Cross-site fetch with preflight"); diff --git a/testing/web-platform/tests/fetch/metadata/fetch.https.sub.any.js b/testing/web-platform/tests/fetch/metadata/fetch.https.sub.any.js new file mode 100644 index 0000000000..aeec5cdf2d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/fetch.https.sub.any.js @@ -0,0 +1,58 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-origin fetch"); +}, "Same-origin fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch"); +}, "Same-site fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch"); +}, "Cross-site fetch"); + +// Mode +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "same-origin"}, { + "site": "same-origin", + "user": "", + "mode": "same-origin", + "dest": "empty" + }, "Same-origin mode"); +}, "Same-origin mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "cors"}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "CORS mode"); +}, "CORS mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "no-cors"}, { + "site": "same-origin", + "user": "", + "mode": "no-cors", + "dest": "empty" + }, "no-CORS mode"); +}, "no-CORS mode"); diff --git a/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html new file mode 100644 index 0000000000..cf322fd34b --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/appcache-manifest.https.sub.html @@ -0,0 +1,341 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/appcache-manifest.sub.https.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for Appcache manifest</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url) { + const iframe = document.createElement('iframe'); + iframe.src = + '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url); + + return new Promise((resolve) => { + addEventListener('message', function onMessage(event) { + if (event.source !== iframe.contentWindow) { + return; + } + removeEventListener('message', onMessage); + resolve(event.data); + }); + + document.body.appendChild(iframe); + }) + .then((message) => { + if (message !== 'okay') { + throw message; + } + }) + .then(() => iframe.remove()); + } + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/audioworklet.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/audioworklet.https.sub.html new file mode 100644 index 0000000000..64fb7607e2 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/audioworklet.https.sub.html @@ -0,0 +1,271 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/audioworklet.https.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for AudioWorklet module</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + return test_driver.bless( + 'Enable WebAudio playback', + () => { + const audioContext = new AudioContext(); + + test.add_cleanup(() => audioContext.close()); + + return audioContext.audioWorklet.addModule(url); + } + ); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['audioworklet']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html new file mode 100644 index 0000000000..332effeb1f --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html @@ -0,0 +1,230 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/css-font-face.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for CSS font-face</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + let count = 0; + + function induceRequest(t, url) { + const id = `el-${count += 1}`; + const style = document.createElement('style'); + style.appendChild(document.createTextNode(` + @font-face { + font-family: wpt-font-family${id}; + src: url(${url}); + } + #el-${id} { + font-family: wpt-font-family${id}; + } + `)); + const div = document.createElement('div'); + div.setAttribute('id', 'el-' + id); + div.appendChild(style); + div.appendChild(document.createTextNode('x')); + document.body.appendChild(div); + + t.add_cleanup(() => div.remove()); + + return document.fonts.ready; + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['font']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/css-font-face.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/generated/css-font-face.sub.tentative.html new file mode 100644 index 0000000000..8a0b90cee1 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/css-font-face.sub.tentative.html @@ -0,0 +1,196 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/css-font-face.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for CSS font-face</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + let count = 0; + + function induceRequest(t, url) { + const id = `el-${count += 1}`; + const style = document.createElement('style'); + style.appendChild(document.createTextNode(` + @font-face { + font-family: wpt-font-family${id}; + src: url(${url}); + } + #el-${id} { + font-family: wpt-font-family${id}; + } + `)); + const div = document.createElement('div'); + div.setAttribute('id', 'el-' + id); + div.appendChild(style); + div.appendChild(document.createTextNode('x')); + document.body.appendChild(div); + + t.add_cleanup(() => div.remove()); + + return document.fonts.ready; + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'])) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/css-images.https.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/generated/css-images.https.sub.tentative.html new file mode 100644 index 0000000000..3fa2401928 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/css-images.https.sub.tentative.html @@ -0,0 +1,1384 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/css-images.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for CSS image-accepting properties</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The subtests in this file use an iframe to induce requests for CSS + * resources because an iframe's `onload` event is the most direct and + * generic mechanism to detect loading of CSS resources. As an optimization, + * the subtests share the same iframe and document. + */ + const declarations = []; + const iframe = document.createElement('iframe'); + const whenIframeReady = new Promise((resolve, reject) => { + iframe.onload = resolve; + iframe.onerror = reject; + }); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-origin'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Cross-site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Cross-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Cross-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Cross-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-origin'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Origin -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Origin -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Origin -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Site -> Same Origin'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'same-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Site -> Same-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Same-Site -> Cross-Site'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_equals(headers['sec-fetch-mode'], 'no-cors'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-mode'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_equals(headers['sec-fetch-dest'], 'image'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-dest'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-user'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-user'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-user'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-user'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, []); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-user'); + + iframe.srcdoc = declarations.map((declaration, index) => ` + <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>` + ).join(''); + document.body.appendChild(iframe); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/css-images.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/generated/css-images.sub.tentative.html new file mode 100644 index 0000000000..f1ef27cf08 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/css-images.sub.tentative.html @@ -0,0 +1,1099 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/css-images.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for CSS image-accepting properties</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The subtests in this file use an iframe to induce requests for CSS + * resources because an iframe's `onload` event is the most direct and + * generic mechanism to detect loading of CSS resources. As an optimization, + * the subtests share the same iframe and document. + */ + const declarations = []; + const iframe = document.createElement('iframe'); + const whenIframeReady = new Promise((resolve, reject) => { + iframe.onload = resolve; + iframe.onerror = reject; + }); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - HTTPS downgrade (header not sent)'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - HTTPS upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_equals(headers['sec-fetch-site'], 'cross-site'); + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor sec-fetch-site - HTTPS downgrade-upgrade'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image sec-fetch-site - HTTPS downgrade-upgrade'); + + iframe.srcdoc = declarations.map((declaration, index) => ` + <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>` + ).join(''); + document.body.appendChild(iframe); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-a.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-a.https.sub.html new file mode 100644 index 0000000000..dffd36c73e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-a.https.sub.html @@ -0,0 +1,482 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-a.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "a" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const anchor = win.document.createElement('a'); + anchor.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + anchor.setAttribute(name, value); + } + + win.document.body.appendChild(anchor); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => anchor.click()); + } else { + anchor.click(); + } + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {"download": ""} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - attributes: download'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {"download": ""} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest - attributes: download'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: true, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - no attributes with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-a.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-a.sub.html new file mode 100644 index 0000000000..0661de3c87 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-a.sub.html @@ -0,0 +1,342 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-a.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "a" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const anchor = win.document.createElement('a'); + anchor.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + anchor.setAttribute(name, value); + } + + win.document.body.appendChild(anchor); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => anchor.click()); + } else { + anchor.click(); + } + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-area.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-area.https.sub.html new file mode 100644 index 0000000000..be3f5f9b62 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-area.https.sub.html @@ -0,0 +1,482 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-area.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "area" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const area = win.document.createElement('area'); + area.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + area.setAttribute(name, value); + } + + win.document.body.appendChild(area); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => area.click()); + } else { + area.click(); + } + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {"download": ""} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - attributes: download'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {"download": ""} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest - attributes: download'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + { + test: t, + userActivated: true, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - no attributes with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-area.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-area.sub.html new file mode 100644 index 0000000000..5f5c338324 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-area.sub.html @@ -0,0 +1,342 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-area.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "area" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const area = win.document.createElement('area'); + area.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + area.setAttribute(name, value); + } + + win.document.body.appendChild(area); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => area.click()); + } else { + area.click(); + } + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + { + test: t, + userActivated: false, + attributes: {} + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-audio.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-audio.https.sub.html new file mode 100644 index 0000000000..a9d951233e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-audio.https.sub.html @@ -0,0 +1,325 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-audio.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "audio" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const audio = document.createElement('audio'); + + for (const [ name, value ] of Object.entries(attributes)) { + audio.setAttribute(name, value); + } + + return new Promise((resolve) => { + audio.setAttribute('src', url); + audio.onload = audio.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['audio']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-audio.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-audio.sub.html new file mode 100644 index 0000000000..2b62632ac2 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-audio.sub.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-audio.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "audio" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const audio = document.createElement('audio'); + + for (const [ name, value ] of Object.entries(attributes)) { + audio.setAttribute(name, value); + } + + return new Promise((resolve) => { + audio.setAttribute('src', url); + audio.onload = audio.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-embed.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-embed.https.sub.html new file mode 100644 index 0000000000..819bed888e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-embed.https.sub.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-embed.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "embed" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + const embed = document.createElement('embed'); + embed.setAttribute('src', url); + document.body.appendChild(embed); + + t.add_cleanup(() => embed.remove()); + + return new Promise((resolve) => embed.addEventListener('load', resolve)); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['embed']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-embed.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-embed.sub.html new file mode 100644 index 0000000000..b6e14a55e4 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-embed.sub.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-embed.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "embed" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + const embed = document.createElement('embed'); + embed.setAttribute('src', url); + document.body.appendChild(embed); + + t.add_cleanup(() => embed.remove()); + + return new Promise((resolve) => embed.addEventListener('load', resolve)); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-frame.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-frame.https.sub.html new file mode 100644 index 0000000000..17504ff563 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-frame.https.sub.html @@ -0,0 +1,309 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-frame.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const frame = document.createElement('frame'); + + const setSrc = () => frame.setAttribute('src', url); + + document.body.appendChild(frame); + test.add_cleanup(() => frame.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + frame.onload = frame.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['frame']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + true + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-frame.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-frame.sub.html new file mode 100644 index 0000000000..2d9a7ec97d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-frame.sub.html @@ -0,0 +1,250 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-frame.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const frame = document.createElement('frame'); + + const setSrc = () => frame.setAttribute('src', url); + + document.body.appendChild(frame); + test.add_cleanup(() => frame.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + frame.onload = frame.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-iframe.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-iframe.https.sub.html new file mode 100644 index 0000000000..fba1c8b9e0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-iframe.https.sub.html @@ -0,0 +1,309 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-iframe.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const iframe = document.createElement('iframe'); + + const setSrc = () => iframe.setAttribute('src', url); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + iframe.onload = iframe.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['iframe']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + t, + true + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-iframe.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-iframe.sub.html new file mode 100644 index 0000000000..6f71cc0d25 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-iframe.sub.html @@ -0,0 +1,250 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-iframe.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const iframe = document.createElement('iframe'); + + const setSrc = () => iframe.setAttribute('src', url); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + iframe.onload = iframe.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + t, + false + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html new file mode 100644 index 0000000000..a19aa117c4 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html @@ -0,0 +1,357 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-img-environment-change.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on image request triggered by change to environment</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + // The response to the request under test must describe a valid image + // resource in order for the `load` event to be fired. + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const iframe = document.createElement('iframe'); + iframe.style.width = '50px'; + document.body.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + iframe.contentDocument.open(); + iframe.contentDocument.close(); + + const image = iframe.contentDocument.createElement('img'); + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + iframe.contentDocument.body.appendChild(image); + + image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`); + image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px'); + + return new Promise((resolve) => { + image.onload = image.onerror = resolve; + }) + .then(() => { + + iframe.style.width = '200px'; + + return new Promise((resolve) => image.onload = resolve); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=anonymous'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=use-credentials'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.sub.html new file mode 100644 index 0000000000..96658726ba --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-img-environment-change.sub.html @@ -0,0 +1,270 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-img-environment-change.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on image request triggered by change to environment</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + // The response to the request under test must describe a valid image + // resource in order for the `load` event to be fired. + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const iframe = document.createElement('iframe'); + iframe.style.width = '50px'; + document.body.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + iframe.contentDocument.open(); + iframe.contentDocument.close(); + + const image = iframe.contentDocument.createElement('img'); + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + iframe.contentDocument.body.appendChild(image); + + image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`); + image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px'); + + return new Promise((resolve) => { + image.onload = image.onerror = resolve; + }) + .then(() => { + + iframe.style.width = '200px'; + + return new Promise((resolve) => image.onload = resolve); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-img.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-img.https.sub.html new file mode 100644 index 0000000000..51d6e082b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-img.https.sub.html @@ -0,0 +1,645 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-img.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "img" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceAttr, attributes) { + const image = document.createElement('img'); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + return new Promise((resolve) => { + image.setAttribute(sourceAttr, url); + image.onload = image.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - src - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - srcset - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - src - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - srcset - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - src - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - srcset - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - src - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - srcset - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - src - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - srcset - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - src - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - srcset - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - src - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - srcset - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - src - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - src - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - src - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - src - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - srcset - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - srcset - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - srcset - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - srcset - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - src - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - srcset - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - src - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - srcset - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-img.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-img.sub.html new file mode 100644 index 0000000000..5a4b152c55 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-img.sub.html @@ -0,0 +1,456 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-img.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "img" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceAttr, attributes) { + const image = document.createElement('img'); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + return new Promise((resolve) => { + image.setAttribute(sourceAttr, url); + image.onload = image.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - src - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - srcset - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - src - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - srcset - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - src - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - srcset - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - src - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - src - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - src - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - srcset - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - src - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - src - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - src - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - srcset - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - src - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - srcset - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - src - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - srcset - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - src - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - srcset - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - src - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - srcset - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - src - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - srcset - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-input-image.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-input-image.https.sub.html new file mode 100644 index 0000000000..7fa674043e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-input-image.https.sub.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-input-image.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "input" element with type="button"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const input = document.createElement('input'); + input.setAttribute('type', 'image'); + + document.body.appendChild(input); + test.add_cleanup(() => input.remove()); + + return new Promise((resolve) => { + input.onload = input.onerror = resolve; + input.setAttribute('src', url); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, []), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, []), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, []), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-input-image.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-input-image.sub.html new file mode 100644 index 0000000000..fb2a146b19 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-input-image.sub.html @@ -0,0 +1,184 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-input-image.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "input" element with type="button"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const input = document.createElement('input'); + input.setAttribute('type', 'image'); + + document.body.appendChild(input); + test.add_cleanup(() => input.remove()); + + return new Promise((resolve) => { + input.onload = input.onerror = resolve; + input.setAttribute('src', url); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpSameSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpCrossSite']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), t) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.https.sub.html new file mode 100644 index 0000000000..b244960755 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.https.sub.html @@ -0,0 +1,371 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-link-icon.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "link" element with rel="icon"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'icon'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": ""} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "anonymous"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=anonymous'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "use-credentials"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=use-credentials'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.sub.html new file mode 100644 index 0000000000..e9226c190a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-link-icon.sub.html @@ -0,0 +1,279 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-link-icon.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "link" element with rel="icon"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'icon'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html new file mode 100644 index 0000000000..bdd684a267 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html @@ -0,0 +1,559 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'prefetch'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + setup(() => { + assert_implements_optional(document.createElement('link').relList.supports('prefetch')); + }); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"crossorigin": ""} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"crossorigin": "anonymous"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=anonymous'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"crossorigin": "use-credentials"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=use-credentials'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "audio"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['audio']); + }); + }, 'sec-fetch-dest attributes: as=audio'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "document"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest attributes: as=document'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "embed"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['embed']); + }); + }, 'sec-fetch-dest attributes: as=embed'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "fetch"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['fetch']); + }); + }, 'sec-fetch-dest attributes: as=fetch'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "font"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['font']); + }); + }, 'sec-fetch-dest attributes: as=font'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "image"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest attributes: as=image'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "object"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['object']); + }); + }, 'sec-fetch-dest attributes: as=object'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "script"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['script']); + }); + }, 'sec-fetch-dest attributes: as=script'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "style"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['style']); + }); + }, 'sec-fetch-dest attributes: as=style'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "track"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['track']); + }); + }, 'sec-fetch-dest attributes: as=track'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "video"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['video']); + }); + }, 'sec-fetch-dest attributes: as=video'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {"as": "worker"} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['worker']); + }); + }, 'sec-fetch-dest attributes: as=worker'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, []), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user no attributes'); + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html new file mode 100644 index 0000000000..c2244883cc --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html @@ -0,0 +1,275 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'prefetch'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + setup(() => { + assert_implements_optional(document.createElement('link').relList.supports('prefetch')); + }); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpSameSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpCrossSite']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes'); + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html new file mode 100644 index 0000000000..3a1a8eb49a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html @@ -0,0 +1,276 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.document.open(); + win.document.write( + `<meta http-equiv="Refresh" content="0; URL=${url}">` + ); + win.document.close(); + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html new file mode 100644 index 0000000000..df3e92e2c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html @@ -0,0 +1,225 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.document.open(); + win.document.write( + `<meta http-equiv="Refresh" content="0; URL=${url}">` + ); + win.document.close(); + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-picture.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-picture.https.sub.html new file mode 100644 index 0000000000..ba6636a019 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-picture.https.sub.html @@ -0,0 +1,997 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-picture.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "picture" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceEl, sourceAttr, attributes) { + const picture = document.createElement('picture'); + const els = { + img: document.createElement('img'), + source: document.createElement('source') + }; + picture.appendChild(els.source); + picture.appendChild(els.img); + document.body.appendChild(picture); + + for (const [ name, value ] of Object.entries(attributes)) { + els.img.setAttribute(name, value); + } + + return new Promise((resolve) => { + els[sourceEl].setAttribute(sourceAttr, url); + els.img.onload = els.img.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - img[src] - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - img[srcset] - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - source[srcset] - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[src] - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - img[src] - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - img[src] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - img[srcset] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - source[srcset] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[src] - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[src] - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[src] - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - img[srcset] - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - source[srcset] - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - img[src] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - img[srcset] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest - source[srcset] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[src] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[srcset] - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - source[srcset] - no attributes'); + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-picture.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-picture.sub.html new file mode 100644 index 0000000000..64f851c682 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-picture.sub.html @@ -0,0 +1,721 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-picture.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "picture" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceEl, sourceAttr, attributes) { + const picture = document.createElement('picture'); + const els = { + img: document.createElement('img'), + source: document.createElement('source') + }; + picture.appendChild(els.source); + picture.appendChild(els.img); + document.body.appendChild(picture); + + for (const [ name, value ] of Object.entries(attributes)) { + els.img.setAttribute(name, value); + } + + return new Promise((resolve) => { + els[sourceEl].setAttribute(sourceAttr, url); + els.img.onload = els.img.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[src] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[src] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[src] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[src] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - img[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - source[srcset] - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[src] - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - img[srcset] - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - source[srcset] - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'img', + 'src', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[src] - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'img', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - img[srcset] - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + 'source', + 'srcset', + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - source[srcset] - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-script.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-script.https.sub.html new file mode 100644 index 0000000000..dcdcba2792 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-script.https.sub.html @@ -0,0 +1,593 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-script.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "script" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const script = document.createElement('script'); + script.setAttribute('src', url); + + for (const [ name, value ] of Object.entries(attributes)) { + script.setAttribute(name, value); + } + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['script']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-script.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-script.sub.html new file mode 100644 index 0000000000..a2526698fb --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-script.sub.html @@ -0,0 +1,488 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-script.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "script" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const script = document.createElement('script'); + script.setAttribute('src', url); + + for (const [ name, value ] of Object.entries(attributes)) { + script.setAttribute(name, value); + } + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, attributes: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url, + {"type": "module"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, attributes: type=module'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.https.sub.html new file mode 100644 index 0000000000..5805b46bd0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.https.sub.html @@ -0,0 +1,243 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-video-poster.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element "poster"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + var video = document.createElement('video'); + video.setAttribute('poster', url); + document.body.appendChild(video); + + const poll = () => { + if (video.clientWidth === 123) { + return; + } + + return new Promise((resolve) => t.step_timeout(resolve, 0)) + .then(poll); + }; + t.add_cleanup(() => video.remove()); + + return poll(); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['image']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.sub.html new file mode 100644 index 0000000000..e6cc5ee7e0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-video-poster.sub.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-video-poster.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element "poster"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + var video = document.createElement('video'); + video.setAttribute('poster', url); + document.body.appendChild(video); + + const poll = () => { + if (video.clientWidth === 123) { + return; + } + + return new Promise((resolve) => t.step_timeout(resolve, 0)) + .then(poll); + }; + t.add_cleanup(() => video.remove()); + + return poll(); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpSameSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpCrossSite'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-video.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-video.https.sub.html new file mode 100644 index 0000000000..971360dcee --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-video.https.sub.html @@ -0,0 +1,325 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-video.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const video = document.createElement('video'); + + for (const [ name, value ] of Object.entries(attributes)) { + video.setAttribute(name, value); + } + + return new Promise((resolve) => { + video.setAttribute('src', url); + video.onload = video.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=anonymous'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - attributes: crossorigin=use-credentials'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['video']); + }); + }, 'sec-fetch-dest - no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/element-video.sub.html b/testing/web-platform/tests/fetch/metadata/generated/element-video.sub.html new file mode 100644 index 0000000000..9707413ab6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/element-video.sub.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/element-video.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const video = document.createElement('video'); + + for (const [ name, value ] of Object.entries(attributes)) { + video.setAttribute(name, value); + } + + return new Promise((resolve) => { + video.setAttribute('src', url); + video.onload = video.onerror = resolve; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no attributes'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html new file mode 100644 index 0000000000..22f930960d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html @@ -0,0 +1,683 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request using the "fetch" API and passing through a Serive Worker</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const scripts = { + fallback: '/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js', + respondWith: '/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js' + }; + + function induceRequest(t, url, init, script) { + const SCOPE = '/fetch/metadata/resources/fetch-via-serviceworker-frame.html'; + const SCRIPT = scripts[script]; + + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then((registration) => { + t.add_cleanup(() => registration.unregister()); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => with_iframe(SCOPE)) + .then((frame) => { + t.add_cleanup(() => frame.remove()); + + return frame.contentWindow.fetch(url, init); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - no init - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - no init - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - init: mode=cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - init: mode=cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "no-cors"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - init: mode=no-cors - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "no-cors"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - init: mode=no-cors - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "same-origin"}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - init: mode=same-origin - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {"mode": "same-origin"}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - init: mode=same-origin - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest - no init - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest - no init - fallback'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no init - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, []), + {}, + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no init - fallback'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/fetch.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/fetch.https.sub.html new file mode 100644 index 0000000000..dde1daede4 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/fetch.https.sub.html @@ -0,0 +1,302 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/fetch.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request using the "fetch" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, init) { + return fetch(url, init); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite']), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site, init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"mode": "cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode - init: mode=cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"mode": "no-cors"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode - init: mode=no-cors'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {"mode": "same-origin"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - init: mode=same-origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest - no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, []), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no init'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/fetch.sub.html b/testing/web-platform/tests/fetch/metadata/generated/fetch.sub.html new file mode 100644 index 0000000000..d28ea9bb90 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/fetch.sub.html @@ -0,0 +1,220 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/fetch.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request using the "fetch" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, init) { + return fetch(url, init); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent), no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade, no init'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin']), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade, no init'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/form-submission.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/form-submission.https.sub.html new file mode 100644 index 0000000000..988b07c74a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/form-submission.https.sub.html @@ -0,0 +1,522 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/form-submission.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML form navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(method, url, userActivated) { + const windowName = String(Math.random()); + const form = document.createElement('form'); + const submit = document.createElement('input'); + submit.setAttribute('type', 'submit'); + form.appendChild(submit); + const win = open('about:blank', windowName); + form.setAttribute('method', method); + form.setAttribute('action', url); + form.setAttribute('target', windowName); + document.body.appendChild(form); + + // Query parameters must be expressed as form values so that they are sent + // with the submission of forms whose method is POST. + Array.from(new URL(url, location.origin).searchParams) + .forEach(([name, value]) => { + const input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', name); + input.setAttribute('value', value); + form.appendChild(input); + }); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.click(submit); + } else { + submit.click(); + } + }) + .then(() => { + form.remove(); + win.close(); + }); + } + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = true; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - GET with user activation'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + const userActivated = true; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - POST with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/form-submission.sub.html b/testing/web-platform/tests/fetch/metadata/generated/form-submission.sub.html new file mode 100644 index 0000000000..f862062aeb --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/form-submission.sub.html @@ -0,0 +1,400 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/form-submission.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML form navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(method, url, userActivated) { + const windowName = String(Math.random()); + const form = document.createElement('form'); + const submit = document.createElement('input'); + submit.setAttribute('type', 'submit'); + form.appendChild(submit); + const win = open('about:blank', windowName); + form.setAttribute('method', method); + form.setAttribute('action', url); + form.setAttribute('target', windowName); + document.body.appendChild(form); + + // Query parameters must be expressed as form values so that they are sent + // with the submission of forms whose method is POST. + Array.from(new URL(url, location.origin).searchParams) + .forEach(([name, value]) => { + const input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', name); + input.setAttribute('value', value); + form.appendChild(input); + }); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.click(submit); + } else { + submit.click(); + } + }) + .then(() => { + form.remove(); + win.close(); + }); + } + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - POST'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('GET', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - GET'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + const userActivated = false; + return induceRequest('POST', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - POST'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.html new file mode 100644 index 0000000000..09f0113895 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.html @@ -0,0 +1,529 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/header-link.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTTP "Link" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, rel, test) { + const iframe = document.createElement('iframe'); + + iframe.setAttribute( + 'src', + '/fetch/metadata/resources/header-link.py' + + `?location=${encodeURIComponent(url)}&rel=${rel}` + ); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + iframe.onload = iframe.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site rel=icon - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site rel=stylesheet - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=icon - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site rel=icon - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode rel=icon'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode rel=stylesheet'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest rel=icon'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=icon'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=stylesheet'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.tentative.html b/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.tentative.html new file mode 100644 index 0000000000..307c37fbf7 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/header-link.https.sub.tentative.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/header-link.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTTP "Link" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, rel, test) { + const iframe = document.createElement('iframe'); + + iframe.setAttribute( + 'src', + '/fetch/metadata/resources/header-link.py' + + `?location=${encodeURIComponent(url)}&rel=${rel}` + ); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + iframe.onload = iframe.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['style']); + }); + }, 'sec-fetch-dest rel=stylesheet'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/header-link.sub.html b/testing/web-platform/tests/fetch/metadata/generated/header-link.sub.html new file mode 100644 index 0000000000..8b6cdae0ed --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/header-link.sub.html @@ -0,0 +1,460 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/header-link.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTTP "Link" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, rel, test) { + const iframe = document.createElement('iframe'); + + iframe.setAttribute( + 'src', + '/fetch/metadata/resources/header-link.py' + + `?location=${encodeURIComponent(url)}&rel=${rel}` + ); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + iframe.onload = iframe.onerror = resolve; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=icon - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=stylesheet - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=icon - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode rel=stylesheet - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=icon - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest rel=stylesheet - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=icon - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user rel=stylesheet - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=icon - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site rel=stylesheet - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'icon', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=icon - HTTPS downgrade-upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], {mime: 'text/html'}), + 'stylesheet', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site rel=stylesheet - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html new file mode 100644 index 0000000000..e63ee423cd --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html @@ -0,0 +1,273 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/header-refresh.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTTP "Refresh" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.location = `/common/refresh.py?location=${encodeURIComponent(url)}` + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/header-refresh.optional.sub.html b/testing/web-platform/tests/fetch/metadata/generated/header-refresh.optional.sub.html new file mode 100644 index 0000000000..4674ada9c6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/header-refresh.optional.sub.html @@ -0,0 +1,222 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/header-refresh.optional.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTTP "Refresh" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.location = `/common/refresh.py?location=${encodeURIComponent(url)}` + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpSameSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpCrossSite'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html new file mode 100644 index 0000000000..72d60fc30c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html @@ -0,0 +1,254 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/script-module-import-dynamic.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dynamic ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['script']); + }); + }, 'sec-fetch-dest'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html new file mode 100644 index 0000000000..088720c23e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/script-module-import-dynamic.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dynamic ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.https.sub.html new file mode 100644 index 0000000000..cea3464f80 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.https.sub.html @@ -0,0 +1,288 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/script-module-import-static.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for static ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url) { + const script = document.createElement('script'); + script.setAttribute('type', 'module'); + script.setAttribute( + 'src', + '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url) + ); + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, [], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, [], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['script']); + }); + }, 'sec-fetch-dest'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, [], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.sub.html b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.sub.html new file mode 100644 index 0000000000..0f94f71cf6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/script-module-import-static.sub.html @@ -0,0 +1,246 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/script-module-import-static.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for static ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url) { + const script = document.createElement('script'); + script.setAttribute('type', 'module'); + script.setAttribute( + 'src', + '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url) + ); + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/serviceworker.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/serviceworker.https.sub.html new file mode 100644 index 0000000000..12e37369a4 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/serviceworker.https.sub.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/serviceworker.https.sub.html +--> +<!DOCTYPE html> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for Service Workers</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(t, url, options, event, clear) { + // Register a service worker and check the request header. + return navigator.serviceWorker.register(url, options) + .then((registration) => { + t.add_cleanup(() => registration.unregister()); + if (event === 'register') { + return; + } + return clear().then(() => registration.update()); + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'register') + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no options - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin, no options - updating'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {"type": "classic"}, 'register') + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - options: type=classic - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {"type": "classic"}, 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - options: type=classic - updating'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'register') + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - no options - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - no options - updating'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'register') + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['serviceworker']); + }); + }, 'sec-fetch-dest - no options - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['serviceworker']); + }); + }, 'sec-fetch-dest - no options - updating'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'register') + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no options - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, {}, 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no options - updating'); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/svg-image.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/svg-image.https.sub.html new file mode 100644 index 0000000000..b059eb3145 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/svg-image.https.sub.html @@ -0,0 +1,367 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/svg-image.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for SVG "image" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + "xmlns:xlink", + "http://www.w3.org/1999/xlink" + ); + const image = document.createElementNS("http://www.w3.org/2000/svg", "image"); + image.setAttribute("href", url); + svg.appendChild(image); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + document.body.appendChild(svg); + t.add_cleanup(() => svg.remove()); + + return new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": ""} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "anonymous"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=anonymous'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {"crossorigin": "use-credentials"} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['cors']); + }); + }, 'sec-fetch-mode attributes: crossorigin=use-credentials'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['empty']); + }); + }, 'sec-fetch-dest no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/svg-image.sub.html b/testing/web-platform/tests/fetch/metadata/generated/svg-image.sub.html new file mode 100644 index 0000000000..a28bbb12eb --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/svg-image.sub.html @@ -0,0 +1,265 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/svg-image.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for SVG "image" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + "xmlns:xlink", + "http://www.w3.org/1999/xlink" + ); + const image = document.createElementNS("http://www.w3.org/2000/svg", "image"); + image.setAttribute("href", url); + svg.appendChild(image); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + document.body.appendChild(svg); + t.add_cleanup(() => svg.remove()); + + return new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }); + } + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpSameSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpCrossSite'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade no attributes'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], params), + {} + ) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade no attributes'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/window-history.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/window-history.https.sub.html new file mode 100644 index 0000000000..c2b3079a6d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/window-history.https.sub.html @@ -0,0 +1,237 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/window-history.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for navigation via the HTML History API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const whenDone = (win) => { + return new Promise((resolve) => { + addEventListener('message', function handle(event) { + if (event.source === win) { + resolve(); + removeEventListener('message', handle); + } + }); + }) + }; + + /** + * Prime the UA's session history such that the location of the request is + * immediately behind the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.back`. + */ + function induceBackRequest(url, test, clear) { + const win = window.open(url); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(clear) + .then(() => win.history.back()) + .then(() => whenDone(win)); + } + + /** + * Prime the UA's session history such that the location of the request is + * immediately ahead of the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.forward`. + */ + function induceForwardRequest(url, test, clear) { + const win = window.open(messageOpenerUrl); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(() => win.location = url) + .then(() => whenDone(win)) + .then(clear) + .then(() => win.history.go(-2)) + .then(() => whenDone(win)) + .then(() => win.history.forward()) + .then(() => whenDone(win)); + } + + const messageOpenerUrl = new URL( + '/fetch/metadata/resources/message-opener.html', location + ); + // For these tests to function, replacement must *not* be enabled during + // navigation. Assignment must therefore take place after the document has + // completely loaded [1]. This event is not directly observable, but it is + // scheduled as a task immediately following the global object's `load` + // event [2]. By queuing a task during the dispatch of the `load` event, + // navigation can be consistently triggered without replacement. + // + // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate + // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end + const responseParams = { + mime: 'text/html', + body: `<script> + window.addEventListener('load', () => { + set`+`Timeout(() => location.assign('${messageOpenerUrl}')); + }); + <`+`/script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - history.forward'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/window-history.sub.html b/testing/web-platform/tests/fetch/metadata/generated/window-history.sub.html new file mode 100644 index 0000000000..333d90c286 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/window-history.sub.html @@ -0,0 +1,360 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/window-history.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for navigation via the HTML History API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const whenDone = (win) => { + return new Promise((resolve) => { + addEventListener('message', function handle(event) { + if (event.source === win) { + resolve(); + removeEventListener('message', handle); + } + }); + }) + }; + + /** + * Prime the UA's session history such that the location of the request is + * immediately behind the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.back`. + */ + function induceBackRequest(url, test, clear) { + const win = window.open(url); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(clear) + .then(() => win.history.back()) + .then(() => whenDone(win)); + } + + /** + * Prime the UA's session history such that the location of the request is + * immediately ahead of the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.forward`. + */ + function induceForwardRequest(url, test, clear) { + const win = window.open(messageOpenerUrl); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(() => win.location = url) + .then(() => whenDone(win)) + .then(clear) + .then(() => win.history.go(-2)) + .then(() => whenDone(win)) + .then(() => win.history.forward()) + .then(() => whenDone(win)); + } + + const messageOpenerUrl = new URL( + '/fetch/metadata/resources/message-opener.html', location + ); + // For these tests to function, replacement must *not* be enabled during + // navigation. Assignment must therefore take place after the document has + // completely loaded [1]. This event is not directly observable, but it is + // scheduled as a task immediately following the global object's `load` + // event [2]. By queuing a task during the dispatch of the `load` event, + // navigation can be consistently triggered without replacement. + // + // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate + // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end + const responseParams = { + mime: 'text/html', + body: `<script> + window.addEventListener('load', () => { + set`+`Timeout(() => location.assign('${messageOpenerUrl}')); + }); + <`+`/script>` + }; + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - history.forward'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - history.back'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - history.forward'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/window-location.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/window-location.https.sub.html new file mode 100644 index 0000000000..4a0d2fdc06 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/window-location.https.sub.html @@ -0,0 +1,1184 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/window-location.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for navigation via the HTML Location API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, navigate, userActivated) { + const win = window.open(); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.bless('enable user activation', () => { + navigate(win, url); + }); + } else { + navigate(win, url); + } + }) + .then(() => win.close()); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['navigate']); + }); + }, 'sec-fetch-mode - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['document']); + }); + }, 'sec-fetch-dest - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, true) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - location with user activation'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, true) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - location.href with user activation'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, true) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - location.assign with user activation'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, true) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-user'); + assert_array_equals(headers['sec-fetch-user'], ['?1']); + }); + }, 'sec-fetch-user - location.replace with user activation'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/window-location.sub.html b/testing/web-platform/tests/fetch/metadata/generated/window-location.sub.html new file mode 100644 index 0000000000..bb3e6805cb --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/window-location.sub.html @@ -0,0 +1,894 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/window-location.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for navigation via the HTML Location API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, navigate, userActivated) { + const win = window.open(); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.bless('enable user activation', () => { + navigate(win, url); + }); + } else { + navigate(win, url); + } + }) + .then(() => win.close()); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpSameSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpCrossSite'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent) - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade - location.replace'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.href'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.assign'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, false) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade - location.replace'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html new file mode 100644 index 0000000000..86f1760755 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html @@ -0,0 +1,118 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + return new Promise((resolve, reject) => { + const worker = new Worker(url, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url, {"type": "module"}) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['same-origin']); + }); + }, 'sec-fetch-mode - options: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['worker']); + }); + }, 'sec-fetch-dest - no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url, {"type": "module"}) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['worker']); + }); + }, 'sec-fetch-dest - options: type=module'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url, {"type": "module"}) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - options: type=module'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html new file mode 100644 index 0000000000..69ac7682a5 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + return new Promise((resolve, reject) => { + const worker = new Worker(url, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpOrigin'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpSameSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpCrossSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpOrigin'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpSameSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpCrossSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpOrigin'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpSameSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpCrossSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpOrigin'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpSameSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no options'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + ['httpCrossSite'], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no options'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html new file mode 100644 index 0000000000..0cd9f35d58 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html @@ -0,0 +1,268 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "importScripts" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + const src = ` + importScripts('${url}'); + postMessage('done'); + `; + const workerUrl = URL.createObjectURL( + new Blob([src], { type: 'application/javascript' }) + ); + return new Promise((resolve, reject) => { + const worker = new Worker(workerUrl, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsCrossSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Cross-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-origin']); + }); + }, 'sec-fetch-site - Same-Origin -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Origin -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same Origin'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['same-site']); + }); + }, 'sec-fetch-site - Same-Site -> Same-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsSameSite', 'httpsCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - Same-Site -> Cross-Site'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-mode'); + assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); + }); + }, 'sec-fetch-mode'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-dest'); + assert_array_equals(headers['sec-fetch-dest'], ['script']); + }); + }, 'sec-fetch-dest'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html new file mode 100644 index 0000000000..0555bbaf43 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html @@ -0,0 +1,228 @@ +<!DOCTYPE html> +<!-- +This test was procedurally generated. Please do not modify it directly. +Sources: +- fetch/metadata/tools/fetch-metadata.conf.yml +- fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "importScripts" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + const src = ` + importScripts('${url}'); + postMessage('done'); + `; + const workerUrl = URL.createObjectURL( + new Blob([src], { type: 'application/javascript' }) + ); + return new Promise((resolve, reject) => { + const worker = new Worker(workerUrl, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-mode'); + }); + }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-dest'); + }); + }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpSameSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpCrossSite'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-user'); + }); + }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_not_own_property(headers, 'sec-fetch-site'); + }); + }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS upgrade'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'], { mime: 'application/javascript' } + ); + + return induceRequest(url) + .then(() => retrieve(key)) + .then((headers) => { + assert_own_property(headers, 'sec-fetch-site'); + assert_array_equals(headers['sec-fetch-site'], ['cross-site']); + }); + }, 'sec-fetch-site - HTTPS downgrade-upgrade'); + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/navigation.https.sub.html b/testing/web-platform/tests/fetch/metadata/navigation.https.sub.html new file mode 100644 index 0000000000..32c9cf77f9 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/navigation.https.sub.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script> + test(t => { + let expected = { + "mode": "navigate", + "site": "none", + "dest": "document" + }; + + let actual = { + "mode": "{{headers[sec-fetch-mode]}}", + "site": "{{headers[sec-fetch-site]}}", + // Skipping `Sec-Fetch-User`, as the test harness isn't consistent here. + "dest": "{{headers[sec-fetch-dest]}}" + }; + + assert_header_equals(actual, expected); + }, "This page's top-level navigation."); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/object.https.sub.html b/testing/web-platform/tests/fetch/metadata/object.https.sub.html new file mode 100644 index 0000000000..fae5b37b59 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/object.https.sub.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body> +<script> + let nonce = token(); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "object-same-origin" + nonce; + + let e = document.createElement('object'); + e.data = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"same-origin", "user":"", "mode":"navigate", "dest": "object"}; + validate_expectations(key, expected, "Same-Origin object") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Same-Origin object"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "object-same-site" + nonce; + + let e = document.createElement('object'); + e.data = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"same-site", "user":"", "mode":"navigate", "dest": "object"}; + validate_expectations(key, expected, "Same-Site object") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Same-Site object"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "object-cross-site" + nonce; + + let e = document.createElement('object'); + e.data = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"cross-site", "user":"", "mode":"navigate", "dest": "object"}; + validate_expectations(key, expected, "Cross-Site object") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Cross-Site object"); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/paint-worklet.https.html b/testing/web-platform/tests/fetch/metadata/paint-worklet.https.html new file mode 100644 index 0000000000..49fc7765f6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/paint-worklet.https.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> + +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<script> + + promise_test(async t => { + const nonce = token(); + const key = "worklet-destination" + nonce; + + await CSS.paintWorklet.addModule("/fetch/metadata/resources/record-header.py?file=" + key); + const expected = {"site": "same-origin", "user": "", "mode": "cors", "dest": "paintworklet"}; + await validate_expectations(key, expected); + }, "The fetch metadata for paint worklet"); + +</script> +<body></body> diff --git a/testing/web-platform/tests/fetch/metadata/preload.https.sub.html b/testing/web-platform/tests/fetch/metadata/preload.https.sub.html new file mode 100644 index 0000000000..29042a8547 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/preload.https.sub.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body></body> +<script> + test(t => { + assert_true(document.createElement('link').relList.supports('preload')); + }, "Browser supports preload."); + + function create_test(host, as, expected) { + async_test(t => { + let nonce = token(); + let key = as + nonce; + + let e = document.createElement('link'); + e.rel = "preload"; + e.href = `https://${host}/fetch/metadata/resources/record-header.py?file=${key}`; + e.setAttribute("crossorigin", "crossorigin"); + if (as !== undefined) { + e.setAttribute("as", as); + } + e.onload = e.onerror = t.step_func(e => { + fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key) + .then(t.step_func(response => response.text())) + .then(t.step_func_done(text => assert_header_equals(text, expected, `preload ${as} ${host}`))) + .catch(t.unreached_func()); + }); + + document.head.appendChild(e); + }, `<link rel='preload' as='${as}' href='https://${host}/...'>`); + } + + let as_tests = [ + [ "fetch", "empty" ], + [ "font", "font" ], + [ "image", "image" ], + [ "script", "script" ], + [ "style", "style" ], + [ "track", "track" ], + ]; + + as_tests.forEach(item => { + create_test("{{host}}:{{ports[https][0]}}", item[0], {"site":"same-origin", "user":"", "mode": "cors", "dest": item[1]}); + create_test("{{hosts[][www]}}:{{ports[https][0]}}", item[0], {"site":"same-site", "user":"", "mode": "cors", "dest": item[1]}); + create_test("{{hosts[alt][www]}}:{{ports[https][0]}}", item[0], {"site":"cross-site", "user":"", "mode": "cors", "dest": item[1]}); + }); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html b/testing/web-platform/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html new file mode 100644 index 0000000000..0f8f320016 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script> +<script src=/common/security-features/resources/common.sub.js></script> +<script src=/common/utils.js></script> +<body> +<script> + let nonce = token(); + let expected = {"site": "cross-site", "user": "", "mode": "cors", "dest": "font"}; + + // Validate various scenarios handle a request that redirects from https => http => https + // correctly and avoids disclosure of any Sec- headers. + RunCommonRedirectTests("Https downgrade-upgrade", MultipleRedirectTo, expected); +</script> +</body> diff --git a/testing/web-platform/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html b/testing/web-platform/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html new file mode 100644 index 0000000000..fa765b66d0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script> +<script src=/common/security-features/resources/common.sub.js></script> +<script src=/common/utils.js></script> +<body> +<script> + let nonce = token(); + let expected = { "site": "cross-site", "user": "", "mode": "cors", "dest": "font" }; + + // Validate various scenarios handle a request that redirects from http => https correctly and add the proper Sec- headers. + RunCommonRedirectTests("Http upgrade", upgradeRedirectTo, expected); +</script> +</body> diff --git a/testing/web-platform/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html b/testing/web-platform/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html new file mode 100644 index 0000000000..4e5a48e6f6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/fetch/metadata/resources/redirectTestHelper.sub.js></script> +<script src=/common/security-features/resources/common.sub.js></script> +<script src=/common/utils.js></script> +<body> + <script> + let nonce = token(); + let expected = { "site": "", "user": "", "mode": "", "dest": "" }; + + // Validate various scenarios handle a request that redirects from https => http correctly and avoids disclosure of any Sec- headers. + RunCommonRedirectTests("Https downgrade", downgradeRedirectTo, expected); +</script> +</body> diff --git a/testing/web-platform/tests/fetch/metadata/report.https.sub.html b/testing/web-platform/tests/fetch/metadata/report.https.sub.html new file mode 100644 index 0000000000..b65f7c0a24 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/report.https.sub.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script> + setup({ explicit_done: true }); + function generate_test(expected, name) { + async_test(t => { + t.step_timeout(_ => { + return validate_expectations("report-" + name, expected, name + " report") + .then(_ => t.done()); + }, 1000); + }, name + " report"); + } + + let counter = 0; + document.addEventListener("securitypolicyviolation", (e) => { + counter++; + if (counter == 3) { + generate_test({"site":"same-origin", "user":"", "mode": "no-cors", "dest": "report"}, "same-origin"); + generate_test({"site":"same-site", "user":"", "mode": "no-cors", "dest": "report"}, "same-site"); + generate_test({"site":"cross-site", "user":"", "mode": "no-cors", "dest": "report"}, "cross-site"); + + done(); + } + }); +</script> + +<!-- The hostname here is unimportant, so long as it doesn't match 'self'. --> +<link id="style" href="https://{{hosts[alt][élève]}}:{{ports[https][0]}}/css/support/a-green.css" rel="stylesheet"> + +<body></body> diff --git a/testing/web-platform/tests/fetch/metadata/report.https.sub.html.sub.headers b/testing/web-platform/tests/fetch/metadata/report.https.sub.html.sub.headers new file mode 100644 index 0000000000..1ec5df78f3 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/report.https.sub.html.sub.headers @@ -0,0 +1,3 @@ +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri /fetch/metadata/resources/record-header.py?file=report-same-origin +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-same-site +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-cross-site diff --git a/testing/web-platform/tests/fetch/metadata/resources/appcache-iframe.sub.html b/testing/web-platform/tests/fetch/metadata/resources/appcache-iframe.sub.html new file mode 100644 index 0000000000..cea9a4feae --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/appcache-iframe.sub.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en" manifest="{{GET[manifest]}}"> +<script> +if (!window.applicationCache) { + parent.postMessage('application cache not supported'); +} else { + applicationCache.onnoupdate = + applicationCache.ondownloading = + applicationCache.onobsolete = + applicationCache.onerror = function() { + parent.postMessage('okay'); + }; +} +</script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/resources/dedicatedWorker.js b/testing/web-platform/tests/fetch/metadata/resources/dedicatedWorker.js new file mode 100644 index 0000000000..18626d3d84 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/dedicatedWorker.js @@ -0,0 +1 @@ +self.postMessage("Loaded"); diff --git a/testing/web-platform/tests/fetch/metadata/resources/echo-as-json.py b/testing/web-platform/tests/fetch/metadata/resources/echo-as-json.py new file mode 100644 index 0000000000..44f68e8fe9 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/echo-as-json.py @@ -0,0 +1,29 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [(b"Content-Type", b"application/json"), + (b"Access-Control-Allow-Credentials", b"true")] + + if b"origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers[b"origin"])) + + body = u"" + + # If we're in a preflight, verify that `Sec-Fetch-Mode` is `cors`. + if request.method == u'OPTIONS': + if request.headers.get(b"sec-fetch-mode") != b"cors": + return (403, b"Failed"), [], body + + headers.append((b"Access-Control-Allow-Methods", b"*")) + headers.append((b"Access-Control-Allow-Headers", b"*")) + else: + body = json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + + return headers, body diff --git a/testing/web-platform/tests/fetch/metadata/resources/echo-as-script.py b/testing/web-platform/tests/fetch/metadata/resources/echo-as-script.py new file mode 100644 index 0000000000..1e7bc91184 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/echo-as-script.py @@ -0,0 +1,14 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [(b"Content-Type", b"text/javascript")] + body = u"var header = %s;" % json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + + return headers, body diff --git a/testing/web-platform/tests/fetch/metadata/resources/es-module.sub.js b/testing/web-platform/tests/fetch/metadata/resources/es-module.sub.js new file mode 100644 index 0000000000..f9668a3dc6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/es-module.sub.js @@ -0,0 +1 @@ +import '{{GET[moduleId]}}'; diff --git a/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js new file mode 100644 index 0000000000..09858b2663 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + // Empty event handler - will fallback to the network. +}); diff --git a/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js new file mode 100644 index 0000000000..8bf8d8f221 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); diff --git a/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html new file mode 100644 index 0000000000..9879802500 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Page Title</title> diff --git a/testing/web-platform/tests/fetch/metadata/resources/header-link.py b/testing/web-platform/tests/fetch/metadata/resources/header-link.py new file mode 100644 index 0000000000..de891163a3 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/header-link.py @@ -0,0 +1,15 @@ +def main(request, response): + """ + Respond with a blank HTML document and a `Link` header which describes + a link relation specified by the requests `location` and `rel` query string + parameters + """ + headers = [ + (b'Content-Type', b'text/html'), + ( + b'Link', + b'<' + request.GET.first(b'location') + b'>; rel=' + request.GET.first(b'rel') + ) + ] + return (200, headers, b'') + diff --git a/testing/web-platform/tests/fetch/metadata/resources/helper.js b/testing/web-platform/tests/fetch/metadata/resources/helper.js new file mode 100644 index 0000000000..725f9a7e43 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/helper.js @@ -0,0 +1,42 @@ +function validate_expectations(key, expected, tag) { + return fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +function validate_expectations_custom_url(url, header, expected, tag) { + return fetch(url, header) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +/** + * @param {object} value + * @param {object} expected + * @param {string} tag + **/ +function assert_header_equals(value, expected, tag) { + if (typeof(value) === "string"){ + assert_not_equals(value, "No header has been recorded"); + value = JSON.parse(value); + } + + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); +} diff --git a/testing/web-platform/tests/fetch/metadata/resources/helper.sub.js b/testing/web-platform/tests/fetch/metadata/resources/helper.sub.js new file mode 100644 index 0000000000..fd179fe6f2 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/helper.sub.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Construct a URL which, when followed, will trigger redirection through zero + * or more specified origins and ultimately resolve in the Python handler + * `record-headers.py`. + * + * @param {string} key - the WPT server "stash" name where the request's + * headers should be stored + * @param {string[]} [origins] - zero or more origin names through which the + * request should pass; see the function + * implementation for a completel list of names + * and corresponding origins; If specified, the + * final origin will be used to access the + * `record-headers.py` hander. + * @param {object} [params] - a collection of key-value pairs to include as + * URL "search" parameters in the final request to + * `record-headers.py` + * + * @returns {string} an absolute URL + */ +function makeRequestURL(key, origins, params) { + const byName = { + httpOrigin: 'http://{{host}}:{{ports[http][0]}}', + httpSameSite: 'http://{{hosts[][www]}}:{{ports[http][0]}}', + httpCrossSite: 'http://{{hosts[alt][]}}:{{ports[http][0]}}', + httpsOrigin: 'https://{{host}}:{{ports[https][0]}}', + httpsSameSite: 'https://{{hosts[][www]}}:{{ports[https][0]}}', + httpsCrossSite: 'https://{{hosts[alt][]}}:{{ports[https][0]}}' + }; + const redirectPath = '/fetch/api/resources/redirect.py?location='; + const path = '/fetch/metadata/resources/record-headers.py?key=' + key; + + let requestUrl = path; + if (params) { + requestUrl += '&' + new URLSearchParams(params).toString(); + } + + if (origins && origins.length) { + requestUrl = byName[origins.pop()] + requestUrl; + + while (origins.length) { + requestUrl = byName[origins.pop()] + redirectPath + + encodeURIComponent(requestUrl); + } + } else { + requestUrl = byName.httpsOrigin + requestUrl; + } + + return requestUrl; +} + +function retrieve(key, options) { + return fetch('/fetch/metadata/resources/record-headers.py?retrieve&key=' + key) + .then((response) => { + if (response.status === 204 && options && options.poll) { + return new Promise((resolve) => setTimeout(resolve, 300)) + .then(() => retrieve(key, options)); + } + + if (response.status !== 200) { + throw new Error('Failed to query for recorded headers.'); + } + + return response.text().then((text) => JSON.parse(text)); + }); +} diff --git a/testing/web-platform/tests/fetch/metadata/resources/message-opener.html b/testing/web-platform/tests/fetch/metadata/resources/message-opener.html new file mode 100644 index 0000000000..eb2af7b250 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/message-opener.html @@ -0,0 +1,17 @@ +<script> +/** + * Send a message to the opening browsing context when the document is + * "completely loaded" (a condition which occurs immediately after the `load` + * and `pageshow` events are fired). + * https://html.spec.whatwg.org/multipage/parsing.html#the-end + */ +'use strict'; + +// The `pageshow` event is used instead of the `load` event because this +// document may itself be accessed via history traversal. In such cases, the +// browser may choose to reuse a cached document and therefore fire no +// additional `load` events. +addEventListener('pageshow', () => { + setTimeout(() => opener.postMessage(null, '*'), 0); +}); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/resources/post-to-owner.py b/testing/web-platform/tests/fetch/metadata/resources/post-to-owner.py new file mode 100644 index 0000000000..256dd6e49d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/post-to-owner.py @@ -0,0 +1,36 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [ + (b"Content-Type", b"text/html"), + (b"Cache-Control", b"no-cache, no-store, must-revalidate") + ] + key = request.GET.first(b"key", None) + + # We serialize the key into JSON, so have to decode it first. + if key is not None: + key = key.decode('utf-8') + + body = u""" + <!DOCTYPE html> + <script src="/portals/resources/stash-utils.sub.js"></script> + <script> + var data = %s; + if (window.opener) + window.opener.postMessage(data, "*"); + if (window.top != window) + window.top.postMessage(data, "*"); + + const key = %s; + if (key) + StashUtils.putValue(key, data); + </script> + """ % (json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }), json.dumps(key)) + return headers, body diff --git a/testing/web-platform/tests/fetch/metadata/resources/record-header.py b/testing/web-platform/tests/fetch/metadata/resources/record-header.py new file mode 100644 index 0000000000..29ff2ed798 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/record-header.py @@ -0,0 +1,145 @@ +import os +import hashlib +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + ## Get the query parameter (key) from URL ## + ## Tests will record POST requests (CSP Report) and GET (rest) ## + if request.GET: + key = request.GET[b'file'] + elif request.POST: + key = request.POST[b'file'] + + ## Convert the key from String to UUID valid String ## + testId = hashlib.md5(key).hexdigest() + + ## Handle the header retrieval request ## + if b'retrieve' in request.GET: + response.writer.write_status(200) + response.writer.write_header(b"Connection", b"close") + response.writer.end_headers() + try: + header_value = request.server.stash.take(testId) + response.writer.write(header_value) + except (KeyError, ValueError) as e: + response.writer.write(u"No header has been recorded") + pass + + response.close_connection = True + + ## Record incoming fetch metadata header value + else: + try: + ## Return a serialized JSON object with one member per header. If the ## + ## header isn't present, the member will contain an empty string. ## + header = json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + request.server.stash.put(testId, header) + except KeyError: + ## The header is already recorded or it doesn't exist + pass + + ## Prevent the browser from caching returned responses and allow CORS ## + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") + response.headers.set(b"Pragma", b"no-cache") + response.headers.set(b"Expires", b"0") + + ## Add a valid ServiceWorker Content-Type ## + if key.startswith(b"serviceworker"): + response.headers.set(b"Content-Type", b"application/javascript") + + ## Add a valid image Content-Type ## + if key.startswith(b"image"): + response.headers.set(b"Content-Type", b"image/png") + file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") + image = file.read() + file.close() + return image + + ## Return a valid .vtt content for the <track> tag ## + if key.startswith(b"track"): + return b"WEBVTT" + + ## Return a valid SharedWorker ## + if key.startswith(b"sharedworker"): + response.headers.set(b"Content-Type", b"application/javascript") + file = open(os.path.join(request.doc_root, u"fetch", u"metadata", + u"resources", u"sharedWorker.js"), u"rb") + shared_worker = file.read() + file.close() + return shared_worker + + ## Return a valid font content and Content-Type ## + if key.startswith(b"font"): + response.headers.set(b"Content-Type", b"application/x-font-ttf") + file = open(os.path.join(request.doc_root, u"fonts", u"Ahem.ttf"), u"rb") + font = file.read() + file.close() + return font + + ## Return a valid audio content and Content-Type ## + if key.startswith(b"audio"): + response.headers.set(b"Content-Type", b"audio/mpeg") + file = open(os.path.join(request.doc_root, u"media", u"sound_5.mp3"), u"rb") + audio = file.read() + file.close() + return audio + + ## Return a valid video content and Content-Type ## + if key.startswith(b"video"): + response.headers.set(b"Content-Type", b"video/mp4") + file = open(os.path.join(request.doc_root, u"media", u"A4.mp4"), u"rb") + video = file.read() + file.close() + return video + + ## Return valid style content and Content-Type ## + if key.startswith(b"style"): + response.headers.set(b"Content-Type", b"text/css") + return b"div { }" + + ## Return a valid embed/object content and Content-Type ## + if key.startswith(b"embed") or key.startswith(b"object"): + response.headers.set(b"Content-Type", b"text/html") + return b"<html>EMBED!</html>" + + ## Return a valid image content and Content-Type for redirect requests ## + if key.startswith(b"redirect"): + response.headers.set(b"Content-Type", b"image/jpeg") + file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") + image = file.read() + file.close() + return image + + ## Return a valid dedicated worker + if key.startswith(b"worker"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"self.postMessage('loaded');" + + ## Return a valid worklet + if key.startswith(b"worklet"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"" + + ## Return a valid XSLT + if key.startswith(b"xslt"): + response.headers.set(b"Content-Type", b"text/xsl") + return b"""<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> + <xsl:template match="@*|node()"> + <xsl:copy> + <xsl:apply-templates select="@*|node()"/> + </xsl:copy> + </xsl:template> +</xsl:stylesheet>""" + + if key.startswith(b"script"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"void 0;" diff --git a/testing/web-platform/tests/fetch/metadata/resources/record-headers.py b/testing/web-platform/tests/fetch/metadata/resources/record-headers.py new file mode 100644 index 0000000000..0362fe228c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/record-headers.py @@ -0,0 +1,73 @@ +import os +import uuid +import hashlib +import time +import json + + +def bytes_to_strings(d): + # Recursively convert bytes to strings in `d`. + if not isinstance(d, dict): + if isinstance(d, (tuple,list,set)): + v = [bytes_to_strings(x) for x in d] + return v + else: + if isinstance(d, bytes): + d = d.decode() + return d + + result = {} + for k,v in d.items(): + if isinstance(k, bytes): + k = k.decode() + if isinstance(v, dict): + v = bytes_to_strings(v) + elif isinstance(v, (tuple,list,set)): + v = [bytes_to_strings(x) for x in v] + elif isinstance(v, bytes): + v = v.decode() + result[k] = v + return result + + +def main(request, response): + # This condition avoids false positives from CORS preflight checks, where the + # request under test may be followed immediately by a request to the same URL + # using a different HTTP method. + if b'requireOPTIONS' in request.GET and request.method != b'OPTIONS': + return + + if b'key' in request.GET: + key = request.GET[b'key'] + elif b'key' in request.POST: + key = request.POST[b'key'] + + ## Convert the key from String to UUID valid String ## + testId = hashlib.md5(key).hexdigest() + + ## Handle the header retrieval request ## + if b'retrieve' in request.GET: + recorded_headers = request.server.stash.take(testId) + + if recorded_headers is None: + return (204, [], b'') + + return (200, [], recorded_headers) + + ## Record incoming fetch metadata header value + else: + try: + request.server.stash.put(testId, json.dumps(bytes_to_strings(request.headers))) + except KeyError: + ## The header is already recorded or it doesn't exist + pass + + ## Prevent the browser from caching returned responses and allow CORS ## + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") + response.headers.set(b"Pragma", b"no-cache") + response.headers.set(b"Expires", b"0") + if b"mime" in request.GET: + response.headers.set(b"Content-Type", request.GET.first(b"mime")) + + return request.GET.first(b"body", request.POST.first(b"body", b"")) diff --git a/testing/web-platform/tests/fetch/metadata/resources/redirectTestHelper.sub.js b/testing/web-platform/tests/fetch/metadata/resources/redirectTestHelper.sub.js new file mode 100644 index 0000000000..1bfbbae70c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/redirectTestHelper.sub.js @@ -0,0 +1,167 @@ +function createVideoElement() { + let el = document.createElement('video'); + el.src = '/media/movie_5.mp4'; + el.setAttribute('controls', ''); + el.setAttribute('crossorigin', ''); + return el; +} + +function createTrack() { + let el = document.createElement('track'); + el.setAttribute('default', ''); + el.setAttribute('kind', 'captions'); + el.setAttribute('srclang', 'en'); + return el; +} + +let secureRedirectURL = 'https://{{host}}:{{ports[https][0]}}/fetch/api/resources/redirect.py?location='; +let insecureRedirectURL = 'http://{{host}}:{{ports[http][0]}}/fetch/api/resources/redirect.py?location='; +let secureTestURL = 'https://{{host}}:{{ports[https][0]}}/fetch/metadata/'; +let insecureTestURL = 'http://{{host}}:{{ports[http][0]}}/fetch/metadata/'; + +// Helper to craft an URL that will go from HTTPS => HTTP => HTTPS to +// simulate us downgrading then upgrading again during the same redirect chain. +function MultipleRedirectTo(partialPath) { + let finalURL = insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); + return secureRedirectURL + encodeURIComponent(finalURL); +} + +// Helper to craft an URL that will go from HTTP => HTTPS to simulate upgrading a +// given request. +function upgradeRedirectTo(partialPath) { + return insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); +} + +// Helper to craft an URL that will go from HTTPS => HTTP to simulate downgrading a +// given request. +function downgradeRedirectTo(partialPath) { + return secureRedirectURL + encodeURIComponent(insecureTestURL + partialPath); +} + +// Helper to run common redirect test cases that don't require special setup on +// the test page itself. +function RunCommonRedirectTests(testNamePrefix, urlHelperMethod, expectedResults) { + async_test(t => { + let testWindow = window.open(urlHelperMethod('resources/post-to-owner.py?top-level-navigation' + nonce)); + t.add_cleanup(_ => testWindow.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != testWindow) { + return; + } + + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'document'; + assert_header_equals(e.data, expectation, testNamePrefix + ' top level navigation'); + t.done(); + })); + }, testNamePrefix + ' top level navigation'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'embed-https-redirect' + nonce; + let e = document.createElement('embed'); + e.src = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'embed'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' embed'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' embed'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'object-https-redirect' + nonce; + let e = document.createElement('object'); + e.data = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'object'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' object'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' object'); + + if (document.createElement('link').relList.supports('preload')) { + async_test(t => { + let key = 'preload' + nonce; + let e = document.createElement('link'); + e.rel = 'preload'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.setAttribute('as', 'track'); + e.onload = e.onerror = t.step_func_done(e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(t.step_func(response => response.text())) + .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' preload'))) + .catch(t.unreached_func()); + }); + document.head.appendChild(e); + }, testNamePrefix + ' preload'); + } + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'style-https-redirect' + nonce; + let e = document.createElement('link'); + e.rel = 'stylesheet'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'no-cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'style'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' stylesheet'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' stylesheet'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'track-https-redirect' + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = urlHelperMethod('resources/record-header.py?file=' + key); + el.onload = t.step_func(_ => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'track'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' track'))) + .then(resolve); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, testNamePrefix + ' track'); +} diff --git a/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors-frame.html b/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors-frame.html new file mode 100644 index 0000000000..9879802500 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors-frame.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Page Title</title> diff --git a/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors.sw.js b/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors.sw.js new file mode 100644 index 0000000000..36c55a7786 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/serviceworker-accessors.sw.js @@ -0,0 +1,14 @@ +addEventListener("fetch", event => { + event.waitUntil(async function () { + if (!event.clientId) return; + const client = await clients.get(event.clientId); + if (!client) return; + + client.postMessage({ + "dest": event.request.headers.get("sec-fetch-dest"), + "mode": event.request.headers.get("sec-fetch-mode"), + "site": event.request.headers.get("sec-fetch-site"), + "user": event.request.headers.get("sec-fetch-user") + }); + }()); +}); diff --git a/testing/web-platform/tests/fetch/metadata/resources/sharedWorker.js b/testing/web-platform/tests/fetch/metadata/resources/sharedWorker.js new file mode 100644 index 0000000000..5eb89cb4f6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/sharedWorker.js @@ -0,0 +1,9 @@ +onconnect = function(e) { + var port = e.ports[0]; + + port.addEventListener('message', function(e) { + port.postMessage("Ready"); + }); + + port.start(); +} diff --git a/testing/web-platform/tests/fetch/metadata/resources/unload-with-beacon.html b/testing/web-platform/tests/fetch/metadata/resources/unload-with-beacon.html new file mode 100644 index 0000000000..b00c9a5776 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/unload-with-beacon.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + // When told, register an unload handler that will trigger a beacon to the + // URL given by the sender of the message. + window.addEventListener('message', e => { + var url = e.data; + window.addEventListener('unload', () => { + navigator.sendBeacon(url, 'blah'); + }); + window.parent.postMessage('navigate-away', '*'); + }); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/resources/xslt-test.sub.xml b/testing/web-platform/tests/fetch/metadata/resources/xslt-test.sub.xml new file mode 100644 index 0000000000..acb478ab64 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/resources/xslt-test.sub.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=xslt-same-origin{{GET[token]}}" type="text/xsl" ?> +<!-- Only testing same-origin XSLT because same-site and cross-site XSLT is blocked. --> + +<!-- postMessage parent back when the resources are loaded --> +<script xmlns="http://www.w3.org/1999/xhtml"><![CDATA[ + setTimeout(function(){ + if (window.opener) + window.opener.postMessage("", "*"); + if (window.top != window) + window.top.postMessage("", "*");}, 100); +]]></script> diff --git a/testing/web-platform/tests/fetch/metadata/serviceworker-accessors.https.sub.html b/testing/web-platform/tests/fetch/metadata/serviceworker-accessors.https.sub.html new file mode 100644 index 0000000000..03a8321d4c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/serviceworker-accessors.https.sub.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- + This test verifies that Fetch Metadata headers are not exposed to Service + Workers via the request's `headers` accessor. +--> +<meta charset="utf-8"/> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/service-workers/service-worker/resources/test-helpers.sub.js></script> +<script src=/common/utils.js></script> +<script> + const SCOPE = 'resources/serviceworker-accessors-frame.html'; + const SCRIPT = 'resources/serviceworker-accessors.sw.js'; + + function assert_headers_not_seen_in_service_worker(frame) { + return new Promise((resolve, reject) => { + frame.contentWindow.fetch(SCOPE, {mode:'no-cors'}); + frame.contentWindow.navigator.serviceWorker.addEventListener('message', e => { + assert_header_equals(e.data, { + "dest": null, + "mode": null, + "site": null, + "user": null + }); + resolve(); + }); + }); + } + + promise_test(async function(t) { + const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE); + + t.add_cleanup(async () => { + if (reg) + await reg.unregister(); + }); + + await wait_for_state(t, reg.installing, 'activated'); + + const frame = await with_iframe(SCOPE); + t.add_cleanup(async () => { + if (frame) + frame.remove(); + }); + + // Trigger a fetch that will go through the service worker, and validate + // the visible headers. + await assert_headers_not_seen_in_service_worker(frame); + }, 'Sec-Fetch headers in Service Worker fetch handler.'); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/sharedworker.https.sub.html b/testing/web-platform/tests/fetch/metadata/sharedworker.https.sub.html new file mode 100644 index 0000000000..4df858208a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/sharedworker.https.sub.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> + +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<script> + let nonce = token(); + let key = "sharedworker-same-origin" + nonce; + + // TESTS // + if (window.Worker) { + + // Same-Origin test + var sharedWorker = new SharedWorker('/fetch/metadata/resources/record-header.py?file=' + key); + sharedWorker.port.start(); + + sharedWorker.onerror = function(){ + test_same_origin(); + } + sharedWorker.port.onmessage = function(e) { + test_same_origin(); + } + sharedWorker.port.postMessage("Ready"); + } + + function test_same_origin(){ + promise_test(t => { + return new Promise((resolve, reject) => { + let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "sharedworker"}; + + validate_expectations(key, expected) + .then(_ => resolve()) + .catch(e => reject(e)); + }) + }, "Same-Origin sharedworker") + } +</script> +<body></body> diff --git a/testing/web-platform/tests/fetch/metadata/style.https.sub.html b/testing/web-platform/tests/fetch/metadata/style.https.sub.html new file mode 100644 index 0000000000..a30d81d70d --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/style.https.sub.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html> +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body></body> +<script> + let nonce = token(); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "style-same-origin" + nonce; + + let e = document.createElement('link'); + e.rel = "stylesheet"; + e.href = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"same-origin", "user":"", "mode": "no-cors", "dest": "style"}; + validate_expectations(key, expected, "Same-Origin style") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Same-Origin style"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "style-same-site" + nonce; + + let e = document.createElement('link'); + e.rel = "stylesheet"; + e.href = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"same-site", "user":"", "mode": "no-cors", "dest": "style"}; + validate_expectations(key, expected, "Same-Site style") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Same-Site style"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "style-cross-site" + nonce; + + let e = document.createElement('link'); + e.rel = "stylesheet"; + e.href = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.onload = e => { + let expected = {"site":"cross-site", "user":"", "mode": "no-cors", "dest": "style"}; + validate_expectations(key, expected, "Cross-Site style") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Cross-Site style"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "style-same-origin-cors" + nonce; + + let e = document.createElement('link'); + e.rel = "stylesheet"; + e.href = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + e.crossOrigin = "anonymous"; + e.onload = e => { + let expected = {"site":"same-origin", "user":"", "mode": "cors", "dest": "style"}; + validate_expectations(key, expected, "Same-Origin, cors style") + .then(_ => resolve()) + .catch(e => reject(e)); + }; + + document.body.appendChild(e); + }) + }, "Same-Origin, cors style"); +</script> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/tools/README.md b/testing/web-platform/tests/fetch/metadata/tools/README.md new file mode 100644 index 0000000000..1c3bac2be5 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/README.md @@ -0,0 +1,126 @@ +# Fetch Metadata test generation framework + +This directory defines a command-line tool for procedurally generating WPT +tests. + +## Motivation + +Many features of the web platform involve the browser making one or more HTTP +requests to remote servers. Only some aspects of these requests are specified +within the standard that defines the relevant feature. Other aspects are +specified by external standards which span the entire platform (e.g. [Fetch +Metadata Request Headers](https://w3c.github.io/webappsec-fetch-metadata/)). + +This state of affairs makes it difficult to maintain test coverage for two +reasons: + +- When a new feature introduces a new kind of web request, it must be verified + to integrate with every cross-cutting standard. +- When a new cross-cutting standard is introduced, it must be verified to + integrate with every kind of web request. + +The tool in this directory attempts to reduce this tension. It allows +maintainers to express instructions for making web requests in an abstract +sense. These generic instructions can be reused by to produce a different suite +of tests for each cross-cutting feature. + +When a new kind of request is proposed, a single generic template can be +defined here. This will provide the maintainers of all cross-cutting features +with clear instruction on how to extend their test suite with the new feature. + +Similarly, when a new cross-cutting feature is proposed, the authors can use +this tool to build a test suite which spans the entire platform. + +## Build script + +To generate the Fetch Metadata tests, run `./wpt update-built --include fetch` +in the root of the repository. + +## Configuration + +The test generation tool requires a YAML-formatted configuration file as its +input. The file should define a dictionary with the following keys: + +- `templates` - a string describing the filesystem path from which template + files should be loaded +- `output_directory` - a string describing the filesystem path where the + generated test files should be written +- `cases` - a list of dictionaries describing how the test templates should be + expanded with individual subtests; each dictionary should have the following + keys: + - `all_subtests` - properties which should be defined for every expansion + - `common_axis` - a list of dictionaries + - `template_axes` - a dictionary relating template names to properties that + should be used when expanding that particular template + +Internally, the tool creates a set of "subtests" for each template. This set is +the Cartesian product of the `common_axis` and the given template's entry in +the `template_axes` dictionary. It uses this set of subtests to expand the +template, creating an output file. Refer to the next section for a concrete +example of how the expansion is performed. + +In general, the tool will output a single file for each template. However, the +`filename_flags` attribute has special semantics. It is used to separate +subtests for the same template file. This is intended to accommodate [the +web-platform-test's filename-based +conventions](https://web-platform-tests.org/writing-tests/file-names.html). + +For instance, when `.https` is present in a test file's name, the WPT test +harness will load that test using the HTTPS protocol. Subtests which include +the value `https` in the `filename_flags` property will be expanded using the +appropriate template but written to a distinct file whose name includes +`.https`. + +The generation tool requires that the configuration file references every +template in the `templates` directory. Because templates and configuration +files may be contributed by different people, this requirement ensures that +configuration authors are aware of all available templates. Some templates may +not be relevant for some features; in those cases, the configuration file can +include an empty array for the template's entry in the `template_axes` +dictionary (as in `template3.html` in the example which follows). + +## Expansion example + +In the following example configuration file, `a`, `b`, `s`, `w`, `x`, `y`, and +`z` all represent associative arrays. + +```yaml +templates: path/to/templates +output_directory: path/to/output +cases: + - every_subtest: s + common_axis: [a, b] + template_axes: + template1.html: [w] + template2.html: [x, y, z] + template3.html: [] +``` + +When run with such a configuration file, the tool would generate two files, +expanded with data as described below (where `(a, b)` represents the union of +`a` and `b`): + + template1.html: [(a, w), (b, w)] + template2.html: [(a, x), (b, x), (a, y), (b, y), (a, z), (b, z)] + template3.html: (zero tests; not expanded) + +## Design Considerations + +**Efficiency of generated output** The tool is capable of generating a large +number of tests given a small amount of input. Naively structured, this could +result in test suites which take large amount of time and computational +resources to complete. The tool has been designed to help authors structure the +generated output to reduce these resource requirements. + +**Literalness of generated output** Because the generated output is how most +people will interact with the tests, it is important that it be approachable. +This tool avoids outputting abstractions which would frustrate attempts to read +the source code or step through its execution environment. + +**Simplicity** The test generation logic itself was written to be approachable. +This makes it easier to anticipate how the tool will behave with new input, and +it lowers the bar for others to contribute improvements. + +Non-goals include conciseness of template files (verbosity makes the potential +expansions more predictable) and conciseness of generated output (verbosity +aids in the interpretation of results). diff --git a/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml b/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml new file mode 100644 index 0000000000..b277bcb7b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/fetch-metadata.conf.yml @@ -0,0 +1,806 @@ +--- +templates: templates +output_directory: ../generated +cases: + - all_subtests: + expected: NULL + filename_flags: [] + common_axis: + - headerName: sec-fetch-site + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-site + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-site + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-mode + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-mode + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-mode + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-dest + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-dest + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-dest + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-user + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-user + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-user + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + template_axes: + # Unused + appcache-manifest.sub.https.html: [] + # The `audioWorklet` interface is only available in secure contexts + # https://webaudio.github.io/web-audio-api/#BaseAudioContext + audioworklet.https.sub.html: [] + # Service workers are only available in secure context + fetch-via-serviceworker.https.sub.html: [] + # Service workers are only available in secure context + serviceworker.https.sub.html: [] + + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + window-history.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + worker-dedicated-constructor.sub.html: [{}] + + # Sec-Fetch-Site - direct requests + - all_subtests: + headerName: sec-fetch-site + filename_flags: [https] + common_axis: + - description: Same origin + origins: [httpsOrigin] + expected: same-origin + - description: Cross-site + origins: [httpsCrossSite] + expected: cross-site + - description: Same site + origins: [httpsSameSite] + expected: same-site + template_axes: + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + audioworklet.https.sub.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{ init: { mode: no-cors } }] + fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + serviceworker.https.sub.html: [{}] + svg-image.sub.html: [{}] + window-history.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection from HTTP + - all_subtests: + headerName: sec-fetch-site + filename_flags: [] + common_axis: + - description: HTTPS downgrade (header not sent) + origins: [httpsOrigin, httpOrigin] + expected: NULL + - description: HTTPS upgrade + origins: [httpOrigin, httpsOrigin] + expected: cross-site + - description: HTTPS downgrade-upgrade + origins: [httpsOrigin, httpOrigin, httpsOrigin] + expected: cross-site + template_axes: + # Unused + # The `audioWorklet` interface is only available in secure contexts + # https://webaudio.github.io/web-audio-api/#BaseAudioContext + audioworklet.https.sub.html: [] + # Service workers are only available in secure context + fetch-via-serviceworker.https.sub.html: [] + # Service workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection from HTTPS + - all_subtests: + headerName: sec-fetch-site + filename_flags: [https] + common_axis: + - description: Same-Origin -> Cross-Site -> Same-Origin redirect + origins: [httpsOrigin, httpsCrossSite, httpsOrigin] + expected: cross-site + - description: Same-Origin -> Same-Site -> Same-Origin redirect + origins: [httpsOrigin, httpsSameSite, httpsOrigin] + expected: same-site + - description: Cross-Site -> Same Origin + origins: [httpsCrossSite, httpsOrigin] + expected: cross-site + - description: Cross-Site -> Same-Site + origins: [httpsCrossSite, httpsSameSite] + expected: cross-site + - description: Cross-Site -> Cross-Site + origins: [httpsCrossSite, httpsCrossSite] + expected: cross-site + - description: Same-Origin -> Same Origin + origins: [httpsOrigin, httpsOrigin] + expected: same-origin + - description: Same-Origin -> Same-Site + origins: [httpsOrigin, httpsSameSite] + expected: same-site + - description: Same-Origin -> Cross-Site + origins: [httpsOrigin, httpsCrossSite] + expected: cross-site + - description: Same-Site -> Same Origin + origins: [httpsSameSite, httpsOrigin] + expected: same-site + - description: Same-Site -> Same-Site + origins: [httpsSameSite, httpsSameSite] + expected: same-site + - description: Same-Site -> Cross-Site + origins: [httpsSameSite, httpsCrossSite] + expected: cross-site + template_axes: + # Service Workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + audioworklet.https.sub.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{ init: { mode: no-cors } }] + fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection with mixed content + # These tests verify the effect that redirection has on the request's "site". + # The initial request must be made to a resource that is "same-site" with its + # origin. This avoids false positives because if the request were made to a + # cross-site resource, the value of "cross-site" would be assigned regardless + # of the subseqent redirection. + # + # Because these conditions necessarily warrant mixed content, only templates + # which can be configured to allow mixed content [1] can be used. + # + # [1] https://w3c.github.io/webappsec-mixed-content/#should-block-fetch + + - common_axis: + - description: HTTPS downgrade-upgrade + headerName: sec-fetch-site + origins: [httpsOrigin, httpOrigin, httpsOrigin] + expected: cross-site + filename_flags: [https] + template_axes: + # Mixed Content considers only a small subset of requests as + # "optionally-blockable." These are the only requests that can be tested + # for the "downgrade-upgrade" scenario, so all other templates must be + # explicitly ignored. + audioworklet.https.sub.html: [] + css-font-face.sub.html: [] + element-embed.sub.html: [] + element-frame.sub.html: [] + element-iframe.sub.html: [] + element-img-environment-change.sub.html: [] + element-link-icon.sub.html: [] + element-link-prefetch.optional.sub.html: [] + element-picture.sub.html: [] + element-script.sub.html: [] + fetch.sub.html: [] + fetch-via-serviceworker.https.sub.html: [] + header-link.sub.html: [] + script-module-import-static.sub.html: [] + script-module-import-dynamic.sub.html: [] + # Service Workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + worker-dedicated-constructor.sub.html: [] + worker-dedicated-importscripts.sub.html: [] + # Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade' + appcache-manifest.sub.https.html: [] + css-images.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-img.sub.html: + # srcset omitted because it is not "optionally-blockable" + # https://w3c.github.io/webappsec-mixed-content/#category-optionally-blockable + - sourceAttr: src + element-input-image.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-refresh.optional.sub.html: [{}] + svg-image.sub.html: [{}] + window-location.sub.html: [{}] + + # Sec-Fetch-Mode + # These tests are served over HTTPS so the induced requests will be both + # same-origin with the document [1] and a potentially-trustworthy URL [2]. + # + # [1] https://html.spec.whatwg.org/multipage/origin.html#same-origin + # [2] https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-url + - common_axis: + - headerName: sec-fetch-mode + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: no-cors + audioworklet.https.sub.html: + # https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script + - expected: cors + css-images.sub.html: + - expected: no-cors + filename_flags: [tentative] + css-font-face.sub.html: + - expected: cors + filename_flags: [tentative] + element-a.sub.html: + - expected: navigate + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: no-cors + element-area.sub.html: + - expected: navigate + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: no-cors + element-audio.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-embed.sub.html: + - expected: no-cors + element-frame.sub.html: + - expected: navigate + element-iframe.sub.html: + - expected: navigate + element-img.sub.html: + - sourceAttr: src + expected: no-cors + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: '' } + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: anonymous } + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: use-credentials } + - sourceAttr: srcset + expected: no-cors + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: '' } + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: anonymous } + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: use-credentials } + element-img-environment-change.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-input-image.sub.html: + - expected: no-cors + element-link-icon.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-link-prefetch.optional.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-meta-refresh.optional.sub.html: + - expected: navigate + element-picture.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-script.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { type: module } + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-video.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-video-poster.sub.html: + - expected: no-cors + fetch.sub.html: + - expected: cors + - expected: cors + init: { mode: cors } + - expected: no-cors + init: { mode: no-cors } + - expected: same-origin + init: { mode: same-origin } + fetch-via-serviceworker.https.sub.html: + - expected: cors + - expected: cors + init: { mode: cors } + - expected: no-cors + init: { mode: no-cors } + - expected: same-origin + init: { mode: same-origin } + form-submission.sub.html: + - method: GET + expected: navigate + - method: POST + expected: navigate + header-link.sub.html: + - rel: icon + expected: no-cors + - rel: stylesheet + expected: no-cors + header-refresh.optional.sub.html: + - expected: navigate + window-history.sub.html: + - expected: navigate + window-location.sub.html: + - expected: navigate + script-module-import-dynamic.sub.html: + - expected: cors + script-module-import-static.sub.html: + - expected: cors + # https://svgwg.org/svg2-draft/linking.html#processingURL-fetch + svg-image.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + serviceworker.https.sub.html: + - expected: same-origin + options: { type: 'classic' } + # https://github.com/whatwg/html/pull/5875 + - expected: same-origin + worker-dedicated-constructor.sub.html: + - expected: same-origin + - options: { type: module } + expected: same-origin + worker-dedicated-importscripts.sub.html: + - expected: no-cors + + # Sec-Fetch-Dest + - common_axis: + - headerName: sec-fetch-dest + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: empty + audioworklet.https.sub.html: + # https://github.com/WebAudio/web-audio-api/issues/2203 + - expected: audioworklet + css-images.sub.html: + - expected: image + filename_flags: [tentative] + css-font-face.sub.html: + - expected: font + filename_flags: [tentative] + element-a.sub.html: + - expected: document + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: empty + element-area.sub.html: + - expected: document + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: empty + element-audio.sub.html: + - expected: audio + element-embed.sub.html: + - expected: embed + element-frame.sub.html: + # https://github.com/whatwg/html/pull/4976 + - expected: frame + element-iframe.sub.html: + # https://github.com/whatwg/html/pull/4976 + - expected: iframe + element-img.sub.html: + - sourceAttr: src + expected: image + - sourceAttr: srcset + expected: image + element-img-environment-change.sub.html: + - expected: image + element-input-image.sub.html: + - expected: image + element-link-icon.sub.html: + - expected: empty + element-link-prefetch.optional.sub.html: + - expected: empty + - elementAttrs: { as: audio } + expected: audio + - elementAttrs: { as: document } + expected: document + - elementAttrs: { as: embed } + expected: embed + - elementAttrs: { as: fetch } + expected: fetch + - elementAttrs: { as: font } + expected: font + - elementAttrs: { as: image } + expected: image + - elementAttrs: { as: object } + expected: object + - elementAttrs: { as: script } + expected: script + - elementAttrs: { as: style } + expected: style + - elementAttrs: { as: track } + expected: track + - elementAttrs: { as: video } + expected: video + - elementAttrs: { as: worker } + expected: worker + element-meta-refresh.optional.sub.html: + - expected: document + element-picture.sub.html: + - expected: image + element-script.sub.html: + - expected: script + element-video.sub.html: + - expected: video + element-video-poster.sub.html: + - expected: image + fetch.sub.html: + - expected: empty + fetch-via-serviceworker.https.sub.html: + - expected: empty + form-submission.sub.html: + - method: GET + expected: document + - method: POST + expected: document + header-link.sub.html: + - rel: icon + expected: empty + - rel: stylesheet + filename_flags: [tentative] + expected: style + header-refresh.optional.sub.html: + - expected: document + window-history.sub.html: + - expected: document + window-location.sub.html: + - expected: document + script-module-import-dynamic.sub.html: + - expected: script + script-module-import-static.sub.html: + - expected: script + serviceworker.https.sub.html: + - expected: serviceworker + # Implemented as "image" in Chromium and Firefox, but specified as + # "empty" + # https://github.com/w3c/svgwg/issues/782 + svg-image.sub.html: + - expected: empty + worker-dedicated-constructor.sub.html: + - expected: worker + - options: { type: module } + expected: worker + worker-dedicated-importscripts.sub.html: + - expected: script + + # Sec-Fetch-User + - common_axis: + - headerName: sec-fetch-user + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: NULL + audioworklet.https.sub.html: + - expected: NULL + css-images.sub.html: + - expected: NULL + filename_flags: [tentative] + css-font-face.sub.html: + - expected: NULL + filename_flags: [tentative] + element-a.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-area.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-audio.sub.html: + - expected: NULL + element-embed.sub.html: + - expected: NULL + element-frame.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-iframe.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-img.sub.html: + - sourceAttr: src + expected: NULL + - sourceAttr: srcset + expected: NULL + element-img-environment-change.sub.html: + - expected: NULL + element-input-image.sub.html: + - expected: NULL + element-link-icon.sub.html: + - expected: NULL + element-link-prefetch.optional.sub.html: + - expected: NULL + element-meta-refresh.optional.sub.html: + - expected: NULL + element-picture.sub.html: + - expected: NULL + element-script.sub.html: + - expected: NULL + element-video.sub.html: + - expected: NULL + element-video-poster.sub.html: + - expected: NULL + fetch.sub.html: + - expected: NULL + fetch-via-serviceworker.https.sub.html: + - expected: NULL + form-submission.sub.html: + - method: GET + expected: NULL + - method: GET + userActivated: TRUE + expected: ?1 + - method: POST + expected: NULL + - method: POST + userActivated: TRUE + expected: ?1 + header-link.sub.html: + - rel: icon + expected: NULL + - rel: stylesheet + expected: NULL + header-refresh.optional.sub.html: + - expected: NULL + window-history.sub.html: + - expected: NULL + window-location.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + script-module-import-dynamic.sub.html: + - expected: NULL + script-module-import-static.sub.html: + - expected: NULL + serviceworker.https.sub.html: + - expected: NULL + svg-image.sub.html: + - expected: NULL + worker-dedicated-constructor.sub.html: + - expected: NULL + - options: { type: module } + expected: NULL + worker-dedicated-importscripts.sub.html: + - expected: NULL diff --git a/testing/web-platform/tests/fetch/metadata/tools/generate.py b/testing/web-platform/tests/fetch/metadata/tools/generate.py new file mode 100755 index 0000000000..fa850c8c8a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/generate.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +import itertools +import os + +import jinja2 +import yaml + +HERE = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.join(HERE, '..', '..', '..') + +def find_templates(starting_directory): + for directory, subdirectories, file_names in os.walk(starting_directory): + for file_name in file_names: + if file_name.startswith('.'): + continue + yield file_name, os.path.join(directory, file_name) + +def test_name(directory, template_name, subtest_flags): + ''' + Create a test name based on a template and the WPT file name flags [1] + required for a given subtest. This name is used to determine how subtests + may be grouped together. In order to promote grouping, the combination uses + a few aspects of how file name flags are interpreted: + + - repeated flags have no effect, so duplicates are removed + - flag sequence does not matter, so flags are consistently sorted + + directory | template_name | subtest_flags | result + ----------|------------------|-----------------|------- + cors | image.html | [] | cors/image.html + cors | image.https.html | [] | cors/image.https.html + cors | image.html | [https] | cors/image.https.html + cors | image.https.html | [https] | cors/image.https.html + cors | image.https.html | [https] | cors/image.https.html + cors | image.sub.html | [https] | cors/image.https.sub.html + cors | image.https.html | [sub] | cors/image.https.sub.html + + [1] docs/writing-tests/file-names.md + ''' + template_name_parts = template_name.split('.') + flags = set(subtest_flags) | set(template_name_parts[1:-1]) + test_name_parts = ( + [template_name_parts[0]] + + sorted(flags) + + [template_name_parts[-1]] + ) + return os.path.join(directory, '.'.join(test_name_parts)) + +def merge(a, b): + if type(a) != type(b): + raise Exception('Cannot merge disparate types') + if type(a) == list: + return a + b + if type(a) == dict: + merged = {} + + for key in a: + if key in b: + merged[key] = merge(a[key], b[key]) + else: + merged[key] = a[key] + + for key in b: + if not key in a: + merged[key] = b[key] + + return merged + + raise Exception('Cannot merge {} type'.format(type(a).__name__)) + +def product(a, b): + ''' + Given two lists of objects, compute their Cartesian product by merging the + elements together. For example, + + product( + [{'a': 1}, {'b': 2}], + [{'c': 3}, {'d': 4}, {'e': 5}] + ) + + returns the following list: + + [ + {'a': 1, 'c': 3}, + {'a': 1, 'd': 4}, + {'a': 1, 'e': 5}, + {'b': 2, 'c': 3}, + {'b': 2, 'd': 4}, + {'b': 2, 'e': 5} + ] + ''' + result = [] + + for a_object in a: + for b_object in b: + result.append(merge(a_object, b_object)) + + return result + +def make_provenance(project_root, cases, template): + return '\n'.join([ + 'This test was procedurally generated. Please do not modify it directly.', + 'Sources:', + '- {}'.format(os.path.relpath(cases, project_root)), + '- {}'.format(os.path.relpath(template, project_root)) + ]) + +def collection_filter(obj, title): + if not obj: + return 'no {}'.format(title) + + members = [] + for name, value in obj.items(): + if value == '': + members.append(name) + else: + members.append('{}={}'.format(name, value)) + + return '{}: {}'.format(title, ', '.join(members)) + +def pad_filter(value, side, padding): + if not value: + return '' + if side == 'start': + return padding + value + + return value + padding + +def main(config_file): + with open(config_file, 'r') as handle: + config = yaml.safe_load(handle.read()) + + templates_directory = os.path.normpath( + os.path.join(os.path.dirname(config_file), config['templates']) + ) + + environment = jinja2.Environment( + variable_start_string='[%', + variable_end_string='%]' + ) + environment.filters['collection'] = collection_filter + environment.filters['pad'] = pad_filter + templates = {} + subtests = {} + + for template_name, path in find_templates(templates_directory): + subtests[template_name] = [] + with open(path, 'r') as handle: + templates[template_name] = environment.from_string(handle.read()) + + for case in config['cases']: + unused_templates = set(templates) - set(case['template_axes']) + + # This warning is intended to help authors avoid mistakenly omitting + # templates. It can be silenced by extending the`template_axes` + # dictionary with an empty list for templates which are intentionally + # unused. + if unused_templates: + print( + 'Warning: case does not reference the following templates:' + ) + print('\n'.join('- {}'.format(name) for name in unused_templates)) + + common_axis = product( + case['common_axis'], [case.get('all_subtests', {})] + ) + + for template_name, template_axis in case['template_axes'].items(): + subtests[template_name].extend(product(common_axis, template_axis)) + + for template_name, template in templates.items(): + provenance = make_provenance( + PROJECT_ROOT, + config_file, + os.path.join(templates_directory, template_name) + ) + get_filename = lambda subtest: test_name( + config['output_directory'], + template_name, + subtest['filename_flags'] + ) + subtests_by_filename = itertools.groupby( + sorted(subtests[template_name], key=get_filename), + key=get_filename + ) + for filename, some_subtests in subtests_by_filename: + with open(filename, 'w') as handle: + handle.write(templates[template_name].render( + subtests=list(some_subtests), + provenance=provenance + ) + '\n') + +if __name__ == '__main__': + main('fetch-metadata.conf.yml') diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html b/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html new file mode 100644 index 0000000000..0dfc084f2e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for Appcache manifest</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url) { + const iframe = document.createElement('iframe'); + iframe.src = + '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url); + + return new Promise((resolve) => { + addEventListener('message', function onMessage(event) { + if (event.source !== iframe.contentWindow) { + return; + } + removeEventListener('message', onMessage); + resolve(event.data); + }); + + document.body.appendChild(iframe); + }) + .then((message) => { + if (message !== 'okay') { + throw message; + } + }) + .then(() => iframe.remove()); + } + + {%- for subtest in subtests %} + + async_test((t) => { + const key = '{{uuid()}}'; + assert_implements_optional( + !!window.applicationCache, 'Application Cache supported.' + ); + + induceRequest(makeRequestURL(key, [% subtest.origins %])) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }) + .then(() => t.done(), t.step_func((error) => { throw error; })); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html new file mode 100644 index 0000000000..7be309c506 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for AudioWorklet module</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + return test_driver.bless( + 'Enable WebAudio playback', + () => { + const audioContext = new AudioContext(); + + test.add_cleanup(() => audioContext.close()); + + return audioContext.audioWorklet.addModule(url); + } + ); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/javascript'}), + t + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/css-font-face.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/css-font-face.sub.html new file mode 100644 index 0000000000..94b33f4e6b --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/css-font-face.sub.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for CSS font-face</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + let count = 0; + + function induceRequest(t, url) { + const id = `el-${count += 1}`; + const style = document.createElement('style'); + style.appendChild(document.createTextNode(` + @font-face { + font-family: wpt-font-family${id}; + src: url(${url}); + } + #el-${id} { + font-family: wpt-font-family${id}; + } + `)); + const div = document.createElement('div'); + div.setAttribute('id', 'el-' + id); + div.appendChild(style); + div.appendChild(document.createTextNode('x')); + document.body.appendChild(div); + + t.add_cleanup(() => div.remove()); + + return document.fonts.ready; + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [% subtest.origins %])) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/css-images.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/css-images.sub.html new file mode 100644 index 0000000000..e394f9f5b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/css-images.sub.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for CSS image-accepting properties</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The subtests in this file use an iframe to induce requests for CSS + * resources because an iframe's `onload` event is the most direct and + * generic mechanism to detect loading of CSS resources. As an optimization, + * the subtests share the same iframe and document. + */ + const declarations = []; + const iframe = document.createElement('iframe'); + const whenIframeReady = new Promise((resolve, reject) => { + iframe.onload = resolve; + iframe.onerror = reject; + }); + + {%- for subtest in subtests %} + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %]); + + declarations.push(`background-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_equals(headers['[%subtest.headerName%]'], '[%subtest.expected%]'); + {%- endif %} + }) + .then(t.step_func_done(), (error) => t.unreached_func()); + }, 'background-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %]); + + declarations.push(`border-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'border-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %]); + + declarations.push(`content: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'content [%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %]); + + declarations.push(`cursor: url("${url}"), auto;`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'cursor [%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + async_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %]); + + declarations.push(`list-style-image: url("${url}");`); + + whenIframeReady + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }) + .then(t.step_func_done(), t.unreached_func()); + }, 'list-style-image [%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + + iframe.srcdoc = declarations.map((declaration, index) => ` + <style>.el${index} { ${declaration} }</style><div class="el${index}"></div>` + ).join(''); + document.body.appendChild(iframe); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-a.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-a.sub.html new file mode 100644 index 0000000000..2bd8e8a40e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-a.sub.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for HTML "a" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const anchor = win.document.createElement('a'); + anchor.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + anchor.setAttribute(name, value); + } + + win.document.body.appendChild(anchor); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => anchor.click()); + } else { + anchor.click(); + } + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}), + { + test: t, + userActivated: [%subtest.userActivated | default(false) | tojson%], + attributes: [%subtest.elementAttrs | default({}) | tojson%] + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] - [%subtest.elementAttrs | collection("attributes")%][% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-area.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-area.sub.html new file mode 100644 index 0000000000..0cef5b2294 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-area.sub.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for HTML "area" element navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, {test, userActivated, attributes}) { + const win = window.open(); + const area = win.document.createElement('area'); + area.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + area.setAttribute(name, value); + } + + win.document.body.appendChild(area); + + test.add_cleanup(() => win.close()); + + if (userActivated) { + test_driver.bless('enable user activation', () => area.click()); + } else { + area.click(); + } + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}), + { + test: t, + userActivated: [%subtest.userActivated | default(false) | tojson%], + attributes: [%subtest.elementAttrs | default({}) | tojson%] + } + ); + + // `induceRequest` does not necessarily trigger a navigation, so the Python + // handler must be polled until it has received the initial request. + return retrieve(key, {poll: true}) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] - [%subtest.elementAttrs | collection("attributes")%][% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-audio.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-audio.sub.html new file mode 100644 index 0000000000..92bc22198e --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-audio.sub.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "audio" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const audio = document.createElement('audio'); + + for (const [ name, value ] of Object.entries(attributes)) { + audio.setAttribute(name, value); + } + + return new Promise((resolve) => { + audio.setAttribute('src', url); + audio.onload = audio.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-embed.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-embed.sub.html new file mode 100644 index 0000000000..18ce09e5fd --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-embed.sub.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "embed" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + const embed = document.createElement('embed'); + embed.setAttribute('src', url); + document.body.appendChild(embed); + + t.add_cleanup(() => embed.remove()); + + return new Promise((resolve) => embed.addEventListener('load', resolve)); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [% subtest.origins %], params)) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-frame.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-frame.sub.html new file mode 100644 index 0000000000..ce90171779 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-frame.sub.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const frame = document.createElement('frame'); + + const setSrc = () => frame.setAttribute('src', url); + + document.body.appendChild(frame); + test.add_cleanup(() => frame.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + frame.onload = frame.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}), + t, + [%subtest.userActivated | default(false) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%][% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-iframe.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-iframe.sub.html new file mode 100644 index 0000000000..43a632a15c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-iframe.sub.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "frame" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test, userActivated) { + const iframe = document.createElement('iframe'); + + const setSrc = () => iframe.setAttribute('src', url); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + if (userActivated) { + test_driver.bless('enable user activation', setSrc); + } else { + setSrc(); + } + + iframe.onload = iframe.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}), + t, + [%subtest.userActivated | default(false) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%][% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html new file mode 100644 index 0000000000..5a65114f18 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on image request triggered by change to environment</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + // The response to the request under test must describe a valid image + // resource in order for the `load` event to be fired. + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const iframe = document.createElement('iframe'); + iframe.style.width = '50px'; + document.body.appendChild(iframe); + t.add_cleanup(() => iframe.remove()); + iframe.contentDocument.open(); + iframe.contentDocument.close(); + + const image = iframe.contentDocument.createElement('img'); + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + iframe.contentDocument.body.appendChild(image); + + image.setAttribute('srcset', `${url} 100w, /media/1x1-green.png 1w`); + image.setAttribute('sizes', '(max-width: 100px) 1px, (min-width: 150px) 123px'); + + return new Promise((resolve) => { + image.onload = image.onerror = resolve; + }) + .then(() => { + + iframe.style.width = '200px'; + + return new Promise((resolve) => image.onload = resolve); + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [% subtest.origins %], params), + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-img.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-img.sub.html new file mode 100644 index 0000000000..1dac5843ec --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-img.sub.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "img" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceAttr, attributes) { + const image = document.createElement('img'); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + return new Promise((resolve) => { + image.setAttribute(sourceAttr, url); + image.onload = image.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + '[%subtest.sourceAttr%]', + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.sourceAttr%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-input-image.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-input-image.sub.html new file mode 100644 index 0000000000..3c50008433 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-input-image.sub.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "input" element with type="button"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const input = document.createElement('input'); + input.setAttribute('type', 'image'); + + document.body.appendChild(input); + test.add_cleanup(() => input.remove()); + + return new Promise((resolve) => { + input.onload = input.onerror = resolve; + input.setAttribute('src', url); + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(makeRequestURL(key, [% subtest.origins %]), t) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-icon.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-icon.sub.html new file mode 100644 index 0000000000..18ce12a689 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-icon.sub.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for HTML "link" element with rel="icon"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'icon'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [% subtest.origins %], params), + [%subtest.elementAttrs | default({}) | tojson%] + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html new file mode 100644 index 0000000000..59d677d8d6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for HTML "link" element with rel="prefetch"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + /** + * The `link` element supports a `load` event. That event would reliably + * indicate that the browser had received the request. Multiple major + * browsers do not implement the event, however, so in order to promote the + * visibility of this test, a less efficient polling-based detection + * mechanism is used. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1638188 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1083034 + */ + function induceRequest(t, url, attributes) { + const link = document.createElement('link'); + link.setAttribute('rel', 'prefetch'); + link.setAttribute('href', url); + + for (const [ name, value ] of Object.entries(attributes)) { + link.setAttribute(name, value); + } + + document.head.appendChild(link); + t.add_cleanup(() => link.remove()); + } + + setup(() => { + assert_implements_optional(document.createElement('link').relList.supports('prefetch')); + }); + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + induceRequest( + t, + makeRequestURL(key, [% subtest.origins %]), + [%subtest.elementAttrs | default({}) | tojson%] + ); + + return retrieve(key, {poll:true}) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html new file mode 100644 index 0000000000..5a8d8f8ecd --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "meta" element with http-equiv="refresh"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.document.open(); + win.document.write( + `<meta http-equiv="Refresh" content="0; URL=${url}">` + ); + win.document.close(); + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-picture.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-picture.sub.html new file mode 100644 index 0000000000..903aeed1f3 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-picture.sub.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "picture" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, sourceEl, sourceAttr, attributes) { + const picture = document.createElement('picture'); + const els = { + img: document.createElement('img'), + source: document.createElement('source') + }; + picture.appendChild(els.source); + picture.appendChild(els.img); + document.body.appendChild(picture); + + for (const [ name, value ] of Object.entries(attributes)) { + els.img.setAttribute(name, value); + } + + return new Promise((resolve) => { + els[sourceEl].setAttribute(sourceAttr, url); + els.img.onload = els.img.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + 'img', + 'src', + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - img[src] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + 'img', + 'srcset', + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - img[srcset] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + 'source', + 'srcset', + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - source[srcset] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> + diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-script.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-script.sub.html new file mode 100644 index 0000000000..4a281ae519 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-script.sub.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "script" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const script = document.createElement('script'); + script.setAttribute('src', url); + + for (const [ name, value ] of Object.entries(attributes)) { + script.setAttribute(name, value); + } + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ); + + return induceRequest(url, + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%-subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-video-poster.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-video-poster.sub.html new file mode 100644 index 0000000000..9cdaf063ac --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-video-poster.sub.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element "poster"</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url) { + var video = document.createElement('video'); + video.setAttribute('poster', url); + document.body.appendChild(video); + + const poll = () => { + if (video.clientWidth === 123) { + return; + } + + return new Promise((resolve) => t.step_timeout(resolve, 0)) + .then(poll); + }; + t.add_cleanup(() => video.remove()); + + return poll(); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest(t, makeRequestURL(key, [% subtest.origins %], params)) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/element-video.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/element-video.sub.html new file mode 100644 index 0000000000..1b7b976d7c --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/element-video.sub.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTML "video" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, attributes) { + const video = document.createElement('video'); + + for (const [ name, value ] of Object.entries(attributes)) { + video.setAttribute(name, value); + } + + return new Promise((resolve) => { + video.setAttribute('src', url); + video.onload = video.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html new file mode 100644 index 0000000000..eead710200 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request using the "fetch" API and passing through a Serive Worker</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const scripts = { + fallback: '/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js', + respondWith: '/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js' + }; + + function induceRequest(t, url, init, script) { + const SCOPE = '/fetch/metadata/resources/fetch-via-serviceworker-frame.html'; + const SCRIPT = scripts[script]; + + return service_worker_unregister_and_register(t, SCRIPT, SCOPE) + .then((registration) => { + t.add_cleanup(() => registration.unregister()); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(() => with_iframe(SCOPE)) + .then((frame) => { + t.add_cleanup(() => frame.remove()); + + return frame.contentWindow.fetch(url, init); + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [% subtest.origins %]), + [%subtest.init | default({}) | tojson%], + 'respondWith' + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%] - respondWith'); + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [% subtest.origins %]), + [%subtest.init | default({}) | tojson%], + 'fallback' + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%] - fallback'); + + {%- endfor %} + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/fetch.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/fetch.sub.html new file mode 100644 index 0000000000..a8dc5368f8 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/fetch.sub.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request using the "fetch" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, init) { + return fetch(url, init); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %]), + [%subtest.init | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.init | collection("init")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/form-submission.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/form-submission.sub.html new file mode 100644 index 0000000000..4c9c8c50f8 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/form-submission.sub.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <title>HTTP headers on request for HTML form navigation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(method, url, userActivated) { + const windowName = String(Math.random()); + const form = document.createElement('form'); + const submit = document.createElement('input'); + submit.setAttribute('type', 'submit'); + form.appendChild(submit); + const win = open('about:blank', windowName); + form.setAttribute('method', method); + form.setAttribute('action', url); + form.setAttribute('target', windowName); + document.body.appendChild(form); + + // Query parameters must be expressed as form values so that they are sent + // with the submission of forms whose method is POST. + Array.from(new URL(url, location.origin).searchParams) + .forEach(([name, value]) => { + const input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', name); + input.setAttribute('value', value); + form.appendChild(input); + }); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.click(submit); + } else { + submit.click(); + } + }) + .then(() => { + form.remove(); + win.close(); + }); + } + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + const userActivated = [% 'true' if subtest.userActivated else 'false' %]; + return induceRequest('[%subtest.method | default("POST")%]', url, userActivated) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%][%subtest.method | default("POST")%][%" with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/header-link.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/header-link.sub.html new file mode 100644 index 0000000000..2831f221d5 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/header-link.sub.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for HTTP "Link" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, rel, test) { + const iframe = document.createElement('iframe'); + + iframe.setAttribute( + 'src', + '/fetch/metadata/resources/header-link.py' + + `?location=${encodeURIComponent(url)}&rel=${rel}` + ); + + document.body.appendChild(iframe); + test.add_cleanup(() => iframe.remove()); + + return new Promise((resolve) => { + iframe.onload = iframe.onerror = resolve; + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], {mime: 'text/html'}), + '[%subtest.rel%]', + t + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] rel=[%subtest.rel%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html new file mode 100644 index 0000000000..ec963d5cc0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for HTTP "Refresh" header</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, test) { + const win = window.open(); + test.add_cleanup(() => win.close()); + + win.location = `/common/refresh.py?location=${encodeURIComponent(url)}` + + return new Promise((resolve) => { + addEventListener('message', (event) => { + if (event.source === win) { + resolve(); + } + }); + }); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage(0, '*')</${''}script>` + }; + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL(key, [% subtest.origins %], responseParams), t + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html new file mode 100644 index 0000000000..653d3cdec4 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dynamic ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ); + + return import(url) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html new file mode 100644 index 0000000000..c8d5f9532a --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for static ECMAScript module import</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url) { + const script = document.createElement('script'); + script.setAttribute('type', 'module'); + script.setAttribute( + 'src', + '/fetch/metadata/resources/es-module.sub.js?moduleId=' + encodeURIComponent(url) + ); + + return new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => reject('Failed to load script'); + document.body.appendChild(script); + }) + .then(() => script.remove()); + } + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + + return induceRequest( + makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ) + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html new file mode 100644 index 0000000000..8284325546 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<!DOCTYPE html> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for Service Workers</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(t, url, options, event, clear) { + // Register a service worker and check the request header. + return navigator.serviceWorker.register(url, options) + .then((registration) => { + t.add_cleanup(() => registration.unregister()); + if (event === 'register') { + return; + } + return clear().then(() => registration.update()); + }); + } + + {%- for subtest in subtests %} + {%- set origin = subtest.origins[0]|default('httpsOrigin') %} + {%- if origin == 'httpsOrigin' or not origin %} + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, [%subtest.options | default({}) | tojson%], 'register') + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options | collection("options")%] - registration'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ); + + return induceRequest(t, url, [%subtest.options | default({}) | tojson%], 'update', () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options | collection("options")%] - updating'); + + {%- endif %} + {%- endfor %} + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/svg-image.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/svg-image.sub.html new file mode 100644 index 0000000000..52f7806b33 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/svg-image.sub.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for SVG "image" element source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const params = { + body: ` + <svg xmlns="http://www.w3.org/2000/svg" width="123" height="123"> + <rect fill="lime" width="123" height="123"/> + </svg> + `, + mime: 'image/svg+xml' + }; + + function induceRequest(t, url, attributes) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + "xmlns:xlink", + "http://www.w3.org/1999/xlink" + ); + const image = document.createElementNS("http://www.w3.org/2000/svg", "image"); + image.setAttribute("href", url); + svg.appendChild(image); + + for (const [ name, value ] of Object.entries(attributes)) { + image.setAttribute(name, value); + } + + document.body.appendChild(svg); + t.add_cleanup(() => svg.remove()); + + return new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }); + } + + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + + return induceRequest( + t, + makeRequestURL(key, [% subtest.origins %], params), + [%subtest.elementAttrs | default({}) | tojson%] + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%] [%subtest.elementAttrs | collection("attributes")%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/window-history.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/window-history.sub.html new file mode 100644 index 0000000000..286d019887 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/window-history.sub.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for navigation via the HTML History API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + const whenDone = (win) => { + return new Promise((resolve) => { + addEventListener('message', function handle(event) { + if (event.source === win) { + resolve(); + removeEventListener('message', handle); + } + }); + }) + }; + + /** + * Prime the UA's session history such that the location of the request is + * immediately behind the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.back`. + */ + function induceBackRequest(url, test, clear) { + const win = window.open(url); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(clear) + .then(() => win.history.back()) + .then(() => whenDone(win)); + } + + /** + * Prime the UA's session history such that the location of the request is + * immediately ahead of the current entry. Because the location may not be + * same-origin with the current browsing context, this must be done via a + * true navigation and not, e.g. the `history.pushState` API. The initial + * navigation will alter the WPT server's internal state; in order to avoid + * false positives, clear that state prior to initiating the second + * navigation via `history.forward`. + */ + function induceForwardRequest(url, test, clear) { + const win = window.open(messageOpenerUrl); + + test.add_cleanup(() => win.close()); + + return whenDone(win) + .then(() => win.location = url) + .then(() => whenDone(win)) + .then(clear) + .then(() => win.history.go(-2)) + .then(() => whenDone(win)) + .then(() => win.history.forward()) + .then(() => whenDone(win)); + } + + const messageOpenerUrl = new URL( + '/fetch/metadata/resources/message-opener.html', location + ); + // For these tests to function, replacement must *not* be enabled during + // navigation. Assignment must therefore take place after the document has + // completely loaded [1]. This event is not directly observable, but it is + // scheduled as a task immediately following the global object's `load` + // event [2]. By queuing a task during the dispatch of the `load` event, + // navigation can be consistently triggered without replacement. + // + // [1] https://html.spec.whatwg.org/multipage/history.html#location-object-setter-navigate + // [2] https://html.spec.whatwg.org/multipage/parsing.html#the-end + const responseParams = { + mime: 'text/html', + body: `<script> + window.addEventListener('load', () => { + set`+`Timeout(() => location.assign('${messageOpenerUrl}')); + }); + <`+`/script>` + }; + {%- for subtest in subtests %} + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + return induceBackRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("right", " - ")%]history.back[%subtest.api%][% " with user activation" if subtest.userActivated%]'); + + promise_test((t) => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + return induceForwardRequest(url, t, () => retrieve(key)) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("right", " - ")%]history.forward[%subtest.api%][% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/window-location.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/window-location.sub.html new file mode 100644 index 0000000000..96f3912361 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/window-location.sub.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + {%- if subtests|length > 10 %} + <meta name="timeout" content="long"> + {%- endif %} + <title>HTTP headers on request for navigation via the HTML Location API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + {%- if subtests|selectattr('userActivated')|list %} + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + {%- endif %} + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <body> + <script> + 'use strict'; + + function induceRequest(url, navigate, userActivated) { + const win = window.open(); + + return new Promise((resolve) => { + addEventListener('message', function(event) { + if (event.source === win) { + resolve(); + } + }); + + if (userActivated) { + test_driver.bless('enable user activation', () => { + navigate(win, url); + }); + } else { + navigate(win, url); + } + }) + .then(() => win.close()); + } + + const responseParams = { + mime: 'text/html', + body: `<script>opener.postMessage('done', '*')</${''}script>` + }; + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + const navigate = (win, path) => { + win.location = path; + }; + return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %]) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location[% " with user activation" if subtest.userActivated%]'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + const navigate = (win, path) => { + win.location.href = path; + }; + return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %]) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.href[% " with user activation" if subtest.userActivated%]'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + const navigate = (win, path) => { + win.location.assign(path); + }; + return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %]) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.assign[% " with user activation" if subtest.userActivated%]'); + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL(key, [% subtest.origins %], responseParams); + + const navigate = (win, path) => { + win.location.replace(path); + }; + return induceRequest(url, navigate, [% 'true' if subtest.userActivated else 'false' %]) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", " - ")%]location.replace[% " with user activation" if subtest.userActivated%]'); + + {%- endfor %} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html new file mode 100644 index 0000000000..fede5965d3 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "Worker" constructor</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + return new Promise((resolve, reject) => { + const worker = new Worker(url, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, + [% subtest.origins %], + { mime: 'application/javascript', body: 'postMessage("")' } + ); + + return induceRequest(url + {%- if subtest.options -%} + , [% subtest.options | tojson %] + {%- endif -%} + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%] - [%subtest.description | pad("end", ", ")%][%subtest.options|collection("options")%]'); + + {%- endfor %} + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html b/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html new file mode 100644 index 0000000000..93e6374d54 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- +[%provenance%] +--> +<html lang="en"> + <meta charset="utf-8"> + <title>HTTP headers on request for dedicated worker via the "importScripts" API</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/fetch/metadata/resources/helper.sub.js"></script> + <script type="module"> + 'use strict'; + function induceRequest(url, options) { + const src = ` + importScripts('${url}'); + postMessage('done'); + `; + const workerUrl = URL.createObjectURL( + new Blob([src], { type: 'application/javascript' }) + ); + return new Promise((resolve, reject) => { + const worker = new Worker(workerUrl, options); + worker.onmessage = resolve; + worker.onerror = reject; + }); + } + + {%- for subtest in subtests %} + + promise_test(() => { + const key = '{{uuid()}}'; + const url = makeRequestURL( + key, [% subtest.origins %], { mime: 'application/javascript' } + ); + + return induceRequest(url + {%- if subtest.options -%} + , [% subtest.options | tojson %] + {%- endif -%} + ) + .then(() => retrieve(key)) + .then((headers) => { + {%- if subtest.expected == none %} + assert_not_own_property(headers, '[%subtest.headerName%]'); + {%- else %} + assert_own_property(headers, '[%subtest.headerName%]'); + assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); + {%- endif %} + }); + }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); + + {%- endfor %} + </script> +</html> diff --git a/testing/web-platform/tests/fetch/metadata/track.https.sub.html b/testing/web-platform/tests/fetch/metadata/track.https.sub.html new file mode 100644 index 0000000000..346798fdc0 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/track.https.sub.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> + +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body> +</body> +<script> + let nonce = token(); + + function createVideoElement() { + let el = document.createElement('video'); + el.src = "/media/movie_5.mp4"; + el.setAttribute("controls", ""); + el.setAttribute("crossorigin", ""); + return el; + } + + function createTrack() { + let el = document.createElement("track"); + el.setAttribute("default", ""); + el.setAttribute("kind", "captions"); + el.setAttribute("srclang", "en"); + return el; + } + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "track-same-origin" + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + el.onload = t.step_func(_ => { + expected = { + "site": "same-origin", + "user": "", + "mode": "cors", // Because the `video` element has `crossorigin` + "dest": "track" + }; + validate_expectations(key, expected, "Same-Origin track") + .then(_ => resolve()); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, "Same-Origin track"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "track-same-site" + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = "https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + el.onload = t.step_func(_ => { + expected = { + "site": "same-site", + "user": "", + "mode": "cors", // Because the `video` element has `crossorigin` + "dest": "track" + }; + validate_expectations(key, expected, "Same-Site track") + .then(resolve) + .catch(reject); + + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, "Same-Site track"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "track-cross-site" + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = "https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + el.onload = t.step_func(_ => { + expected = { + "site": "cross-site", + "user": "", + "mode": "cors", // Because the `video` element has `crossorigin` + "dest": "track" + }; + validate_expectations(key, expected,"Cross-Site track") + .then(resolve) + .catch(reject); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, "Cross-Site track"); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "track-same-origin-cors" + nonce; + let video = createVideoElement(); + + // Unset `crossorigin` to change the CORS mode: + video.crossOrigin = undefined; + + let el = createTrack(); + el.src = "https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=" + key; + el.onload = t.step_func(_ => { + expected = { + "site":"same-origin", + "user":"", + "mode": "same-origin", + "dest": "track" + }; + validate_expectations(key, expected, "Same-Origin, CORS track") + .then(_ => resolve()); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, "Same-Origin, CORS track"); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/trailing-dot.https.sub.any.js b/testing/web-platform/tests/fetch/metadata/trailing-dot.https.sub.any.js new file mode 100644 index 0000000000..5e32fc4e7f --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/trailing-dot.https.sub.any.js @@ -0,0 +1,30 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same origin, but spelled with a trailing dot."); +}, "Fetching a resource from the same origin, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same site, but spelled with a trailing dot."); +}, "Fetching a resource from the same site, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from a cross-site host, spelled with a trailing dot."); +}, "Fetching a resource from a cross-site host, spelled with a trailing dot."); diff --git a/testing/web-platform/tests/fetch/metadata/unload.https.sub.html b/testing/web-platform/tests/fetch/metadata/unload.https.sub.html new file mode 100644 index 0000000000..bc26048c81 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/unload.https.sub.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-vendor.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<body> +<script> + // The test + // 1. Creates a same-origin iframe + // 2. Adds to the iframe an unload handler that will + // trigger a request to <unload_request_url>/.../record-header.py... + // 3. Navigate the iframe to a cross-origin url (to data: url) + // 4. Waits until the request goes through + // 5. Verifies Sec-Fetch-Site request header of the request. + // + // This is a regression test for https://crbug.com/986577. + function create_test(unload_request_origin, expectations) { + async_test(t => { + // STEP 1: Create an iframe. + let nonce = token(); + let key = "unload-test-" + nonce; + let url = unload_request_origin + + "/fetch/metadata/resources/record-header.py?file=" + key; + let i = document.createElement('iframe'); + i.src = 'resources/unload-with-beacon.html'; + i.onload = () => { + // STEP 2: Ask the iframe to add an unload handler. + i.contentWindow.postMessage(url, '*'); + }; + window.addEventListener('message', e => { + // STEP 3: Navigate the iframe away + i.contentWindow.location = 'data:text/html,DONE'; + }); + document.body.appendChild(i); + + // STEPS 4 and 5: Wait for the beacon to go through and verify + // the request headers. + function wait_and_verify() { + t.step_timeout(() => { + fetch("resources/record-header.py?retrieve=true&file=" + key) + .then(response => response.text()) + .then(text => t.step(() => { + if (text == 'No header has been recorded') { + wait_and_verify(); + return; + } + assert_header_equals(text, expectations); + t.done(); + })) + }, 200); + } + wait_and_verify(); + }, "Fetch from an unload handler"); + } + + create_test("https://{{host}}:{{ports[https][0]}}", { + "site": "same-origin", + "user": "", + "mode": "no-cors", + "dest": "empty" + }); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/window-open.https.sub.html b/testing/web-platform/tests/fetch/metadata/window-open.https.sub.html new file mode 100644 index 0000000000..94ba76a19f --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/window-open.https.sub.html @@ -0,0 +1,199 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-vendor.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<body> +<script> + // Forced navigations: + async_test(t => { + let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-origin", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Same-origin window, forced"); + t.done(); + })); + }, "Same-origin window, forced"); + + async_test(t => { + let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-site", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Same-site window, forced"); + t.done(); + })); + }, "Same-site window, forced"); + + async_test(t => { + let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "cross-site", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Cross-site window, forced"); + t.done(); + })); + }, "Cross-site window, forced"); + + async_test(t => { + let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + let messages = 0; + window.addEventListener('message', t.step_func(e => { + messages++; + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-origin", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Same-origin window, forced, reloaded"); + + if (messages == 1) { + w.location.reload(); + } else { + t.done(); + } + })); + }, "Same-origin window, forced, reloaded"); + + async_test(t => { + let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + let messages = 0; + window.addEventListener('message', t.step_func(e => { + messages++; + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-site", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Same-site window, forced, reloaded"); + + if (messages == 1) { + w.location.reload(); + } else { + t.done(); + } + })); + }, "Same-site window, forced, reloaded"); + + async_test(t => { + let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + let messages = 0; + window.addEventListener('message', t.step_func(e => { + messages++; + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "cross-site", + "user": "", + "mode": "navigate", + "dest": "document" + }, "Cross-site window, forced, reloaded"); + + if (messages == 1) { + w.location.reload(); + } else { + t.done(); + } + })); + }, "Cross-site window, forced, reloaded"); + + // User-activated navigations: + async_test(t => { + let b = document.createElement('button'); + b.onclick = t.step_func(_ => { + let w = window.open("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-origin", + "user": "?1", + "mode": "navigate", + "dest": "document" + }, "Same-origin window, user-activated"); + t.done(); + })); + }); + document.body.appendChild(b); + test_driver.click(b); + }, "Same-origin window, user-activated"); + + async_test(t => { + let b = document.createElement('button'); + b.onclick = t.step_func(_ => { + let w = window.open("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "same-site", + "user": "?1", + "mode": "navigate", + "dest": "document" + }, "Same-site window, user-activated"); + t.done(); + })); + }); + document.body.appendChild(b); + test_driver.click(b); + }, "Same-site window, user-activated"); + + async_test(t => { + let b = document.createElement('button'); + b.onclick = t.step_func(_ => { + let w = window.open("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/post-to-owner.py"); + t.add_cleanup(_ => w.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != w) + return; + + assert_header_equals(e.data, { + "site": "cross-site", + "user": "?1", + "mode": "navigate", + "dest": "document" + }, "Cross-site window, user-activated"); + t.done(); + })); + }); + document.body.appendChild(b); + test_driver.click(b); + }, "Cross-site window, user-activated"); +</script> diff --git a/testing/web-platform/tests/fetch/metadata/worker.https.sub.html b/testing/web-platform/tests/fetch/metadata/worker.https.sub.html new file mode 100644 index 0000000000..20a4fe5416 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/worker.https.sub.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> + +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<script> + let nonce = token(); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = "worker-same-origin" + nonce; + let w = new Worker("/fetch/metadata/resources/record-header.py?file=" + key); + w.onmessage = e => { + let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "worker"}; + validate_expectations(key, expected) + .then(_ => resolve()) + .catch(e => reject(e)); + }; + }); + }, "Same-Origin worker"); +</script> +<body></body> diff --git a/testing/web-platform/tests/fetch/metadata/xslt.https.sub.html b/testing/web-platform/tests/fetch/metadata/xslt.https.sub.html new file mode 100644 index 0000000000..dc72d7b8a6 --- /dev/null +++ b/testing/web-platform/tests/fetch/metadata/xslt.https.sub.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> + +<link rel="author" href="mtrzos@google.com" title="Maciek Trzos"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/fetch/metadata/resources/helper.js></script> +<script src=/common/utils.js></script> +<script> + // Open a window with XML document which loads resources via <?xml-stylesheet/> tag + let nonce = token(); + let w = window.open("resources/xslt-test.sub.xml?token=" + nonce); + window.addEventListener('message', function(e) { + if (e.source != w) + return; + + // Only testing same-origin XSLT because same-site and cross-site XSLT is blocked. + promise_test(t => { + let expected = {"site":"same-origin", "user":"", "mode": "same-origin", "dest": "xslt"}; + return validate_expectations("xslt-same-origin" + nonce, expected); + }, "Same-Origin xslt"); + + w.close(); + }); + +</script> diff --git a/testing/web-platform/tests/fetch/nosniff/image.html b/testing/web-platform/tests/fetch/nosniff/image.html new file mode 100644 index 0000000000..9dfdb94cf6 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/image.html @@ -0,0 +1,39 @@ +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + // Note: images get always sniffed, nosniff doesn't do anything + // (but note the tentative Cross-Origin Read Blocking (CORB) tests + // - for example wpt/fetch/corb/img-mime-types-coverage.tentative.sub.html). + var passes = [ + // Empty or non-sensical MIME types + null, "", "x", "x/x", + + // Image MIME types + "image/gif", "image/png", "image/png;blah", "image/svg+xml", + + // CORB-protected MIME types (but note that CORB doesn't apply here, + // because CORB ignores same-origin requests). + "text/html", "application/xml", "application/blah+xml" + ] + + const get_url = (mime) => { + let url = "resources/image.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + return url + } + + passes.forEach(function(mime) { + async_test(function(t) { + var img = document.createElement("img") + img.onerror = t.unreached_func("Unexpected error event") + img.onload = t.step_func_done(function(){ + assert_equals(img.width, 96) + }) + img.src = get_url(mime) + document.body.appendChild(img) + }, "URL query: " + mime) + }) +</script> diff --git a/testing/web-platform/tests/fetch/nosniff/importscripts.html b/testing/web-platform/tests/fetch/nosniff/importscripts.html new file mode 100644 index 0000000000..920b6bdd40 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/importscripts.html @@ -0,0 +1,14 @@ +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + async_test(function(t) { + var w = new Worker("importscripts.js") + w.onmessage = t.step_func(function(e) { + if(e.data == "END") + t.done() + else + assert_equals(e.data, "PASS") + }) + }, "Test importScripts()") +</script> diff --git a/testing/web-platform/tests/fetch/nosniff/importscripts.js b/testing/web-platform/tests/fetch/nosniff/importscripts.js new file mode 100644 index 0000000000..18952805bb --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/importscripts.js @@ -0,0 +1,28 @@ +// Testing importScripts() +function log(w) { this.postMessage(w) } +function f() { log("FAIL") } +function p() { log("PASS") } + +const get_url = (mime, outcome) => { + let url = "resources/js.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + if (outcome) { + url += "&outcome=p" + } + return url +} + +[null, "", "x", "x/x", "text/html", "text/json"].forEach(function(mime) { + try { + importScripts(get_url(mime)) + } catch(e) { + (e.name == "NetworkError") ? p() : log("FAIL (no NetworkError exception): " + mime) + } + +}) +importScripts(get_url("text/javascript", true)) +importScripts(get_url("text/ecmascript", true)) +importScripts(get_url("text/ecmascript;blah", true)) +log("END") diff --git a/testing/web-platform/tests/fetch/nosniff/parsing-nosniff.window.js b/testing/web-platform/tests/fetch/nosniff/parsing-nosniff.window.js new file mode 100644 index 0000000000..2a2648653c --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/parsing-nosniff.window.js @@ -0,0 +1,27 @@ +promise_test(() => fetch("resources/x-content-type-options.json").then(res => res.json()).then(runTests), "Loading JSON…"); + +function runTests(allTestData) { + for (let i = 0; i < allTestData.length; i++) { + const testData = allTestData[i], + input = encodeURIComponent(testData.input); + promise_test(t => { + let resolve; + const promise = new Promise(r => resolve = r); + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + // A <script> element loading a classic script does not care about the MIME type, unless + // X-Content-Type-Options: nosniff is specified, in which case a JavaScript MIME type is + // enforced, which x/x is not. + if (testData.nosniff) { + script.onerror = resolve; + script.onload = t.unreached_func("Script should not have loaded"); + } else { + script.onerror = t.unreached_func("Script should have loaded"); + script.onload = resolve; + } + script.src = "resources/nosniff.py?nosniff=" + input; + document.body.appendChild(script); + return promise; + }, input); + } +} diff --git a/testing/web-platform/tests/fetch/nosniff/resources/css.py b/testing/web-platform/tests/fetch/nosniff/resources/css.py new file mode 100644 index 0000000000..8afb56991d --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/css.py @@ -0,0 +1,23 @@ +def main(request, response): + type = request.GET.first(b"type", None) + is_revalidation = request.headers.get(b"If-Modified-Since", None) + + content = b"/* nothing to see here */" + + response.add_required_headers = False + if is_revalidation is not None: + response.writer.write_status(304) + response.writer.write_header(b"x-content-type-options", b"nosniff") + response.writer.write_header(b"content-length", 0) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + response.writer.write(b"") + else: + response.writer.write_status(200) + response.writer.write_header(b"x-content-type-options", b"nosniff") + response.writer.write_header(b"content-length", len(content)) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + response.writer.write(content) diff --git a/testing/web-platform/tests/fetch/nosniff/resources/image.py b/testing/web-platform/tests/fetch/nosniff/resources/image.py new file mode 100644 index 0000000000..9fd367c85c --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/image.py @@ -0,0 +1,24 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + type = request.GET.first(b"type", None) + + if type != None and b"svg" in type: + filename = u"green-96x96.svg" + else: + filename = u"blue96x96.png" + + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"../../../images", filename) + body = open(path, u"rb").read() + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"x-content-type-options", b"nosniff") + response.writer.write_header(b"content-length", len(body)) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(body) diff --git a/testing/web-platform/tests/fetch/nosniff/resources/js.py b/testing/web-platform/tests/fetch/nosniff/resources/js.py new file mode 100644 index 0000000000..784050a2ca --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/js.py @@ -0,0 +1,17 @@ +def main(request, response): + outcome = request.GET.first(b"outcome", b"f") + type = request.GET.first(b"type", b"Content-Type missing") + + content = b"// nothing to see here" + content += b"\n" + content += b"log('FAIL: " + type + b"')" if (outcome == b"f") else b"p()" + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"x-content-type-options", b"nosniff") + response.writer.write_header(b"content-length", len(content)) + if(type != b"Content-Type missing"): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(content) diff --git a/testing/web-platform/tests/fetch/nosniff/resources/nosniff.py b/testing/web-platform/tests/fetch/nosniff/resources/nosniff.py new file mode 100644 index 0000000000..159ecfbebd --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/nosniff.py @@ -0,0 +1,11 @@ +def main(request, response): + response.add_required_headers = False + output = b"HTTP/1.1 220 YOU HAVE NO POWER HERE\r\n" + output += b"Content-Length: 22\r\n" + output += b"Connection: close\r\n" + output += b"Content-Type: x/x\r\n" + output += request.GET.first(b"nosniff") + b"\r\n" + output += b"\r\n" + output += b"// nothing to see here" + response.writer.write(output) + response.close_connection = True diff --git a/testing/web-platform/tests/fetch/nosniff/resources/worker.py b/testing/web-platform/tests/fetch/nosniff/resources/worker.py new file mode 100644 index 0000000000..2d7e3f6c90 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/worker.py @@ -0,0 +1,16 @@ +def main(request, response): + type = request.GET.first(b"type", None) + + content = b"// nothing to see here" + content += b"\n" + content += b"this.postMessage('hi')" + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"x-content-type-options", b"nosniff") + response.writer.write_header(b"content-length", len(content)) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(content) diff --git a/testing/web-platform/tests/fetch/nosniff/resources/x-content-type-options.json b/testing/web-platform/tests/fetch/nosniff/resources/x-content-type-options.json new file mode 100644 index 0000000000..080fc1990b --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/resources/x-content-type-options.json @@ -0,0 +1,62 @@ +[ + { + "input": "X-Content-Type-Options: NOSNIFF", + "nosniff": true + }, + { + "input": "x-content-type-OPTIONS: nosniff", + "nosniff": true + }, + { + "input": "X-Content-Type-Options: nosniff,,@#$#%%&^&^*()()11!", + "nosniff": true + }, + { + "input": "X-Content-Type-Options: @#$#%%&^&^*()()11!,nosniff", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: nosniff\r\nX-Content-Type-Options: no", + "nosniff": true + }, + { + "input": "X-Content-Type-Options: no\r\nX-Content-Type-Options: nosniff", + "nosniff": false + }, + { + "input": "X-Content-Type-Options:\r\nX-Content-Type-Options: nosniff", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: nosniff\r\nX-Content-Type-Options: nosniff", + "nosniff": true + }, + { + "input": "X-Content-Type-Options: ,nosniff", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: nosniff\u000C", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: nosniff\u000B", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: nosniff\u000B,nosniff", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: 'NosniFF'", + "nosniff": false + }, + { + "input": "X-Content-Type-Options: \"nosniFF\"", + "nosniff": false + }, + { + "input": "Content-Type-Options: nosniff", + "nosniff": false + } +] diff --git a/testing/web-platform/tests/fetch/nosniff/script.html b/testing/web-platform/tests/fetch/nosniff/script.html new file mode 100644 index 0000000000..e0b5dac709 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/script.html @@ -0,0 +1,43 @@ +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + var log = function() {}, // see comment below + p = function() {}, // see comment below + fails = [null, "", "x", "x/x", "text/html", "text/json"], + passes = ["text/javascript", "text/ecmascript", "text/ecmascript;blah", "text/javascript1.0"] + + // Ideally we'd also check whether the scripts in fact execute, but that would involve + // timers and might get a bit racy without cross-browser support for the execute events. + + const get_url = (mime, outcome) => { + let url = "resources/js.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + if (outcome) { + url += "&outcome=p" + } + return url + } + + fails.forEach(function(mime) { + async_test(function(t) { + var script = document.createElement("script") + script.onerror = t.step_func_done(function(){}) + script.onload = t.unreached_func("Unexpected load event") + script.src = get_url(mime) + document.body.appendChild(script) + }, "URL query: " + mime) + }) + + passes.forEach(function(mime) { + async_test(function(t) { + var script = document.createElement("script") + script.onerror = t.unreached_func("Unexpected error event") + script.onload = t.step_func_done(function(){}) + script.src = get_url(mime, true) + document.body.appendChild(script) + }, "URL query: " + mime) + }) +</script> diff --git a/testing/web-platform/tests/fetch/nosniff/stylesheet.html b/testing/web-platform/tests/fetch/nosniff/stylesheet.html new file mode 100644 index 0000000000..8f2b5476e9 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/stylesheet.html @@ -0,0 +1,60 @@ +<!-- quirks mode is important, text/css is already required otherwise --> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + var fails = [null, "", "x", "x/x", "text/html", "text/json"], + passes = ["text/css", "text/css;charset=utf-8", "text/css;blah"] + + const get_url = (mime) => { + let url = "resources/css.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + return url + } + + fails.forEach(function(mime) { + async_test(function(t) { + var link = document.createElement("link") + link.rel = "stylesheet" + link.onerror = t.step_func_done() + link.onload = t.unreached_func("Unexpected load event") + link.href = get_url(mime) + document.body.appendChild(link) + }, "URL query: " + mime) + }) + + fails.forEach(function(mime) { + async_test(function(t) { + var link = document.createElement("link") + link.rel = "stylesheet" + link.onerror = t.step_func_done() + link.onload = t.unreached_func("Unexpected load event") + link.href = get_url(mime) + document.body.appendChild(link) + }, "Revalidated URL query: " + mime) + }) + + passes.forEach(function(mime) { + async_test(function(t) { + var link = document.createElement("link") + link.rel = "stylesheet" + link.onerror = t.unreached_func("Unexpected error event") + link.onload = t.step_func_done() + link.href = get_url(mime) + document.body.appendChild(link) + }, "URL query: " + mime) + }) + + passes.forEach(function(mime) { + async_test(function(t) { + var link = document.createElement("link") + link.rel = "stylesheet" + link.onerror = t.unreached_func("Unexpected error event") + link.onload = t.step_func_done() + link.href = get_url(mime) + document.body.appendChild(link) + }, "Revalidated URL query: " + mime) + }) +</script> diff --git a/testing/web-platform/tests/fetch/nosniff/worker.html b/testing/web-platform/tests/fetch/nosniff/worker.html new file mode 100644 index 0000000000..c8c1076df5 --- /dev/null +++ b/testing/web-platform/tests/fetch/nosniff/worker.html @@ -0,0 +1,28 @@ +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + var workers = [], + fails = ["", "?type=", "?type=x", "?type=x/x", "?type=text/html", "?type=text/json"], + passes = ["?type=text/javascript", "?type=text/ecmascript", "?type=text/ecmascript;yay"] + + fails.forEach(function(urlpart) { + async_test(function(t) { + var w = new Worker("resources/worker.py" + urlpart) + w.onmessage = t.unreached_func("Unexpected message event") + w.onerror = t.step_func_done(function(){}) + workers.push(w) // avoid GC + }, "URL query: " + urlpart) + }) + + passes.forEach(function(urlpart) { + async_test(function(t) { + var w = new Worker("resources/worker.py" + urlpart) + w.onmessage = t.step_func_done(function(e){ + assert_equals(e.data, "hi") + }) + w.onerror = t.unreached_func("Unexpected error event") + workers.push(w) // avoid GC + }, "URL query: " + urlpart) + }) +</script> diff --git a/testing/web-platform/tests/fetch/orb/resources/data.json b/testing/web-platform/tests/fetch/orb/resources/data.json new file mode 100644 index 0000000000..f2a886f39d --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/data.json @@ -0,0 +1,3 @@ +{ + "hello": "world" +} diff --git a/testing/web-platform/tests/fetch/orb/resources/data_non_ascii.json b/testing/web-platform/tests/fetch/orb/resources/data_non_ascii.json new file mode 100644 index 0000000000..64566c50c1 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/data_non_ascii.json @@ -0,0 +1 @@ +["你好"] diff --git a/testing/web-platform/tests/fetch/orb/resources/empty.json b/testing/web-platform/tests/fetch/orb/resources/empty.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/empty.json @@ -0,0 +1 @@ +{} diff --git a/testing/web-platform/tests/fetch/orb/resources/font.ttf b/testing/web-platform/tests/fetch/orb/resources/font.ttf Binary files differnew file mode 100644 index 0000000000..9023592ef5 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/font.ttf diff --git a/testing/web-platform/tests/fetch/orb/resources/image.png b/testing/web-platform/tests/fetch/orb/resources/image.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/image.png diff --git a/testing/web-platform/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json b/testing/web-platform/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json Binary files differnew file mode 100644 index 0000000000..157a8f5430 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json diff --git a/testing/web-platform/tests/fetch/orb/resources/js-unlabeled.js b/testing/web-platform/tests/fetch/orb/resources/js-unlabeled.js new file mode 100644 index 0000000000..a880a5bc72 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/js-unlabeled.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png b/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png diff --git a/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers b/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers new file mode 100644 index 0000000000..156209f9c8 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/testing/web-platform/tests/fetch/orb/resources/png-unlabeled.png b/testing/web-platform/tests/fetch/orb/resources/png-unlabeled.png Binary files differnew file mode 100644 index 0000000000..820f8cace2 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/png-unlabeled.png diff --git a/testing/web-platform/tests/fetch/orb/resources/script-asm-js-invalid.js b/testing/web-platform/tests/fetch/orb/resources/script-asm-js-invalid.js new file mode 100644 index 0000000000..8d1bbd6abc --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script-asm-js-invalid.js @@ -0,0 +1,4 @@ +function f() { + "use asm"; + return; +} diff --git a/testing/web-platform/tests/fetch/orb/resources/script-asm-js-valid.js b/testing/web-platform/tests/fetch/orb/resources/script-asm-js-valid.js new file mode 100644 index 0000000000..79b375fe05 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script-asm-js-valid.js @@ -0,0 +1,4 @@ +function f() { + "use asm"; + return {}; +} diff --git a/testing/web-platform/tests/fetch/orb/resources/script-iso-8559-1.js b/testing/web-platform/tests/fetch/orb/resources/script-iso-8559-1.js new file mode 100644 index 0000000000..3bccb6af93 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script-iso-8559-1.js @@ -0,0 +1,4 @@ +"use strict"; +function fn() { + return "An"; +} diff --git a/testing/web-platform/tests/fetch/orb/resources/script-utf16-bom.js b/testing/web-platform/tests/fetch/orb/resources/script-utf16-bom.js Binary files differnew file mode 100644 index 0000000000..16b76e9d5e --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script-utf16-bom.js diff --git a/testing/web-platform/tests/fetch/orb/resources/script-utf16-without-bom.js b/testing/web-platform/tests/fetch/orb/resources/script-utf16-without-bom.js Binary files differnew file mode 100644 index 0000000000..d983086b03 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script-utf16-without-bom.js diff --git a/testing/web-platform/tests/fetch/orb/resources/script.js b/testing/web-platform/tests/fetch/orb/resources/script.js new file mode 100644 index 0000000000..19675d25d8 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/script.js @@ -0,0 +1,4 @@ +"use strict"; +function fn() { + return 42; +} diff --git a/testing/web-platform/tests/fetch/orb/resources/sound.mp3 b/testing/web-platform/tests/fetch/orb/resources/sound.mp3 Binary files differnew file mode 100644 index 0000000000..a15d1de328 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/sound.mp3 diff --git a/testing/web-platform/tests/fetch/orb/resources/text.txt b/testing/web-platform/tests/fetch/orb/resources/text.txt new file mode 100644 index 0000000000..270c611ee7 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/text.txt @@ -0,0 +1 @@ +hello, world! diff --git a/testing/web-platform/tests/fetch/orb/resources/utils.js b/testing/web-platform/tests/fetch/orb/resources/utils.js new file mode 100644 index 0000000000..45fbc4cb38 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/resources/utils.js @@ -0,0 +1,101 @@ +function header(name, value) { + return `header(${name},${value})`; +} + +function contentType(type) { + return header("Content-Type", type); +} + +function contentTypeOptions(type) { + return header("X-Content-Type-Options", type); +} + +function testFetchNoCors(_t, path, { headers }) { + return fetch(path, { + ...(headers ? { headers } : {}), + mode: "no-cors", + }); +} + +function testElementInitiator(t, path, name) { + let element = document.createElement(name); + element.src = path; + t.add_cleanup(() => element.remove()); + return new Promise((resolve, reject) => { + element.onerror = e => reject(new TypeError()); + element.onload = resolve; + + document.body.appendChild(element); + }); +} + +function testImageInitiator(t, path) { + return testElementInitiator(t, path, "img"); +} + +function testAudioInitiator(t, path) { + return testElementInitiator(t, path, "audio"); +} + +function testVideoInitiator(t, path) { + return testElementInitiator(t, path, "video"); +} + +function testScriptInitiator(t, path) { + return testElementInitiator(t, path, "script"); +} + +function runTest(t, test, file, options, ...pipe) { + const path = `${file}${pipe.length ? `?pipe=${pipe.join("|")}` : ""}`; + return test(t, path, options) +} + +function testRunAll(file, testCallback, adapter, options) { + let testcase = function (test, message, skip) { + return {test, message, skip}; + }; + + const name = "..."; + [ testcase(testFetchNoCors, `fetch(${name}, {mode: "no-cors"})`, false || options.skip.includes("fetch")), + testcase(testImageInitiator, `<img src=${name}>`, options.onlyFetch || options.skip.includes("image")), + testcase(testAudioInitiator, `<audio src=${name}>`, options.onlyFetch || options.skip.includes("audio")), + testcase(testVideoInitiator, `<video src=${name}>`, options.onlyFetch || options.skip.includes("video")), + testcase(testScriptInitiator, `<script src=${name}>`, options.onlyFetch || options.skip.includes("script")), + ].filter(({skip}) => !skip) + .forEach(({test, message}) => { + testCallback((t, ...args) => adapter(t, runTest(t, test, file, options, ...args), message), header => `${header}: ${message}`); + }); +} + +function expected_block(file, testCallback, options = {}) { + let defaultOptions = { + onlyFetch: !self.GLOBAL.isWindow(), + skip: [] + }; + testRunAll(file, testCallback, (t, promise, message) => promise_rejects_js(t, TypeError, promise, message), { ...defaultOptions, ...options }); +} + +function expected_allow(file, testCallback, options = {}) { + let defaultOptions = { + onlyFetch: !self.GLOBAL.isWindow(), + skip: [], + headers: null + }; + testRunAll(file, testCallback, (_t, promise, _message) => promise, { ...defaultOptions, ...options }); +} + +function expected_allow_fetch(file, testCallback, options = {}) { + let defaultOptions = { + skip: [], + headers: null, + }; + testRunAll(file, testCallback, (_t, promise, _message) => promise, { ...defaultOptions, ...options, onlyFetch: true }); +} + +function expected_block_fetch(file, testCallback, options = {}) { + let defaultOptions = { + skip: [], + headers: null, + }; + testRunAll(file, testCallback, (t, promise, message) => promise_rejects_js(t, TypeError, promise, message), { ...defaultOptions, ...options, onlyFetch: true }); +} diff --git a/testing/web-platform/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html b/testing/web-platform/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html new file mode 100644 index 0000000000..38e70c69ad --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html @@ -0,0 +1,20 @@ +<!-- Test verifies that compressed images should not be blocked +--> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +async_test(function(t) { + let url = "http://{{domains[www1]}}:{{ports[http][0]}}" + url = url + "/fetch/orb/resources/png-unlabeled.png?pipe=gzip" + + const img = document.createElement("img"); + img.src = url; + img.onerror = t.unreached_func("Unexpected error event") + img.onload = t.step_func_done(function () { + assert_true(true); + }) + document.body.appendChild(img) +}, "ORB shouldn't block compressed images"); +</script> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/content-range.sub.any.js b/testing/web-platform/tests/fetch/orb/tentative/content-range.sub.any.js new file mode 100644 index 0000000000..c965aea4f8 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/content-range.sub.any.js @@ -0,0 +1,20 @@ +// META: script=/fetch/orb/resources/utils.js + +const url = + "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/image.png"; + +expected_allow_fetch( + url, + (orb_test, message) => + promise_test( + t => orb_test(t, header("Content-Range", "bytes 0-99/1010"), "slice(null,100)", "status(206)"), + message("ORB shouldn't block opaque range of image/png starting at zero")), + { headers: new Headers([["Range", "bytes=0-99"]]) }); + +expected_block_fetch( + url, + (orb_test, message) => + promise_test( + t => orb_test(t, header("Content-Range", "bytes 10-99/1010"), "slice(10,100)", "status(206)"), + message("ORB should block opaque range of image/png not starting at zero, that isn't subsequent")), + { headers: new Headers([["Range", "bytes 10-99"]]) }); diff --git a/testing/web-platform/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html b/testing/web-platform/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html new file mode 100644 index 0000000000..5dc6c5d63a --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html @@ -0,0 +1,126 @@ +<!-- Test verifies that cross-origin, nosniff images are 1) blocked when their + MIME type is covered by ORB and 2) allowed otherwise. + + This test is very similar to fetch/orb/img-mime-types-coverage.tentative.sub.html, + except that it focuses on MIME types relevant to ORB. +--> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> + var passes = [ + // ORB safelisted MIME-types - i.e. ones covered by: + // - https://github.com/annevk/orb + + "text/css", + "image/svg+xml", + + // JavaScript MIME types + "application/ecmascript", + "application/javascript", + "application/x-ecmascript", + "application/x-javascript", + "text/ecmascript", + "text/javascript", + "text/javascript1.0", + "text/javascript1.1", + "text/javascript1.2", + "text/javascript1.3", + "text/javascript1.4", + "text/javascript1.5", + "text/jscript", + "text/livescript", + "text/x-ecmascript", + "text/x-javascript", + ] + + var fails = [ + // ORB blocklisted MIME-types - i.e. ones covered by: + // - https://github.com/annevk/orb + + "text/html", + + // JSON MIME type + "application/json", + "text/json", + "application/ld+json", + + // XML MIME type + "text/xml", + "application/xml", + "application/xhtml+xml", + + "application/dash+xml", + "application/gzip", + "application/msexcel", + "application/mspowerpoint", + "application/msword", + "application/msword-template", + "application/pdf", + "application/vnd.apple.mpegurl", + "application/vnd.ces-quickpoint", + "application/vnd.ces-quicksheet", + "application/vnd.ces-quickword", + "application/vnd.ms-excel", + "application/vnd.ms-excel.sheet.macroenabled.12", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "application/vnd.ms-word", + "application/vnd.ms-word.document.12", + "application/vnd.ms-word.document.macroenabled.12", + "application/vnd.msword", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.presentationml.template", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "application/vnd.presentation-openxml", + "application/vnd.presentation-openxmlm", + "application/vnd.spreadsheet-openxml", + "application/vnd.wordprocessing-openxml", + "application/x-gzip", + "application/x-protobuf", + "application/x-protobuffer", + "application/zip", + "audio/mpegurl", + "multipart/byteranges", + "multipart/signed", + "text/event-stream", + "text/csv", + "text/vtt", +] + + const get_url = (mime) => { + // www1 is cross-origin, so the HTTP response is ORB-eligible --> + url = "http://{{domains[www1]}}:{{ports[http][0]}}" + url = url + "/fetch/nosniff/resources/image.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + return url + } + + passes.forEach(function (mime) { + async_test(function (t) { + var img = document.createElement("img") + img.onerror = t.unreached_func("Unexpected error event") + img.onload = t.step_func_done(function () { + assert_equals(img.width, 96) + }) + img.src = get_url(mime) + document.body.appendChild(img) + }, "ORB should allow the response if Content-Type is: '" + mime + "'. ") + }) + + fails.forEach(function (mime) { + async_test(function (t) { + var img = document.createElement("img") + img.onerror = t.step_func_done() + img.onload = t.unreached_func("Unexpected load event") + img.src = get_url(mime) + document.body.appendChild(img) + }, "ORB should block the response if Content-Type is: '" + mime + "'. ") + }) +</script> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html b/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html new file mode 100644 index 0000000000..66462fb5e3 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not ORB-eligible. --> +<img src="../resources/png-mislabeled-as-html.png"> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html b/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html new file mode 100644 index 0000000000..aa03f4db63 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<!-- Test verifies that ORB allows an mislabeled cross-origin image after sniffing. --> +<meta charset="utf-8"> +<!-- Reference page uses same-origin resources, which are not ORB-eligible. --> +<link rel="match" href="img-png-mislabeled-as-html.sub-ref.html"> +<!-- www1 is cross-origin, so the HTTP response is ORB-eligible --> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/png-mislabeled-as-html.png"> diff --git a/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html b/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html new file mode 100644 index 0000000000..2d5e3bb8b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<!-- Same-origin, so the HTTP response is not ORB-eligible. --> +<img src="../resources/png-unlabeled.png"> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub.html b/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub.html new file mode 100644 index 0000000000..77415f6af1 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/img-png-unlabeled.sub.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<!-- Test verifies that ORB allows an unlabeled cross-origin image after sniffing. --> +<meta charset="utf-8"> +<!-- Reference page uses same-origin resources, which are not ORB-eligible. --> +<link rel="match" href="img-png-unlabeled.sub-ref.html"> +<!-- www1 is cross-origin, so the HTTP response is ORB-eligible --> +<img src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/png-unlabeled.png"> diff --git a/testing/web-platform/tests/fetch/orb/tentative/known-mime-type.sub.any.js b/testing/web-platform/tests/fetch/orb/tentative/known-mime-type.sub.any.js new file mode 100644 index 0000000000..cc6d208fd3 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/known-mime-type.sub.any.js @@ -0,0 +1,99 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +expected_block( + `${path}/font.ttf`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("font/ttf")), + message("ORB should block opaque font/ttf"))); + +expected_block( + `${path}/text.txt`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("text/plain")), + message("ORB should block opaque text/plain"))); + +expected_block( + `${path}/data.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB should block opaque application/json (non-empty)"))); + +expected_block( + `${path}/empty.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB should block opaque application/json (empty)"))); + +expected_block( + `${path}/data_non_ascii.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB should block opaque application/json which contains non ascii characters"))); + +expected_allow( + `${path}/image.png`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("image/png")), + message("ORB shouldn't block opaque image/png")), + { skip: ["audio", "video", "script"] }); + +expected_allow( + `${path}/script.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("text/javascript")), + message("ORB shouldn't block opaque text/javascript")), + { skip: ["image", "audio", "video"] }); + +// Test javascript validation can correctly decode the content with BOM. +expected_allow( + `${path}/script-utf16-bom.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB shouldn't block opaque text/javascript (utf16 encoded with BOM)")), + { skip: ["image", "audio", "video"] }); + +// Test javascript validation can correctly decode the content with the http charset hint. +expected_allow( + `${path}/script-utf16-without-bom.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json; charset=utf-16")), + message("ORB shouldn't block opaque text/javascript (utf16 encoded without BOM but charset is provided in content-type)")), + { skip: ["image", "audio", "video"] }); + +// Test javascript validation can correctly decode the content for iso-8559-1 (fallback decoder in Firefox). +expected_allow( + `${path}/script-iso-8559-1.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB shouldn't block opaque text/javascript (iso-8559-1 encoded)")), + { skip: ["image", "audio", "video"] }); + +// Test javascript validation can correctly parse asm.js. +expected_allow( + `${path}/script-asm-js-valid.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB shouldn't block text/javascript with valid asm.js")), + { skip: ["image", "audio", "video"] }); + +// Test javascript validation can correctly parse invalid asm.js with valid JS syntax. +expected_allow( + `${path}/script-asm-js-invalid.js`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json")), + message("ORB shouldn't block text/javascript with invalid asm.js")), + { skip: ["image", "audio", "video"] }); diff --git a/testing/web-platform/tests/fetch/orb/tentative/nosniff.sub.any.js b/testing/web-platform/tests/fetch/orb/tentative/nosniff.sub.any.js new file mode 100644 index 0000000000..d1e01fd87b --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/nosniff.sub.any.js @@ -0,0 +1,32 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +expected_block( + `${path}/text.txt`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("text/plain"), contentTypeOptions("nosniff")), + message("ORB should block opaque text/plain with nosniff"))); + +expected_block( + `${path}/data.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json"), contentTypeOptions("nosniff")), + message("ORB should block opaque-response-blocklisted MIME type with nosniff"))); + +expected_block( + `${path}/data.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentTypeOptions("nosniff")), + message("ORB should block opaque response with empty Content-Type and nosniff"))); + +expected_allow( + `${path}/image.png`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType(""), contentType("text/javascript")), + message("ORB shouldn't block opaque image with empty Content-Type and nosniff")), + { skip: ["audio", "video", "script"] }); diff --git a/testing/web-platform/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html b/testing/web-platform/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html new file mode 100644 index 0000000000..fe85440798 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<!-- Test verifies that gziped script which parses as Javascript (not JSON) without Content-Type will execute with ORB. --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +setup({ single_test: true }); +window.has_executed_script = false; +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled.js?pipe=gzip|header(Content-Type,)"> +</script> + +<script> +// Verify what observable effects the <script> tag above had. +// Assertion should hold with and without ORB: +assert_true(window.has_executed_script, + 'The cross-origin script should execute'); +done(); +</script> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/script-unlabeled.sub.html b/testing/web-platform/tests/fetch/orb/tentative/script-unlabeled.sub.html new file mode 100644 index 0000000000..4987f1307e --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/script-unlabeled.sub.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<!-- Test verifies that script which parses as Javascript (not JSON) without Content-Type will execute with ORB. --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +setup({ single_test: true }); +window.has_executed_script = false; +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<script src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled.js"> +</script> + +<script> +// Verify what observable effects the <script> tag above had. +// Assertion should hold with and without ORB: +assert_true(window.has_executed_script, + 'The cross-origin script should execute'); +done(); +</script> + diff --git a/testing/web-platform/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html b/testing/web-platform/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html new file mode 100644 index 0000000000..b15f976a66 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- Test verifies that utf-16 encoded script (without BOM) which parses as Javascript (not JSON) will execute with ORB. --> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> + +<script> +setup({ single_test: true }); +window.has_executed_script = false; +</script> + +<!-- www1 is cross-origin, so the HTTP response is CORB-eligible --> +<script charset="utf-16" src="http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/js-unlabeled-utf16-without-bom.json"> +</script> + +<script> +// Verify what observable effects the <script> tag above had. +// Assertion should hold with and without ORB: +assert_true(window.has_executed_script, + 'The cross-origin script should execute'); +done(); +</script> diff --git a/testing/web-platform/tests/fetch/orb/tentative/status.sub.any.js b/testing/web-platform/tests/fetch/orb/tentative/status.sub.any.js new file mode 100644 index 0000000000..e281ca6f53 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/status.sub.any.js @@ -0,0 +1,16 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +expected_block( + `${path}/data.json`, + (orb_test, message) => promise_test( + t => orb_test(t, contentType("application/json"), "status(206)"), + message("ORB should block opaque-response-blocklisted MIME type with status 206"))); + +expected_block( + `${path}/data.json`, + (orb_test, message) => + promise_test( + t => orb_test(t, contentType("application/json"), "status(302)"), + message("ORB should block opaque response with non-ok status"))); diff --git a/testing/web-platform/tests/fetch/orb/tentative/status.sub.html b/testing/web-platform/tests/fetch/orb/tentative/status.sub.html new file mode 100644 index 0000000000..a62bdeb35e --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/status.sub.html @@ -0,0 +1,17 @@ +'use strict'; + +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +async_test(function(t) { + let url = "http://{{domains[www1]}}:{{ports[http][0]}}" + url = `${url}/fetch/orb/resources/sound.mp3?pipe=status(301)|header(Content-Type,)` + + const video = document.createElement("video"); + video.src = url; + video.onerror = t.step_func_done(); + video.onload = t.unreached_func("Unexpected error event"); + document.body.appendChild(video); +}, "ORB should block initial media requests with status not 200 or 206"); +</script> diff --git a/testing/web-platform/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js b/testing/web-platform/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js new file mode 100644 index 0000000000..268ebb0258 --- /dev/null +++ b/testing/web-platform/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js @@ -0,0 +1,40 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +expected_allow_fetch( + `${path}/font.ttf`, + (promise, message) => + promise_test( + t => promise(t, contentType("")), + message("ORB shouldn't block opaque failed missing MIME type (font/ttf)"))); + +expected_allow_fetch( + `${path}/text.ttf`, + (promise, message) => + promise_test( + t => promise(t, contentType("")), + message("ORB shouldn't block opaque failed missing MIME type (text/plain)"))); + +expected_allow_fetch( + `${path}/data.json`, + (promise, message) => + promise_test( + t => promise(t, contentType("")), + message("ORB shouldn't block opaque failed missing MIME type (application/json)"))); + +expected_allow( + `${path}/image.png`, + (promise, message) => + promise_test( + t => promise(t, contentType("")), + message("ORB shouldn't block opaque failed missing MIME type (image/png)")), + { skip: ["audio", "video", "script"] }); + +expected_allow( + `${path}/script.js`, + (promise, message) => + promise_test( + t => promise(t, contentType("")), + message("ORB shouldn't block opaque failed missing MIME type (text/javascript)")), + { skip: ["image", "audio", "video"] }); diff --git a/testing/web-platform/tests/fetch/origin/assorted.window.js b/testing/web-platform/tests/fetch/origin/assorted.window.js new file mode 100644 index 0000000000..033d010f35 --- /dev/null +++ b/testing/web-platform/tests/fetch/origin/assorted.window.js @@ -0,0 +1,211 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const origins = get_host_info(); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + // Cross-origin -> same-origin will result in setting the tainted origin flag for the second + // request. + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url) + "&dummyJS"; + + await fetch(url, { mode: "no-cors", method: "POST" }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and 308 redirect"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + frame.src = url; + frame.onload = () => { + resolve(); + frame.remove(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], "no Origin header"); + assert_equals(json[1], "no Origin header"); +}, "Origin header and GET navigation"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + self.addEventListener("message", e => { + if (e.data === "loaded") { + resolve(); + frame.remove(); + } + }, { once: true }); + frame.onload = () => { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = url; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and POST navigation"); + +function navigationReferrerPolicy(referrerPolicy, destination, expectedOrigin) { + return async function () { + const stash = token(); + const referrerPolicyPath = "/fetch/origin/resources/referrer-policy.py"; + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let postUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash; + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = origins.HTTP_ORIGIN + referrerPolicyPath + + "?referrerPolicy=" + referrerPolicy; + self.addEventListener("message", function listener(e) { + if (e.data === "loaded") { + resolve(); + frame.remove(); + self.removeEventListener("message", listener); + } else if (e.data === "action") { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = postUrl; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + }); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function fetchReferrerPolicy(referrerPolicy, destination, fetchMode, expectedOrigin, httpMethod) { + return async function () { + const stash = token(); + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let fetchUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash + "&dummyJS"; + + await fetch(fetchUrl, { mode: fetchMode, method: httpMethod , "referrerPolicy": referrerPolicy}); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function referrerPolicyTestString(referrerPolicy, method, destination) { + return "Origin header and " + method + " " + destination + " with Referrer-Policy " + + referrerPolicy; +} + +[ + { + "policy": "no-referrer", + "expectedOriginForSameOrigin": "null", + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "same-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "origin-when-cross-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "no-referrer-when-downgrade", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "unsafe-url", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, +].forEach(testObj => { + [ + { + "name": "same-origin", + "expectedOrigin": testObj.expectedOriginForSameOrigin + }, + { + "name": "cross-origin", + "expectedOrigin": testObj.expectedOriginForCrossOrigin + } + ].forEach(destination => { + // Test form POST navigation + promise_test(navigationReferrerPolicy(testObj.policy, + destination.name, + destination.expectedOrigin), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " navigation")); + // Test fetch + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "no-cors", + destination.expectedOrigin, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch no-cors mode")); + + // Test cors mode POST + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + origins.HTTP_ORIGIN, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch cors mode")); + + // Test cors mode GET + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + (destination.name == "same-origin") ? "no Origin header" : origins.HTTP_ORIGIN, + "GET"), + referrerPolicyTestString(testObj.policy, "GET", + destination.name + " fetch cors mode")); + }); +}); diff --git a/testing/web-platform/tests/fetch/origin/resources/redirect-and-stash.py b/testing/web-platform/tests/fetch/origin/resources/redirect-and-stash.py new file mode 100644 index 0000000000..36c584c08c --- /dev/null +++ b/testing/web-platform/tests/fetch/origin/resources/redirect-and-stash.py @@ -0,0 +1,38 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + key = request.GET.first(b"stash") + origin = request.headers.get(b"origin") + if origin is None: + origin = b"no Origin header" + + origin_list = request.server.stash.take(key) + + if b"dump" in request.GET: + response.headers.set(b"Content-Type", b"application/json") + response.content = json.dumps(origin_list) + return + + if origin_list is None: + origin_list = [isomorphic_decode(origin)] + else: + origin_list.append(isomorphic_decode(origin)) + + request.server.stash.put(key, origin_list) + + if b"location" in request.GET: + location = request.GET.first(b"location") + if b"dummyJS" in request.GET: + location += b"&dummyJS" + response.status = 308 + response.headers.set(b"Location", location) + return + + response.headers.set(b"Content-Type", b"text/html") + response.headers.set(b"Access-Control-Allow-Origin", b"*") + if b"dummyJS" in request.GET: + response.content = b"console.log('dummy JS')" + else: + response.content = b"<meta charset=utf-8>\n<body><script>parent.postMessage('loaded','*')</script></body>" diff --git a/testing/web-platform/tests/fetch/origin/resources/referrer-policy.py b/testing/web-platform/tests/fetch/origin/resources/referrer-policy.py new file mode 100644 index 0000000000..15716e068b --- /dev/null +++ b/testing/web-platform/tests/fetch/origin/resources/referrer-policy.py @@ -0,0 +1,7 @@ +def main(request, response): + if b"referrerPolicy" in request.GET: + response.headers.set(b"Referrer-Policy", + request.GET.first(b"referrerPolicy")) + response.status = 200 + response.headers.set(b"Content-Type", b"text/html") + response.content = b"<meta charset=utf-8>\n<body><script>parent.postMessage('action','*')</script></body>" diff --git a/testing/web-platform/tests/fetch/private-network-access/META.yml b/testing/web-platform/tests/fetch/private-network-access/META.yml new file mode 100644 index 0000000000..944ce6f14a --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/META.yml @@ -0,0 +1,7 @@ +spec: https://wicg.github.io/private-network-access/ +suggested_reviewers: + - letitz + - lyf + - hemeryar + - camillelamy + - mikewest diff --git a/testing/web-platform/tests/fetch/private-network-access/README.md b/testing/web-platform/tests/fetch/private-network-access/README.md new file mode 100644 index 0000000000..a69aab4872 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/README.md @@ -0,0 +1,10 @@ +# Private Network Access tests + +This directory contains tests for Private Network Access' integration with +the Fetch specification. + +See also: + +* [The specification](https://wicg.github.io/private-network-access/) +* [The repository](https://github.com/WICG/private-network-access/) +* [Open issues](https://github.com/WICG/private-network-access/issues/) diff --git a/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.https.window.js new file mode 100644 index 0000000000..4e860ad381 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.https.window.js @@ -0,0 +1,191 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: variant=?include=from-treat-as-public +// +// These tests verify that secure contexts can navigate to less-public address +// spaces via an anchor link iff the target server responds affirmatively to +// preflight requests. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey("from-local", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: NavigationTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "local to private: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + key, + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = + `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel(t => anchorTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "failed preflight."); + + promise_test_parallel(t => anchorTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing CORS headers."); + + promise_test_parallel(t => anchorTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing PNA header."); + + promise_test_parallel(t => anchorTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + expected: NavigationTestResult.SUCCESS, + }), prefix + "success."); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey('from-private', makePreflightTests, { + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +subsetTestByKey("from-private", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "private to public: no preflight required."); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_PRIVATE, + targetName: "private", +}); + +subsetTestByKey("from-public", promise_test_parallel, t => anchorTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => anchorTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to local (same-origin): no preflight required.'); + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => anchorTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.'); diff --git a/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.window.js new file mode 100644 index 0000000000..cb53865808 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/anchor.tentative.window.js @@ -0,0 +1,95 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/ +// +// These tests verify that non-secure contexts cannot open a new window via an +// anchor link to less-public address spaces. + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "private to local: failure."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "public to local: failure."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "public to private: failure."); + +promise_test_parallel(t => anchorTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "public to public: no preflight required."); + +promise_test_parallel(t => anchorTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test_parallel(t => anchorTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test_parallel(t => anchorTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js new file mode 100644 index 0000000000..33e94d57f1 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js @@ -0,0 +1,91 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that contexts can navigate fenced frames to more-public or +// same address spaces without private network access preflight request header. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, + }), + 'local to local: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PRIVATE}, + expected: FrameTestResult.SUCCESS, + }), + 'local to private: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'local to public: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PRIVATE}, + expected: FrameTestResult.SUCCESS, + }), + 'private to private: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'private to public: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PUBLIC}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'public to public: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.'); + +promise_test( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {preflight: PreflightBehavior.optionalSuccess(token())} + }, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local: optional preflight'); diff --git a/testing/web-platform/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js new file mode 100644 index 0000000000..2dff325e3e --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js @@ -0,0 +1,330 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: variant=?include=baseline +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that secure contexts can fetch subresources in fenced +// frames from all address spaces, provided that the target server, if more +// private than the initiator, respond affirmatively to preflight requests. +// + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey( + 'from-local', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_LOCAL}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to local: no preflight required.'); + +subsetTestByKey( + 'from-local', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PRIVATE, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to private: no preflight required.'); + + +subsetTestByKey( + 'from-local', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to public: no preflight required.'); + +// Strictly speaking, the following two tests do not exercise PNA-specific +// logic, but they serve as a baseline for comparison, ensuring that non-PNA +// preflight requests are sent and handled as expected. + +subsetTestByKey( + 'baseline', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + 'local to public: PUT preflight failure.'); + +subsetTestByKey( + 'baseline', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to public: PUT preflight success.'); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - final response is missing CORS headers +// - success +// - success with PUT method (non-"simple" request) +// - no-cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + subsetKey, + source, + sourceDescription, + targetServer, + targetDescription, +}) { + const prefix = `${sourceDescription} to ${targetDescription}: `; + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'failed preflight.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noCorsHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing CORS headers on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing PNA header on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.success(token())}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing CORS headers on final response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + prefix + 'success.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + prefix + 'PUT success.'); + + subsetTestByKey( + subsetKey, promise_test, t => fencedFrameFetchTest(t, { + source, + target: {server: targetServer}, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode failed preflight.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noCorsHeader(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode missing CORS headers on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noPnaHeader(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode missing PNA header on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.success(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.OPAQUE, + }), + prefix + 'no-CORS mode success.'); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: 'from-private', + source: {server: Server.HTTPS_PRIVATE}, + sourceDescription: 'private', + targetServer: Server.HTTPS_LOCAL, + targetDescription: 'local', +}); + +subsetTestByKey( + 'from-private', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PRIVATE}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'private to private: no preflight required.'); + +subsetTestByKey( + 'from-private', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: { + server: Server.HTTPS_PRIVATE, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'private to public: no preflight required.'); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: 'from-public', + source: {server: Server.HTTPS_PUBLIC}, + sourceDescription: 'public', + targetServer: Server.HTTPS_LOCAL, + targetDescription: 'local', +}); + +makePreflightTests({ + subsetKey: 'from-public', + source: {server: Server.HTTPS_PUBLIC}, + sourceDescription: 'public', + targetServer: Server.HTTPS_PRIVATE, + targetDescription: 'private', +}); + +subsetTestByKey( + 'from-public', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PUBLIC}, + target: {server: Server.HTTPS_PUBLIC}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'public to public: no preflight required.'); diff --git a/testing/web-platform/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js new file mode 100644 index 0000000000..370cc9fbe9 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js @@ -0,0 +1,150 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that contexts can navigate fenced frames to less-public +// address spaces iff the target server responds affirmatively to preflight +// requests. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - parent navigates child: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - preflight response has the required PNA related headers, but still fails +// because of the limitation of fenced frame that subjects to PNA checks. +// +function makePreflightTests({ + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.failure()}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'failed preflight.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noCorsHeader(token())}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'missing CORS headers.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noPnaHeader(token())}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'missing PNA header.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin() + }, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'failed because fenced frames are incompatible with PNA.'); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +makePreflightTests({ + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +makePreflightTests({ + sourceServer: Server.HTTPS_PUBLIC, + sourceName: 'public', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +makePreflightTests({ + sourceServer: Server.HTTPS_PUBLIC, + sourceName: 'public', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +makePreflightTests({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: 'local', +}); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.FAILURE, + }), + 'treat-as-public-address to local (same-origin): fenced frame embedder ' + + 'initiated navigation has opaque origin.'); + +makePreflightTests({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js new file mode 100644 index 0000000000..084e03282f --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js @@ -0,0 +1,80 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that documents fetched from the `local` or `private` +// address space yet carrying the `treat-as-public-address` CSP directive are +// treated as if they had been fetched from the `public` address space. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + preflight: PreflightBehavior.noPnaHeader(token()), + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to local: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to local: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to local (same-origin): no preflight required."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to private: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to private: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.https.window.js new file mode 100644 index 0000000000..606443dc14 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.https.window.js @@ -0,0 +1,271 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: variant=?include=baseline +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that secure contexts can fetch subresources from all +// address spaces, provided that the target server, if more private than the +// initiator, respond affirmatively to preflight requests. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: fetch.window.js + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey("from-local", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: FetchTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to private: no preflight required."); + + +subsetTestByKey("from-local", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Strictly speaking, the following two tests do not exercise PNA-specific +// logic, but they serve as a baseline for comparison, ensuring that non-PNA +// preflight requests are sent and handled as expected. + +subsetTestByKey("baseline", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { method: "PUT" }, + expected: FetchTestResult.FAILURE, +}), "local to public: PUT preflight failure."); + +subsetTestByKey("baseline", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + fetchOptions: { method: "PUT" }, + expected: FetchTestResult.SUCCESS, +}), "local to public: PUT preflight success."); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - final response is missing CORS headers +// - success +// - success with PUT method (non-"simple" request) +// - no-cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + subsetKey, + source, + sourceDescription, + targetServer, + targetDescription, +}) { + const prefix = + `${sourceDescription} to ${targetDescription}: `; + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "failed preflight."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noCorsHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing CORS headers on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing PNA header on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing CORS headers on final response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }), prefix + "success."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { method: "PUT" }, + expected: FetchTestResult.SUCCESS, + }), prefix + "PUT success."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { server: targetServer }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode failed preflight."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode missing CORS headers on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode missing PNA header on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, + }), prefix + "no-CORS mode success."); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: "from-private", + source: { server: Server.HTTPS_PRIVATE }, + sourceDescription: "private", + targetServer: Server.HTTPS_LOCAL, + targetDescription: "local", +}); + +subsetTestByKey("from-private", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FetchTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "private to public: no preflight required."); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: "from-public", + source: { server: Server.HTTPS_PUBLIC }, + sourceDescription: "public", + targetServer: Server.HTTPS_LOCAL, + targetDescription: "local", +}); + +makePreflightTests({ + subsetKey: "from-public", + source: { server: Server.HTTPS_PUBLIC }, + sourceDescription: "public", + targetServer: Server.HTTPS_PRIVATE, + targetDescription: "private", +}); + +subsetTestByKey("from-public", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: FetchTestResult.SUCCESS, +}), "public to public: no preflight required."); + diff --git a/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.window.js new file mode 100644 index 0000000000..8ee54c9056 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/fetch.tentative.window.js @@ -0,0 +1,183 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that non-secure contexts cannot fetch subresources from +// less-public address spaces, and can fetch them otherwise. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: fetch.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: FetchTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: FetchTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: FetchTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// These tests verify that documents fetched from the `local` address space yet +// carrying the `treat-as-public-address` CSP directive are treated as if they +// had been fetched from the `public` address space. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// These tests verify that HTTPS iframes embedded in an HTTP top-level document +// cannot fetch subresources from less-public address spaces. Indeed, even +// though the iframes have HTTPS origins, they are non-secure contexts because +// their parent is a non-secure context. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public https to private: failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.https.window.js new file mode 100644 index 0000000000..1e00c0af41 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.https.window.js @@ -0,0 +1,267 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: variant=?include=from-treat-as-public +// META: variant=?include=grandparent +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that secure contexts can navigate iframes to less-public +// address spaces iff the target server responds affirmatively to preflight +// requests. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: iframe.tentative.window.js + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: FrameTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FrameTestResult.SUCCESS, +}), "local to private: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - parent navigates child: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + key, + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = + `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: FrameTestResult.FAILURE, + }), prefix + "failed preflight."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + expected: FrameTestResult.FAILURE, + }), prefix + "missing CORS headers."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + expected: FrameTestResult.FAILURE, + }), prefix + "missing PNA header."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + expected: FrameTestResult.SUCCESS, + }), prefix + "success."); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey('from-private', makePreflightTests, { + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FrameTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "private to public: no preflight required."); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_PRIVATE, + targetName: "private", +}); + +subsetTestByKey("from-public", promise_test_parallel, t => iframeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local (same-origin): no preflight required.' +); + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.HTTPS_PRIVATE, + targetName: "private", +}); + +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.' +); + +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {preflight: PreflightBehavior.optionalSuccess(token())} + }, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local: optional preflight' +); + +// The following tests verify that when a grandparent frame navigates its +// grandchild, the IP address space of the grandparent is compared against the +// IP address space of the response. Indeed, the navigation initiator in this +// case is the grandparent, not the parent. + +subsetTestByKey('grandparent', iframeGrandparentTest, { + name: 'local to local, grandparent navigates: no preflight required.', + grandparentServer: Server.HTTPS_LOCAL, + child: {server: Server.HTTPS_PUBLIC}, + grandchild: {server: Server.OTHER_HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, +}); + +subsetTestByKey('grandparent', iframeGrandparentTest, { + name: "local to local (same-origin), grandparent navigates: no preflight required.", + grandparentServer: Server.HTTPS_LOCAL, + child: { server: Server.HTTPS_PUBLIC }, + grandchild: { server: Server.HTTPS_LOCAL }, + expected: FrameTestResult.SUCCESS, +}); + +subsetTestByKey('grandparent', iframeGrandparentTest, { + name: "public to local, grandparent navigates: failure.", + grandparentServer: Server.HTTPS_PUBLIC, + child: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + grandchild: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: FrameTestResult.FAILURE, +}); + +subsetTestByKey('grandparent', iframeGrandparentTest, { + name: "public to local, grandparent navigates: success.", + grandparentServer: Server.HTTPS_PUBLIC, + child: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + grandchild: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + expected: FrameTestResult.SUCCESS, +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.window.js new file mode 100644 index 0000000000..441e0884d2 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/iframe.tentative.window.js @@ -0,0 +1,110 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that non-secure contexts cannot navigate iframes to +// less-public address spaces, and can navigate them otherwise. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: iframe.tentative.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: FrameTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PRIVATE }, + expected: FrameTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_LOCAL }, + expected: FrameTestResult.FAILURE, +}), "private to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: FrameTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_LOCAL }, + expected: FrameTestResult.FAILURE, +}), "public to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PRIVATE }, + expected: FrameTestResult.FAILURE, +}), "public to private: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "public to public: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: FrameTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: FrameTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PUBLIC }, + expected: FrameTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// The following test verifies that when a grandparent frame navigates its +// grandchild, the IP address space of the grandparent is compared against the +// IP address space of the response. Indeed, the navigation initiator in this +// case is the grandparent, not the parent. + +iframeGrandparentTest({ + name: "local to local, grandparent navigates: success.", + grandparentServer: Server.HTTP_LOCAL, + child: { server: Server.HTTP_PUBLIC }, + grandchild: { server: Server.HTTP_LOCAL }, + expected: FrameTestResult.SUCCESS, +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js new file mode 100644 index 0000000000..dbae5193b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js @@ -0,0 +1,278 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access +// +// These tests verify that secure contexts can fetch non-secure subresources +// from more private address spaces, avoiding mixed context checks, as long as +// they specify a valid `targetAddressSpace` fetch option that matches the +// target server's address space. + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// Given `addressSpace`, returns the other three possible IP address spaces. +function otherAddressSpaces(addressSpace) { + switch (addressSpace) { + case "local": return ["unknown", "private", "public"]; + case "private": return ["unknown", "local", "public"]; + case "public": return ["unknown", "local", "private"]; + } +} + +// Generates tests of `targetAddressSpace` for the given (source, target) +// address space pair, expecting fetches to succeed iff `targetAddressSpace` is +// correct. +// +// Scenarios exercised: +// +// - cors mode: +// - missing targetAddressSpace option +// - incorrect targetAddressSpace option (x3, see `otherAddressSpaces()`) +// - failed preflight +// - success +// - success with PUT method (non-"simple" request) +// - no-cors mode: +// - success +// +function makeTests({ source, target }) { + const sourceServer = Server.get("https", source); + const targetServer = Server.get("http", target); + + const makeTest = ({ + fetchOptions, + targetBehavior, + name, + expected + }) => { + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: targetBehavior, + }, + fetchOptions, + expected, + }), `${sourceServer.name} to ${targetServer.name}: ${name}.`); + }; + + makeTest({ + name: "missing targetAddressSpace", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + expected: FetchTestResult.FAILURE, + }); + + const correctAddressSpace = targetServer.addressSpace; + + for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) { + makeTest({ + name: `wrong targetAddressSpace "${targetAddressSpace}"`, + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace }, + expected: FetchTestResult.FAILURE, + }); + } + + makeTest({ + name: "failed preflight", + targetBehavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.FAILURE, + }); + + makeTest({ + name: "success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.SUCCESS, + }); + + makeTest({ + name: "PUT success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { + targetAddressSpace: correctAddressSpace, + method: "PUT", + }, + expected: FetchTestResult.SUCCESS, + }); + + makeTest({ + name: "no-cors success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { + targetAddressSpace: correctAddressSpace, + mode: "no-cors", + }, + expected: FetchTestResult.OPAQUE, + }); +} + +// Generates tests for the given (source, target) address space pair expecting +// that `targetAddressSpace` cannot be used to bypass mixed content. +// +// Scenarios exercised: +// +// - wrong `targetAddressSpace` (x3, see `otherAddressSpaces()`) +// - correct `targetAddressSpace` +// +function makeNoBypassTests({ source, target }) { + const sourceServer = Server.get("https", source); + const targetServer = Server.get("http", target); + + const prefix = `${sourceServer.name} to ${targetServer.name}: `; + + const correctAddressSpace = targetServer.addressSpace; + for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) { + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace }, + expected: FetchTestResult.FAILURE, + }), prefix + `wrong targetAddressSpace "${targetAddressSpace}".`); + } + + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.FAILURE, + }), prefix + 'not a private network request.'); +} + +// Source: local secure context. +// +// Fetches to the local and private address spaces cannot use +// `targetAddressSpace` to bypass mixed content, as they are not otherwise +// blocked by Private Network Access. + +makeNoBypassTests({ source: "local", target: "local" }); +makeNoBypassTests({ source: "local", target: "private" }); +makeNoBypassTests({ source: "local", target: "public" }); + +// Source: private secure context. +// +// Fetches to the local address space requires the right `targetAddressSpace` +// option, as well as a successful preflight response carrying a PNA-specific +// header. +// +// Fetches to the private address space cannot use `targetAddressSpace` to +// bypass mixed content, as they are not otherwise blocked by Private Network +// Access. + +makeTests({ source: "private", target: "local" }); + +makeNoBypassTests({ source: "private", target: "private" }); +makeNoBypassTests({ source: "private", target: "public" }); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require the right +// `targetAddressSpace` option, as well as a successful preflight response +// carrying a PNA-specific header. + +makeTests({ source: "public", target: "local" }); +makeTests({ source: "public", target: "private" }); + +makeNoBypassTests({ source: "public", target: "public" }); + +// These tests verify that documents fetched from the `local` address space yet +// carrying the `treat-as-public-address` CSP directive are treated as if they +// had been fetched from the `public` address space. + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "private" }, + expected: FetchTestResult.FAILURE, +}), 'https-treat-as-public to http-local: wrong targetAddressSpace "private".'); + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "local" }, + expected: FetchTestResult.SUCCESS, +}), "https-treat-as-public to http-local: success."); + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "local" }, + expected: FetchTestResult.FAILURE, +}), 'https-treat-as-public to http-private: wrong targetAddressSpace "local".'); + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "private" }, + expected: FetchTestResult.SUCCESS, +}), "https-treat-as-public to http-private: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.https.window.js new file mode 100644 index 0000000000..3eeb435bad --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.https.window.js @@ -0,0 +1,36 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches from within worker +// scopes are subject to Private Network Access checks, just like a worker +// script fetches from within document scopes (for non-nested workers). The +// latter are tested in: worker.https.window.js +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: nested-worker.window.js + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.window.js new file mode 100644 index 0000000000..6d246e1c76 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/nested-worker.tentative.window.js @@ -0,0 +1,36 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches from within worker +// scopes are subject to Private Network Access checks, just like a worker +// script fetches from within document scopes (for non-nested workers). The +// latter are tested in: worker.window.js +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: nested-worker.https.window.js + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js new file mode 100644 index 0000000000..87dbf501f6 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js @@ -0,0 +1,88 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#cors-preflight +// +// These tests verify that PNA preflight responses are cached. +// +// TODO(https://crbug.com/1268312): We cannot currently test that cache +// entries are keyed by target IP address space because that requires +// loading the same URL from different IP address spaces, and the WPT +// framework does not allow that. +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "private to local: success."); + +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "public to local: success."); + +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "public to private: success.");
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/private-network-access/redirect.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/redirect.tentative.https.window.js new file mode 100644 index 0000000000..efbd8f31f9 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/redirect.tentative.https.window.js @@ -0,0 +1,640 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// This test verifies that Private Network Access checks are applied to all +// the endpoints in a redirect chain, relative to the same client context. + +// local -> private -> public +// +// Request 1 (local -> private): no preflight. +// Request 2 (local -> public): no preflight. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "local to private to public: success."); + +// local -> private -> local +// +// Request 1 (local -> private): no preflight. +// Request 2 (local -> local): no preflight. +// +// This checks that the client for the second request is still the initial +// context, not the redirector. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "local to private to local: success."); + +// private -> private -> local +// +// Request 1 (private -> private): no preflight. +// Request 2 (private -> local): preflight required. +// +// This verifies that PNA checks are applied after redirects. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "private to private to local: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "private to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "private to private to local: no-cors success."); + +// private -> local -> private +// +// Request 1 (private -> local): preflight required. +// Request 2 (private -> private): no preflight. +// +// This verifies that PNA checks are applied independently to every step in a +// redirect chain. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "private to local to private: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "private to local to private: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "private to local to private: no-cors success."); + +// public -> private -> local +// +// Request 1 (public -> private): preflight required. +// Request 2 (public -> local): preflight required. +// +// This verifies that PNA checks are applied to every step in a redirect chain. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "public to private to local: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "public to private to local: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "public to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "public to private to local: no-cors success."); + +// treat-as-public -> local -> private + +// Request 1 (treat-as-public -> local): preflight required. +// Request 2 (treat-as-public -> private): preflight required. + +// This verifies that PNA checks are applied to every step in a redirect chain. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to local to private: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: no-cors failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: no-cors failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to local to private: no-cors success."); + +// treat-as-public -> local (same-origin) -> private + +// Request 1 (treat-as-public -> local (same-origin)): no preflight required. +// Request 2 (treat-as-public -> private): preflight required. + +// This verifies that PNA checks are applied only to the second step in a +// redirect chain if the first step is same-origin and the origin is potentially +// trustworthy. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local (same-origin) to private: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to local (same-origin) to private: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local (same-origin) to private: no-cors failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to local (same-origin) to private: no-cors success."); + +// treat-as-public -> private -> local + +// Request 1 (treat-as-public -> private): preflight required. +// Request 2 (treat-as-public -> local): preflight required. + +// This verifies that PNA checks are applied to every step in a redirect chain. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.OTHER_HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.OTHER_HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: no-cors failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.OTHER_HTTPS_LOCAL }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: no-cors failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.OTHER_HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to private to local: no-cors success."); + +// treat-as-public -> private -> local (same-origin) + +// Request 1 (treat-as-public -> private): preflight required. +// Request 2 (treat-as-public -> local (same-origin)): no preflight required. + +// This verifies that PNA checks are only applied to the first step in a +// redirect chain if the second step is same-origin and the origin is +// potentially trustworthy. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local (same-origin): failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to private to local (same-origin): success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ server: Server.HTTPS_LOCAL }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local (same-origin): no-cors failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_LOCAL }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to private to local (same-origin): no-cors success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/anchor.html b/testing/web-platform/tests/fetch/private-network-access/resources/anchor.html new file mode 100644 index 0000000000..0780b3fa50 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/anchor.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Anchor</title> +<body></body> +<script> + window.onmessage = (event) => { + window.onmessage = (event) => parent.postMessage(event.data, "*"); + const { url } = event.data; + const anchor = document.createElement('a'); + anchor.href = url; + anchor.rel = 'opener'; + anchor.target = '_blank'; + document.body.appendChild(anchor); + anchor.click(); + }; +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/executor.html b/testing/web-platform/tests/fetch/private-network-access/resources/executor.html new file mode 100644 index 0000000000..d71212951c --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/executor.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Executor</title> +<body></body> +<script src="/common/dispatcher/dispatcher.js"></script> +<script> + const uuid = new URL(window.location).searchParams.get("executor-uuid"); + const executor = new Executor(uuid); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html new file mode 100644 index 0000000000..b14601dba5 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="../../../fenced-frame/resources/utils.js"></script> +<title>Fetcher</title> +<script> + const url = new URL(location.href).searchParams.get("url"); + const mode = new URL(location.href).searchParams.get("mode"); + const method = new URL(location.href).searchParams.get("method"); + const [error_token, ok_token, body_token, type_token] = parseKeylist(); + + fetch(url, {mode: mode, method: method}) + .then(async function(response) { + const body = await response.text(); + writeValueToServer(ok_token, response.ok); + writeValueToServer(body_token, body); + writeValueToServer(type_token, response.type); + writeValueToServer(error_token, ""); + }) + .catch(error => { + writeValueToServer(ok_token, ""); + writeValueToServer(body_token, ""); + writeValueToServer(type_token, ""); + writeValueToServer(error_token, error.toString()); + }); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers new file mode 100644 index 0000000000..6247f6d632 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers @@ -0,0 +1 @@ +Supports-Loading-Mode: fenced-frame
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access-target.https.html b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access-target.https.html new file mode 100644 index 0000000000..2b55e056f3 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access-target.https.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="../../../fenced-frame/resources/utils.js"></script> +<title>Fenced frame target</title> +<script> + const [frame_loaded_key] = parseKeylist(); + writeValueToServer(frame_loaded_key, 'loaded'); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html new file mode 100644 index 0000000000..98f118432e --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="../../../fenced-frame/resources/utils.js"></script> +<script src="/common/utils.js"></script> +<title>Fenced frame</title> +<body></body> +<script> +(async () => { + const target = new URL(location.href).searchParams.get("fenced_frame_url"); + const urn = await runSelectURL(target); + attachFencedFrame(urn); +})(); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html.headers b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html.headers new file mode 100644 index 0000000000..6247f6d632 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fenced-frame-private-network-access.https.html.headers @@ -0,0 +1 @@ +Supports-Loading-Mode: fenced-frame
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.html b/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.html new file mode 100644 index 0000000000..000a5cc25b --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Fetcher</title> +<script> + window.addEventListener("message", function (event) { + const { url, options } = event.data; + fetch(url, options) + .then(async function(response) { + const body = await response.text(); + const message = { + ok: response.ok, + type: response.type, + body: body, + }; + parent.postMessage(message, "*"); + }) + .catch(error => { + parent.postMessage({ error: error.toString() }, "*"); + }); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.js b/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.js new file mode 100644 index 0000000000..3a1859876d --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/fetcher.js @@ -0,0 +1,20 @@ +async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; +} + +async function fetchAndPost(url) { + try { + const message = await doFetch(url); + self.postMessage(message); + } catch(e) { + self.postMessage({ error: e.name }); + } +} + +const url = new URL(self.location.href).searchParams.get("url"); +fetchAndPost(url); diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/iframed-no-preflight-received.html b/testing/web-platform/tests/fetch/private-network-access/resources/iframed-no-preflight-received.html new file mode 100644 index 0000000000..20b5150d44 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/iframed-no-preflight-received.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Iframed</title> +<script> + const uuid = new URL(window.location).searchParams.get("iframe-uuid"); + top.postMessage({ uuid, message: "no preflight received" }, "*"); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/iframed.html b/testing/web-platform/tests/fetch/private-network-access/resources/iframed.html new file mode 100644 index 0000000000..c889c2882a --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/iframed.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Iframed</title> +<script> + const uuid = new URL(window.location).searchParams.get("iframe-uuid"); + top.postMessage({ uuid, message: "loaded" }, "*"); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/iframer.html b/testing/web-platform/tests/fetch/private-network-access/resources/iframer.html new file mode 100644 index 0000000000..304cc54ae4 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/iframer.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Iframer</title> +<body></body> +<script> + const child = document.createElement("iframe"); + child.src = new URL(window.location).searchParams.get("url"); + document.body.appendChild(child); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/no-preflight-received.html b/testing/web-platform/tests/fetch/private-network-access/resources/no-preflight-received.html new file mode 100644 index 0000000000..5ee533e182 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/no-preflight-received.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>No preflight received</title> +<script> + if (window.opener) window.opener.postMessage("no preflight received", "*"); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/open-to-existing-window.html b/testing/web-platform/tests/fetch/private-network-access/resources/open-to-existing-window.html new file mode 100644 index 0000000000..6460024bc8 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/open-to-existing-window.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Opener to an existing window</title> +<body></body> +<script> + window.onmessage = (event) => { + window.onmessage = (event) => parent.postMessage(event.data, "*"); + const { url, token } = event.data; + window.open('', token); + window.open(url, token); + }; +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/openee.html b/testing/web-platform/tests/fetch/private-network-access/resources/openee.html new file mode 100644 index 0000000000..8f0a859cb3 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/openee.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Openee</title> +<script> + if (window.opener) { + window.opener.postMessage("success", "*"); + } +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/opener.html b/testing/web-platform/tests/fetch/private-network-access/resources/opener.html new file mode 100644 index 0000000000..78b66c6db7 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/opener.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Opener</title> +<body></body> +<script> + window.onmessage = (event) => { + window.onmessage = (event) => parent.postMessage(event.data, "*"); + const { url } = event.data; + window.open(url); + }; +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/preflight.py b/testing/web-platform/tests/fetch/private-network-access/resources/preflight.py new file mode 100644 index 0000000000..4467663239 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/preflight.py @@ -0,0 +1,191 @@ +# This endpoint responds to both preflight requests and the subsequent requests. +# +# Its behavior can be configured with various search/GET parameters, all of +# which are optional: +# +# - treat-as-public-once: Must be a valid UUID if set. +# If set, then this endpoint expects to receive a non-preflight request first, +# for which it sets the `Content-Security-Policy: treat-as-public-address` +# response header. This allows testing "DNS rebinding", where a URL first +# resolves to the public IP address space, then a non-public IP address space. +# - preflight-uuid: Must be a valid UUID if set, distinct from the value of the +# `treat-as-public-once` parameter if both are set. +# If set, then this endpoint expects to receive a preflight request first +# followed by a regular request, as in the regular CORS protocol. If the +# `treat-as-public-once` header is also set, it takes precedence: this +# endpoint expects to receive a non-preflight request first, then a preflight +# request, then finally a regular request. +# If unset, then this endpoint expects to receive no preflight request, only +# a regular (non-OPTIONS) request. +# - preflight-headers: Valid values are: +# - cors: this endpoint responds with valid CORS headers to preflights. These +# should be sufficient for non-PNA preflight requests to succeed, but not +# for PNA-specific preflight requests. +# - cors+pna: this endpoint responds with valid CORS and PNA headers to +# preflights. These should be sufficient for both non-PNA preflight +# requests and PNA-specific preflight requests to succeed. +# - cors+pna+sw: this endpoint responds with valid CORS and PNA headers and +# "Access-Control-Allow-Headers: Service-Worker" to preflights. These should +# be sufficient for both non-PNA preflight requests and PNA-specific +# preflight requests to succeed. This allows the main request to fetch a +# service worker script. +# - unspecified, or any other value: this endpoint responds with no CORS or +# PNA headers. Preflight requests should fail. +# - final-headers: Valid values are: +# - cors: this endpoint responds with valid CORS headers to CORS-enabled +# non-preflight requests. These should be sufficient for non-preflighted +# CORS-enabled requests to succeed. +# - unspecified: this endpoint responds with no CORS headers to non-preflight +# requests. This should fail CORS-enabled requests, but be sufficient for +# no-CORS requests. +# +# The following parameters only affect non-preflight responses: +# +# - redirect: If set, the response code is set to 301 and the `Location` +# response header is set to this value. +# - mime-type: If set, the `Content-Type` response header is set to this value. +# - file: Specifies a path (relative to this file's directory) to a file. If +# set, the response body is copied from this file. +# - random-js-prefix: If set to any value, the response body is prefixed with +# a Javascript comment line containing a random value. This is useful in +# service worker tests, since service workers are only updated if the new +# script is not byte-for-byte identical with the old script. +# - body: If set and `file` is not, the response body is set to this value. +# + +import os +import random + +from wptserve.utils import isomorphic_encode + +_ACAO = ("Access-Control-Allow-Origin", "*") +_ACAPN = ("Access-Control-Allow-Private-Network", "true") +_ACAH = ("Access-Control-Allow-Headers", "Service-Worker") + +def _get_response_headers(method, mode, origin): + acam = ("Access-Control-Allow-Methods", method) + + if mode == b"cors": + return [acam, _ACAO] + + if mode == b"cors+pna": + return [acam, _ACAO, _ACAPN] + + if mode == b"cors+pna+sw": + return [acam, _ACAO, _ACAPN, _ACAH] + + if mode == b"navigation": + return [ + acam, + ("Access-Control-Allow-Origin", origin), + ("Access-Control-Allow-Credentials", "true"), + _ACAPN, + ] + + return [] + +def _get_expect_single_preflight(request): + return request.GET.get(b"expect-single-preflight") + +def _is_preflight_optional(request): + return request.GET.get(b"is-preflight-optional") or \ + request.GET.get(b"file-if-no-preflight-received") + +def _get_preflight_uuid(request): + return request.GET.get(b"preflight-uuid") + +def _is_loaded_in_fenced_frame(request): + return request.GET.get(b"is-loaded-in-fenced-frame") + +def _should_treat_as_public_once(request): + uuid = request.GET.get(b"treat-as-public-once") + if uuid is None: + # If the search parameter is not given, never treat as public. + return False + + # If the parameter is given, we treat the request as public only if the UUID + # has never been seen and stashed. + result = request.server.stash.take(uuid) is None + request.server.stash.put(uuid, "") + return result + +def _handle_preflight_request(request, response): + if _should_treat_as_public_once(request): + return (400, [], "received preflight for first treat-as-public request") + + uuid = _get_preflight_uuid(request) + if uuid is None: + return (400, [], "missing `preflight-uuid` param from preflight URL") + + value = request.server.stash.take(uuid) + request.server.stash.put(uuid, "preflight") + if _get_expect_single_preflight(request) and value is not None: + return (400, [], "received duplicated preflight") + + method = request.headers.get("Access-Control-Request-Method") + mode = request.GET.get(b"preflight-headers") + origin = request.headers.get("Origin") + headers = _get_response_headers(method, mode, origin) + + return (headers, "preflight") + +def _final_response_body(request, missing_preflight): + file_name = None + if missing_preflight and not request.GET.get(b"is-preflight-optional"): + file_name = request.GET.get(b"file-if-no-preflight-received") + if file_name is None: + file_name = request.GET.get(b"file") + if file_name is None: + return request.GET.get(b"body") or "success" + + prefix = b"" + if request.GET.get(b"random-js-prefix"): + value = random.randint(0, 1000000000) + prefix = isomorphic_encode("// Random value: {}\n\n".format(value)) + + path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), file_name) + with open(path, 'rb') as f: + contents = f.read() + + return prefix + contents + +def _handle_final_request(request, response): + missing_preflight = False + if _should_treat_as_public_once(request): + headers = [("Content-Security-Policy", "treat-as-public-address"),] + else: + uuid = _get_preflight_uuid(request) + if uuid is not None: + missing_preflight = request.server.stash.take(uuid) is None + if missing_preflight and not _is_preflight_optional(request): + return (405, [], "no preflight received") + request.server.stash.put(uuid, "final") + + mode = request.GET.get(b"final-headers") + origin = request.headers.get("Origin") + headers = _get_response_headers(request.method, mode, origin) + + redirect = request.GET.get(b"redirect") + if redirect is not None: + headers.append(("Location", redirect)) + return (301, headers, b"") + + mime_type = request.GET.get(b"mime-type") + if mime_type is not None: + headers.append(("Content-Type", mime_type),) + + if _is_loaded_in_fenced_frame(request): + headers.append(("Supports-Loading-Mode", "fenced-frame")) + + body = _final_response_body(request, missing_preflight) + return (headers, body) + +def main(request, response): + try: + if request.method == "OPTIONS": + return _handle_preflight_request(request, response) + else: + return _handle_final_request(request, response) + except BaseException as e: + # Surface exceptions to the client, where they show up as assertion errors. + return (500, [("X-exception", str(e))], "exception: {}".format(e)) diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/service-worker-bridge.html b/testing/web-platform/tests/fetch/private-network-access/resources/service-worker-bridge.html new file mode 100644 index 0000000000..816de535fe --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/service-worker-bridge.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>ServiceWorker Bridge</title> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + // This bridge document exists to perform service worker commands on behalf + // of a test page. It lives within the same scope (including origin) as the + // service worker script, allowing it to be controlled by the service worker. + + async function register({ url, options }) { + await navigator.serviceWorker.register(url, options); + return { loaded: true }; + } + + async function unregister({ scope }) { + const registration = await navigator.serviceWorker.getRegistration(scope); + if (!registration) { + return { unregistered: false, error: "no registration" }; + } + + const unregistered = await registration.unregister(); + return { unregistered }; + } + + async function update({ scope }) { + const registration = await navigator.serviceWorker.getRegistration(scope); + if (!registration) { + return { updated: false, error: "no registration" }; + } + + const newRegistration = await registration.update(); + return { updated: true }; + } + + // Total number of `controllerchange` events since document creation. + let totalNumControllerChanges = 0; + navigator.serviceWorker.addEventListener("controllerchange", () => { + totalNumControllerChanges++; + }); + + // Using `navigator.serviceWorker.ready` does not allow noticing new + // controllers after an update, so we count `controllerchange` events instead. + // This has the added benefit of ensuring that subsequent fetches are handled + // by the service worker, whereas `ready` does not guarantee that. + async function wait({ numControllerChanges }) { + if (totalNumControllerChanges >= numControllerChanges) { + return { + controlled: !!navigator.serviceWorker.controller, + numControllerChanges: totalNumControllerChanges, + }; + } + + let remaining = numControllerChanges - totalNumControllerChanges; + await new Promise((resolve) => { + navigator.serviceWorker.addEventListener("controllerchange", () => { + remaining--; + if (remaining == 0) { + resolve(); + } + }); + }); + + return { + controlled: !!navigator.serviceWorker.controller, + numControllerChanges, + }; + } + + async function doFetch({ url, options }) { + const response = await fetch(url, options); + const body = await response.text(); + return { + ok: response.ok, + body, + }; + } + + async function setPermission({ name, state }) { + await test_driver.set_permission({ name }, state); + + // Double-check, just to be sure. + // See the comment in `../service-worker-background-fetch.js`. + const permissionStatus = await navigator.permissions.query({ name }); + return { state: permissionStatus.state }; + } + + async function backgroundFetch({ scope, url }) { + const registration = await navigator.serviceWorker.getRegistration(scope); + if (!registration) { + return { error: "no registration" }; + } + + const fetchRegistration = + await registration.backgroundFetch.fetch("test", url); + const resultReady = new Promise((resolve) => { + fetchRegistration.addEventListener("progress", () => { + if (fetchRegistration.result) { + resolve(); + } + }); + }); + + let ok; + let body; + const record = await fetchRegistration.match(url); + if (record) { + const response = await record.responseReady; + body = await response.text(); + ok = response.ok; + } + + // Wait for the result after getting the response. If the steps are + // inverted, then sometimes the response is not found due to an + // `UnknownError`. + await resultReady; + + return { + result: fetchRegistration.result, + failureReason: fetchRegistration.failureReason, + ok, + body, + }; + } + + function getAction(action) { + switch (action) { + case "register": + return register; + case "unregister": + return unregister; + case "wait": + return wait; + case "update": + return update; + case "fetch": + return doFetch; + case "set-permission": + return setPermission; + case "background-fetch": + return backgroundFetch; + } + } + + window.addEventListener("message", async (evt) => { + let message; + try { + const action = getAction(evt.data.action); + message = await action(evt.data); + } catch(e) { + message = { error: e.name }; + } + parent.postMessage(message, "*"); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/service-worker.js b/testing/web-platform/tests/fetch/private-network-access/resources/service-worker.js new file mode 100644 index 0000000000..bca71ad910 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/service-worker.js @@ -0,0 +1,18 @@ +self.addEventListener("install", () => { + // Skip waiting before replacing the previously-active service worker, if any. + // This allows the bridge script to notice the controller change and query + // the install time via fetch. + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + // Claim all clients so that the bridge script notices the activation. + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url).searchParams.get("proxied-url"); + if (url) { + event.respondWith(fetch(url)); + } +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/shared-fetcher.js b/testing/web-platform/tests/fetch/private-network-access/resources/shared-fetcher.js new file mode 100644 index 0000000000..30bde1e054 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/shared-fetcher.js @@ -0,0 +1,23 @@ +async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; +} + +async function fetchAndPost(url, port) { + try { + const message = await doFetch(url); + port.postMessage(message); + } catch(e) { + port.postMessage({ error: e.name }); + } +} + +const url = new URL(self.location.href).searchParams.get("url"); + +self.addEventListener("connect", async (evt) => { + await fetchAndPost(url, evt.ports[0]); +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html b/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html new file mode 100644 index 0000000000..a79869b2f9 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>SharedWorker Blob Fetcher</title> +<script> + window.addEventListener("message", function (evt) { + let { url } = evt.data; + + const workerScriptContent = ` + async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; + } + + async function fetchAndPost(url, port) { + try { + const message = await doFetch(url); + port.postMessage(message); + } catch(e) { + port.postMessage({ error: e.name }); + } + } + + const url = "${url}"; + + self.addEventListener("connect", async (evt) => { + await fetchAndPost(url, evt.ports[0]); + }); + `; + const blob = + new Blob([workerScriptContent], {type: 'application/javascript'}); + const workerScriptUrl = URL.createObjectURL(blob); + + const worker = new SharedWorker(workerScriptUrl); + + URL.revokeObjectURL(workerScriptUrl); + + worker.onerror = (evt) => { + parent.postMessage({ error: evt.message || "unknown error" }, "*"); + }; + + worker.port.addEventListener("message", (evt) => { + parent.postMessage(evt.data, "*"); + }); + worker.port.start(); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-fetcher.html b/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-fetcher.html new file mode 100644 index 0000000000..4af4b1f239 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/shared-worker-fetcher.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>SharedWorker Fetcher</title> +<script> + window.addEventListener("message", function (evt) { + let { url } = evt.data; + + const worker = new SharedWorker(url); + + worker.onerror = (evt) => { + parent.postMessage({ error: evt.message || "unknown error" }, "*"); + }; + + worker.port.addEventListener("message", (evt) => { + parent.postMessage(evt.data, "*"); + }); + worker.port.start(); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/socket-opener.html b/testing/web-platform/tests/fetch/private-network-access/resources/socket-opener.html new file mode 100644 index 0000000000..48d27216be --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/socket-opener.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>WebSocket Opener</title> +<script> + window.addEventListener("message", function (event) { + const socket = new WebSocket(event.data); + + socket.onopen = () => { + parent.postMessage("open", "*"); + }; + socket.onclose = (evt) => { + parent.postMessage(`close: code ${evt.code}`, "*"); + }; + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/support.sub.js b/testing/web-platform/tests/fetch/private-network-access/resources/support.sub.js new file mode 100644 index 0000000000..46a9d9e076 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/support.sub.js @@ -0,0 +1,857 @@ +// Creates a new iframe in `doc`, calls `func` on it and appends it as a child +// of `doc`. +// Returns a promise that resolves to the iframe once loaded (successfully or +// not). +// The iframe is removed from `doc` once test `t` is done running. +// +// NOTE: There exists no interoperable way to check whether an iframe failed to +// load, so this should only be used when the iframe is expected to load. It +// also means we cannot wire the iframe's `error` event to a promise +// rejection. See: https://github.com/whatwg/html/issues/125 +function appendIframeWith(t, doc, func) { + return new Promise(resolve => { + const child = doc.createElement("iframe"); + t.add_cleanup(() => child.remove()); + + child.addEventListener("load", () => resolve(child), { once: true }); + func(child); + doc.body.appendChild(child); + }); +} + +// Appends a child iframe to `doc` sourced from `src`. +// +// See `appendIframeWith()` for more details. +function appendIframe(t, doc, src) { + return appendIframeWith(t, doc, child => { child.src = src; }); +} + +// Registers an event listener that will resolve this promise when this +// window receives a message posted to it. +// +// `options` has the following shape: +// +// { +// source: If specified, this function waits for the first message from the +// given source only, ignoring other messages. +// +// filter: If specified, this function calls `filter` on each incoming +// message, and resolves iff it returns true. +// } +// +function futureMessage(options) { + return new Promise(resolve => { + window.addEventListener("message", (e) => { + if (options?.source && options.source !== e.source) { + return; + } + + if (options?.filter && !options.filter(e.data)) { + return; + } + + resolve(e.data); + }); + }); +}; + +// Like `promise_test()`, but executes tests in parallel like `async_test()`. +// +// Cribbed from COEP tests. +function promise_test_parallel(promise, description) { + async_test(test => { + promise(test) + .then(() => test.done()) + .catch(test.step_func(error => { throw error; })); + }, description); +}; + +async function postMessageAndAwaitReply(target, message) { + const reply = futureMessage({ source: target }); + target.postMessage(message, "*"); + return await reply; +} + +// Maps protocol (without the trailing colon) and address space to port. +const SERVER_PORTS = { + "http": { + "local": {{ports[http][0]}}, + "private": {{ports[http-private][0]}}, + "public": {{ports[http-public][0]}}, + }, + "https": { + "local": {{ports[https][0]}}, + "other-local": {{ports[https][1]}}, + "private": {{ports[https-private][0]}}, + "public": {{ports[https-public][0]}}, + }, + "ws": { + "local": {{ports[ws][0]}}, + }, + "wss": { + "local": {{ports[wss][0]}}, + }, +}; + +// A `Server` is a web server accessible by tests. It has the following shape: +// +// { +// addressSpace: the IP address space of the server ("local", "private" or +// "public"), +// name: a human-readable name for the server, +// port: the port on which the server listens for connections, +// protocol: the protocol (including trailing colon) spoken by the server, +// } +// +// Constants below define the available servers, which can also be accessed +// programmatically with `get()`. +class Server { + // Maps the given `protocol` (without a trailing colon) and `addressSpace` to + // a server. Returns null if no such server exists. + static get(protocol, addressSpace) { + const ports = SERVER_PORTS[protocol]; + if (ports === undefined) { + return null; + } + + const port = ports[addressSpace]; + if (port === undefined) { + return null; + } + + return { + addressSpace, + name: `${protocol}-${addressSpace}`, + port, + protocol: protocol + ':', + }; + } + + static HTTP_LOCAL = Server.get("http", "local"); + static HTTP_PRIVATE = Server.get("http", "private"); + static HTTP_PUBLIC = Server.get("http", "public"); + static HTTPS_LOCAL = Server.get("https", "local"); + static OTHER_HTTPS_LOCAL = Server.get("https", "other-local"); + static HTTPS_PRIVATE = Server.get("https", "private"); + static HTTPS_PUBLIC = Server.get("https", "public"); + static WS_LOCAL = Server.get("ws", "local"); + static WSS_LOCAL = Server.get("wss", "local"); +}; + +// Resolves a URL relative to the current location, returning an absolute URL. +// +// `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". +// `options`, if defined, should have the following shape: +// +// { +// // Optional. Overrides the protocol of the returned URL. +// protocol, +// +// // Optional. Overrides the port of the returned URL. +// port, +// +// // Extra headers. +// headers, +// +// // Extra search params. +// searchParams, +// } +// +function resolveUrl(url, options) { + const result = new URL(url, window.location); + if (options === undefined) { + return result; + } + + const { port, protocol, headers, searchParams } = options; + if (port !== undefined) { + result.port = port; + } + if (protocol !== undefined) { + result.protocol = protocol; + } + if (headers !== undefined) { + const pipes = []; + for (key in headers) { + pipes.push(`header(${key},${headers[key]})`); + } + result.searchParams.append("pipe", pipes.join("|")); + } + if (searchParams !== undefined) { + for (key in searchParams) { + result.searchParams.append(key, searchParams[key]); + } + } + + return result; +} + +// Computes options to pass to `resolveUrl()` for a source document's URL. +// +// `server` identifies the server from which to load the document. +// `treatAsPublic`, if set to true, specifies that the source document should +// be artificially placed in the `public` address space using CSP. +function sourceResolveOptions({ server, treatAsPublic }) { + const options = {...server}; + if (treatAsPublic) { + options.headers = { "Content-Security-Policy": "treat-as-public-address" }; + } + return options; +} + +// Computes the URL of a preflight handler configured with the given options. +// +// `server` identifies the server from which to load the resource. +// `behavior` specifies the behavior of the target server. It may contain: +// - `preflight`: The result of calling one of `PreflightBehavior`'s methods. +// - `response`: The result of calling one of `ResponseBehavior`'s methods. +// - `redirect`: A URL to which the target should redirect GET requests. +function preflightUrl({ server, behavior }) { + assert_not_equals(server, undefined, 'server'); + const options = {...server}; + if (behavior) { + const { preflight, response, redirect } = behavior; + options.searchParams = { + ...preflight, + ...response, + }; + if (redirect !== undefined) { + options.searchParams.redirect = redirect; + } + } + + return resolveUrl("resources/preflight.py", options); +} + +// Methods generate behavior specifications for how `resources/preflight.py` +// should behave upon receiving a preflight request. +const PreflightBehavior = { + // The preflight response should fail with a non-2xx code. + failure: () => ({}), + + // The preflight response should be missing CORS headers. + // `uuid` should be a UUID that uniquely identifies the preflight request. + noCorsHeader: (uuid) => ({ + "preflight-uuid": uuid, + }), + + // The preflight response should be missing PNA headers. + // `uuid` should be a UUID that uniquely identifies the preflight request. + noPnaHeader: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors", + }), + + // The preflight response should succeed. + // `uuid` should be a UUID that uniquely identifies the preflight request. + success: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + }), + + optionalSuccess: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + "is-preflight-optional": true, + }), + + // The preflight response should succeed and allow service-worker header. + // `uuid` should be a UUID that uniquely identifies the preflight request. + serviceWorkerSuccess: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna+sw", + }), + + // The preflight response should succeed only if it is the first preflight. + // `uuid` should be a UUID that uniquely identifies the preflight request. + singlePreflight: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + "expect-single-preflight": true, + }), + + // The preflight response should succeed and allow origins and headers for + // navigations. + navigation: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "navigation", + }), +}; + +// Methods generate behavior specifications for how `resources/preflight.py` +// should behave upon receiving a regular (non-preflight) request. +const ResponseBehavior = { + // The response should succeed without CORS headers. + default: () => ({}), + + // The response should succeed with CORS headers. + allowCrossOrigin: () => ({ "final-headers": "cors" }), +}; + +const FetchTestResult = { + SUCCESS: { + ok: true, + body: "success", + }, + OPAQUE: { + ok: false, + type: "opaque", + body: "", + }, + FAILURE: { + error: "TypeError: Failed to fetch", + }, +}; + +// Runs a fetch test. Tries to fetch a given subresource from a given document. +// +// Main argument shape: +// +// { +// // Optional. Passed to `sourceResolveOptions()`. +// source, +// +// // Optional. Passed to `preflightUrl()`. +// target, +// +// // Optional. Passed to `fetch()`. +// fetchOptions, +// +// // Required. One of the values in `FetchTestResult`. +// expected, +// } +// +async function fetchTest(t, { source, target, fetchOptions, expected }) { + const sourceUrl = + resolveUrl("resources/fetcher.html", sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage({ source: iframe.contentWindow }); + + const message = { + url: targetUrl.href, + options: fetchOptions, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { error, ok, type, body } = await reply; + + assert_equals(error, expected.error, "error"); + + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + + if (expected.type !== undefined) { + assert_equals(type, expected.type, "response type"); + } +} + +// Similar to `fetchTest`, but replaced iframes with fenced frames. +async function fencedFrameFetchTest(t, { source, target, fetchOptions, expected }) { + const fetcher_url = + resolveUrl("resources/fenced-frame-fetcher.https.html", sourceResolveOptions(source)); + + const target_url = preflightUrl(target); + target_url.searchParams.set("is-loaded-in-fenced-frame", true); + + fetcher_url.searchParams.set("mode", fetchOptions.mode); + fetcher_url.searchParams.set("method", fetchOptions.method); + fetcher_url.searchParams.set("url", target_url); + + const error_token = token(); + const ok_token = token(); + const body_token = token(); + const type_token = token(); + const source_url = generateURL(fetcher_url, [error_token, ok_token, body_token, type_token]); + + const urn = await generateURNFromFledge(source_url, []); + attachFencedFrame(urn); + + const error = await nextValueFromServer(error_token); + const ok = await nextValueFromServer(ok_token); + const body = await nextValueFromServer(body_token); + const type = await nextValueFromServer(type_token); + + assert_equals(error, expected.error || "" , "error"); + assert_equals(body, expected.body || "", "response body"); + assert_equals(ok, expected.ok !== undefined ? expected.ok.toString() : "", "response ok"); + if (expected.type !== undefined) { + assert_equals(type, expected.type, "response type"); + } +} + +const XhrTestResult = { + SUCCESS: { + loaded: true, + status: 200, + body: "success", + }, + FAILURE: { + loaded: false, + status: 0, + }, +}; + +// Runs an XHR test. Tries to fetch a given subresource from a given document. +// +// Main argument shape: +// +// { +// // Optional. Passed to `sourceResolveOptions()`. +// source, +// +// // Optional. Passed to `preflightUrl()`. +// target, +// +// // Optional. Method to use when sending the request. Defaults to "GET". +// method, +// +// // Required. One of the values in `XhrTestResult`. +// expected, +// } +// +async function xhrTest(t, { source, target, method, expected }) { + const sourceUrl = + resolveUrl("resources/xhr-sender.html", sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + const message = { + url: targetUrl.href, + method: method, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { loaded, status, body } = await reply; + + assert_equals(loaded, expected.loaded, "response loaded"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} + +const FrameTestResult = { + SUCCESS: "loaded", + FAILURE: "timeout", +}; + +async function iframeTest(t, { source, target, expected }) { + // Allows running tests in parallel. + const uuid = token(); + + const targetUrl = preflightUrl(target); + targetUrl.searchParams.set("file", "iframed.html"); + targetUrl.searchParams.set("iframe-uuid", uuid); + targetUrl.searchParams.set( + "file-if-no-preflight-received", + "iframed-no-preflight-received.html", + ); + + const sourceUrl = + resolveUrl("resources/iframer.html", sourceResolveOptions(source)); + sourceUrl.searchParams.set("url", targetUrl); + + const messagePromise = futureMessage({ + filter: (data) => data.uuid === uuid, + }); + const iframe = await appendIframe(t, document, sourceUrl); + + // The grandchild frame posts a message iff it loads successfully. + // There exists no interoperable way to check whether an iframe failed to + // load, so we use a timeout. + // See: https://github.com/whatwg/html/issues/125 + const result = await Promise.race([ + messagePromise.then((data) => data.message), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 2000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +const NavigationTestResult = { + SUCCESS: "success", + FAILURE: "timeout", +}; + +async function windowOpenTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + targetUrl.searchParams.set("file", "openee.html"); + targetUrl.searchParams.set( + "file-if-no-preflight-received", + "no-preflight-received.html", + ); + + const sourceUrl = + resolveUrl("resources/opener.html", sourceResolveOptions(source)); + sourceUrl.searchParams.set("url", targetUrl); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage({ source: iframe.contentWindow }); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const result = await Promise.race([ + reply, + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 10000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +async function windowOpenExistingTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + targetUrl.searchParams.set("file", "openee.html"); + targetUrl.searchParams.set( + "file-if-no-preflight-received", + "no-preflight-received.html", + ); + + const sourceUrl = resolveUrl( + 'resources/open-to-existing-window.html', sourceResolveOptions(source)); + sourceUrl.searchParams.set("url", targetUrl); + sourceUrl.searchParams.set("token", token()); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage({ source: iframe.contentWindow }); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const result = await Promise.race([ + reply, + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 10000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +async function anchorTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + targetUrl.searchParams.set("file", "openee.html"); + targetUrl.searchParams.set( + "file-if-no-preflight-received", + "no-preflight-received.html", + ); + + const sourceUrl = + resolveUrl("resources/anchor.html", sourceResolveOptions(source)); + sourceUrl.searchParams.set("url", targetUrl); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage({ source: iframe.contentWindow }); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const result = await Promise.race([ + reply, + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 10000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +// Similar to `iframeTest`, but replaced iframes with fenced frames. +async function fencedFrameTest(t, { source, target, expected }) { + // Allows running tests in parallel. + const target_url = preflightUrl(target); + target_url.searchParams.set("file", "fenced-frame-private-network-access-target.https.html"); + target_url.searchParams.set("is-loaded-in-fenced-frame", true); + + const frame_loaded_key = token(); + const child_frame_target = generateURL(target_url, [frame_loaded_key]); + + const source_url = + resolveUrl("resources/fenced-frame-private-network-access.https.html", sourceResolveOptions(source)); + source_url.searchParams.set("fenced_frame_url", child_frame_target); + + const urn = await generateURNFromFledge(source_url, []); + attachFencedFrame(urn); + + // The grandchild fenced frame writes a value to the server iff it loads + // successfully. + const result = (expected == FrameTestResult.SUCCESS) ? + await nextValueFromServer(frame_loaded_key) : + await Promise.race([ + nextValueFromServer(frame_loaded_key), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 10000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +const iframeGrandparentTest = ({ + name, + grandparentServer, + child, + grandchild, + expected, +}) => promise_test_parallel(async (t) => { + // Allows running tests in parallel. + const grandparentUuid = token(); + const childUuid = token(); + const grandchildUuid = token(); + + const grandparentUrl = + resolveUrl("resources/executor.html", grandparentServer); + grandparentUrl.searchParams.set("executor-uuid", grandparentUuid); + + const childUrl = preflightUrl(child); + childUrl.searchParams.set("file", "executor.html"); + childUrl.searchParams.set("executor-uuid", childUuid); + + const grandchildUrl = preflightUrl(grandchild); + grandchildUrl.searchParams.set("file", "iframed.html"); + grandchildUrl.searchParams.set("iframe-uuid", grandchildUuid); + + const iframe = await appendIframe(t, document, grandparentUrl); + + const addChild = (url) => new Promise((resolve) => { + const child = document.createElement("iframe"); + child.src = url; + child.addEventListener("load", () => resolve(), { once: true }); + document.body.appendChild(child); + }); + + const grandparentCtx = new RemoteContext(grandparentUuid); + await grandparentCtx.execute_script(addChild, [childUrl]); + + // Add a blank grandchild frame inside the child. + // Apply a timeout to this step so that failures at this step do not block the + // execution of other tests. + const childCtx = new RemoteContext(childUuid); + await Promise.race([ + childCtx.execute_script(addChild, ["about:blank"]), + new Promise((resolve, reject) => t.step_timeout( + () => reject("timeout adding grandchild"), + 2000 /* ms */ + )), + ]); + + const messagePromise = futureMessage({ + filter: (data) => data.uuid === grandchildUuid, + }); + await grandparentCtx.execute_script((url) => { + const child = window.frames[0]; + const grandchild = child.frames[0]; + grandchild.location = url; + }, [grandchildUrl]); + + // The great-grandchild frame posts a message iff it loads successfully. + // There exists no interoperable way to check whether an iframe failed to + // load, so we use a timeout. + // See: https://github.com/whatwg/html/issues/125 + const result = await Promise.race([ + messagePromise.then((data) => data.message), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 2000 /* ms */); + }), + ]); + + assert_equals(result, expected); +}, name); + +const WebsocketTestResult = { + SUCCESS: "open", + + // The code is a best guess. It is not yet entirely specified, so it may need + // to be changed in the future based on implementation experience. + FAILURE: "close: code 1006", +}; + +// Runs a websocket test. Attempts to open a websocket from `source` (in an +// iframe) to `target`, then checks that the result is as `expected`. +// +// Argument shape: +// +// { +// // Required. Passed to `sourceResolveOptions()`. +// source, +// +// // Required. +// target: { +// // Required. Target server. +// server, +// } +// +// // Required. Should be one of the values in `WebsocketTestResult`. +// expected, +// } +// +async function websocketTest(t, { source, target, expected }) { + const sourceUrl = + resolveUrl("resources/socket-opener.html", sourceResolveOptions(source)); + + const targetUrl = resolveUrl("/echo", target.server); + + const iframe = await appendIframe(t, document, sourceUrl); + + const reply = futureMessage(); + iframe.contentWindow.postMessage(targetUrl.href, "*"); + + assert_equals(await reply, expected); +} + +const WorkerScriptTestResult = { + SUCCESS: { loaded: true }, + FAILURE: { error: "unknown error" }, +}; + +function workerScriptUrl(target) { + const url = preflightUrl(target); + + url.searchParams.append("body", "postMessage({ loaded: true })") + url.searchParams.append("mime-type", "application/javascript") + + return url; +} + +async function workerScriptTest(t, { source, target, expected }) { + const sourceUrl = + resolveUrl("resources/worker-fetcher.html", sourceResolveOptions(source)); + + const targetUrl = workerScriptUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +async function nestedWorkerScriptTest(t, { source, target, expected }) { + const targetUrl = workerScriptUrl(target); + + const sourceUrl = resolveUrl( + "resources/worker-fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl); + + // Iframe must be same-origin with the parent worker. + const iframeUrl = new URL("worker-fetcher.html", sourceUrl); + + const iframe = await appendIframe(t, document, iframeUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +async function sharedWorkerScriptTest(t, { source, target, expected }) { + const sourceUrl = resolveUrl("resources/shared-worker-fetcher.html", + sourceResolveOptions(source)); + const targetUrl = preflightUrl(target); + targetUrl.searchParams.append( + "body", "onconnect = (e) => e.ports[0].postMessage({ loaded: true })") + targetUrl.searchParams.append("mime-type", "application/javascript") + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +// Results that may be expected in tests. +const WorkerFetchTestResult = { + SUCCESS: { status: 200, body: "success" }, + FAILURE: { error: "TypeError" }, +}; + +async function workerFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const sourceUrl = + resolveUrl("resources/fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl.href); + + const fetcherUrl = new URL("worker-fetcher.html", sourceUrl); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, status, body } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} + +async function workerBlobFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const fetcherUrl = resolveUrl( + 'resources/worker-blob-fetcher.html', sourceResolveOptions(source)); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, status, body } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} + +async function sharedWorkerFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const sourceUrl = + resolveUrl("resources/shared-fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl.href); + + const fetcherUrl = new URL("shared-worker-fetcher.html", sourceUrl); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, status, body } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} + +async function sharedWorkerBlobFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const fetcherUrl = resolveUrl( + 'resources/shared-worker-blob-fetcher.html', + sourceResolveOptions(source)); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, status, body } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/worker-blob-fetcher.html b/testing/web-platform/tests/fetch/private-network-access/resources/worker-blob-fetcher.html new file mode 100644 index 0000000000..5a50271e11 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/worker-blob-fetcher.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Worker Blob Fetcher</title> +<script> + window.addEventListener("message", function (evt) { + const { url } = evt.data; + + const workerScriptContent = ` + async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; + } + + async function fetchAndPost(url) { + try { + const message = await doFetch(url); + self.postMessage(message); + } catch(e) { + self.postMessage({ error: e.name }); + } + } + + fetchAndPost("${url}"); + `; + const blob = + new Blob([workerScriptContent], {type: 'application/javascript'}); + const workerScriptUrl = URL.createObjectURL(blob); + + const worker = new Worker(workerScriptUrl); + + URL.revokeObjectURL(workerScriptUrl); + + worker.addEventListener("message", (evt) => { + parent.postMessage(evt.data, "*"); + }); + + worker.addEventListener("error", (evt) => { + parent.postMessage({ error: evt.message || "unknown error" }, "*"); + }); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.html b/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.html new file mode 100644 index 0000000000..bd155a532b --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Worker Fetcher</title> +<script> + window.addEventListener("message", function (evt) { + let { url } = evt.data; + + const worker = new Worker(url); + + worker.addEventListener("message", (evt) => { + parent.postMessage(evt.data, "*"); + }); + + worker.addEventListener("error", (evt) => { + parent.postMessage({ error: evt.message || "unknown error" }, "*"); + }); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.js b/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.js new file mode 100644 index 0000000000..aab49afe6f --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/worker-fetcher.js @@ -0,0 +1,11 @@ +const url = new URL(self.location).searchParams.get("url"); +const worker = new Worker(url); + +// Relay messages from the worker to the parent frame. +worker.addEventListener("message", (evt) => { + self.postMessage(evt.data); +}); + +worker.addEventListener("error", (evt) => { + self.postMessage({ error: evt.message || "unknown error" }); +}); diff --git a/testing/web-platform/tests/fetch/private-network-access/resources/xhr-sender.html b/testing/web-platform/tests/fetch/private-network-access/resources/xhr-sender.html new file mode 100644 index 0000000000..b131fa41f9 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/resources/xhr-sender.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>XHR Sender</title> +<script> + window.addEventListener("message", function (event) { + let { url, method } = event.data; + if (!method) { + method = "GET"; + } + + const xhr = new XMLHttpRequest; + + xhr.addEventListener("load", (evt) => { + const message = { + loaded: true, + status: xhr.status, + body: xhr.responseText, + }; + parent.postMessage(message, "*"); + }); + + xhr.addEventListener("error", (evt) => { + const message = { + loaded: false, + status: xhr.status, + }; + parent.postMessage(message, "*"); + }); + + xhr.open(method, url); + xhr.send(); + }); +</script> diff --git a/testing/web-platform/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js new file mode 100644 index 0000000000..8d1028cc5e --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js @@ -0,0 +1,143 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// Spec: https://wicg.github.io/background-fetch/ +// +// These tests check that background fetches from within `ServiceWorker` scripts +// are not subject to Private Network Access checks. + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { ok: true, body: "success", result: "success", failureReason: "" }, +}; + +async function makeTest(t, { source, target, expected }) { + const scriptUrl = + resolveUrl("resources/service-worker.js", sourceResolveOptions(source)); + + const bridgeUrl = new URL("service-worker-bridge.html", scriptUrl); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + { + const { error, state } = await request({ + action: "set-permission", + name: "background-fetch", + state: "granted", + }); + + assert_equals(error, undefined, "set permission error"); + assert_equals(state, "granted", "permission state"); + } + + { + const { error, result, failureReason, ok, body } = await request({ + action: "background-fetch", + url: targetUrl.href, + }); + + assert_equals(error, expected.error, "error"); + assert_equals(failureReason, expected.failureReason, "fetch failure reason"); + assert_equals(result, expected.result, "fetch result"); + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + } +} + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js new file mode 100644 index 0000000000..cb6d1f79b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js @@ -0,0 +1,235 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/common/subset-tests.js +// META: variant=?1-8 +// META: variant=?9-last +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `ServiceWorker` scripts are +// subject to Private Network Access checks, just like fetches from within +// documents. + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { ok: true, body: "success" }, + FAILURE: { error: "TypeError" }, +}; + +async function makeTest(t, { source, target, expected }) { + const bridgeUrl = resolveUrl( + "resources/service-worker-bridge.html", + sourceResolveOptions({ server: source.server })); + + const scriptUrl = + resolveUrl("resources/service-worker.js", sourceResolveOptions(source)); + + const realTargetUrl = preflightUrl(target); + + // Fetch a URL within the service worker's scope, but tell it which URL to + // really fetch. + const targetUrl = new URL("service-worker-proxy", scriptUrl); + targetUrl.searchParams.append("proxied-url", realTargetUrl.href); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + try { + const { controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 1, + }); + + assert_equals(numControllerChanges, 1, "controller change"); + assert_true(controlled, "bridge script is controlled"); + + const { error, ok, body } = await request({ + action: "fetch", + url: targetUrl.href, + }); + + assert_equals(error, expected.error, "fetch error"); + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + } finally { + // Always unregister the service worker. + const { error, unregistered } = await request({ + action: "unregister", + scope: new URL("./", scriptUrl).href, + }); + + assert_equals(error, undefined, "unregister error"); + assert_true(unregistered, "unregistered"); + } +} + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "local to local: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "private to local: failed preflight."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "private to local: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.SUCCESS, +}), "private to private: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "public to local: failed preflight."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "public to local: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "public to private: failed preflight."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "public to private: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "public to public: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local (same-origin): no preflight required."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +subsetTest(promise_test, t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js new file mode 100644 index 0000000000..4882d235bb --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js @@ -0,0 +1,106 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that `ServiceWorker` script update fetches are exempt from +// Private Network Access checks because they are always same-origin and the +// origin is potentially trustworthy. The client of the fetch, for PNA purposes, +// is taken to be the previous script. +// +// The tests is carried out by instantiating a service worker from a resource +// that carries the `Content-Security-Policy: treat-as-public-address` header, +// such that the registration is placed in the public IP address space. When +// the script is fetched for an update, the client is thus considered public, +// yet the same-origin fetch observes that the server's IP endpoint is not +// necessarily in the public IP address space. +// +// See also: worker.https.window.js + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { updated: true }, + FAILURE: { error: "TypeError" }, +}; + +async function makeTest(t, { target, expected }) { + // The bridge must be same-origin with the service worker script. + const bridgeUrl = resolveUrl( + "resources/service-worker-bridge.html", + sourceResolveOptions({ server: target.server })); + + const scriptUrl = preflightUrl(target); + scriptUrl.searchParams.append("treat-as-public-once", token()); + scriptUrl.searchParams.append("mime-type", "application/javascript"); + scriptUrl.searchParams.append("file", "service-worker.js"); + scriptUrl.searchParams.append("random-js-prefix", true); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + try { + let { controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 1, + }); + + assert_equals(numControllerChanges, 1, "controller change"); + assert_true(controlled, "bridge script is controlled"); + + const { error, updated } = await request({ action: "update" }); + + assert_equals(error, expected.error, "update error"); + assert_equals(updated, expected.updated, "registration updated"); + + // Stop here if we do not expect the update to succeed. + if (!expected.updated) { + return; + } + + ({ controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 2, + })); + + assert_equals(numControllerChanges, 2, "controller change"); + assert_true(controlled, "bridge script still controlled"); + } finally { + const { error, unregistered } = await request({ + action: "unregister", + scope: new URL("./", scriptUrl).href, + }); + + assert_equals(error, undefined, "unregister error"); + assert_true(unregistered, "unregistered"); + } +} + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "update public to local: success."); + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.SUCCESS, +}), "update public to private: success."); + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "update public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/service-worker.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/service-worker.tentative.https.window.js new file mode 100644 index 0000000000..046f662a12 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/service-worker.tentative.https.window.js @@ -0,0 +1,84 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `ServiceWorker` script fetches are exempt from +// Private Network Access checks because they are always same-origin and the +// origin is potentially trustworthy. +// +// See also: worker.https.window.js + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { + register: { loaded: true }, + unregister: { unregistered: true }, + }, + FAILURE: { + register: { error: "TypeError" }, + unregister: { unregistered: false, error: "no registration" }, + }, +}; + +async function makeTest(t, { source, target, expected }) { + const sourceUrl = resolveUrl("resources/service-worker-bridge.html", + sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + targetUrl.searchParams.append("body", "undefined"); + targetUrl.searchParams.append("mime-type", "application/javascript"); + + const scope = resolveUrl(`resources/${token()}`, {...target.server}).href; + + const iframe = await appendIframe(t, document, sourceUrl); + + { + const reply = futureMessage(); + const message = { + action: "register", + url: targetUrl.href, + options: { scope }, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.register.error, "register error"); + assert_equals(loaded, expected.register.loaded, "response loaded"); + } + + { + const reply = futureMessage(); + iframe.contentWindow.postMessage({ action: "unregister", scope }, "*"); + + const { error, unregistered } = await reply; + assert_equals(error, expected.unregister.error, "unregister error"); + assert_equals( + unregistered, expected.unregister.unregistered, "worker unregistered"); + } +} + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js new file mode 100644 index 0000000000..269abb7edc --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js @@ -0,0 +1,168 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts that are +// loaded from blob URLs are subject to Private Network Access checks, just like +// fetches from within documents. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: shared-worker-blob-fetch.window.js + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failed preflight."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local (same-origin): no preflight required."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js new file mode 100644 index 0000000000..d430ea7383 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js @@ -0,0 +1,173 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts that are +// loaded from blob URLs are subject to Private Network Access checks, just like +// fetches from within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: shared-worker-blob-fetch.https.window.js + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTP_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local https to local: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local https to local https: success."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local https: failure."); + +promise_test(t => sharedWorkerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local https: failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js new file mode 100644 index 0000000000..e5f2b94920 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js @@ -0,0 +1,167 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts are subject +// to Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: shared-worker-fetch.window.js + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local (same-origin): no preflight required."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js new file mode 100644 index 0000000000..9bc1a89bea --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js @@ -0,0 +1,154 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts are subject +// to Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: shared-worker-fetch.https.window.js + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local https to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to private: failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.https.window.js new file mode 100644 index 0000000000..24ae108782 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.https.window.js @@ -0,0 +1,34 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror `Worker` tests, except using `SharedWorker`. +// See also: worker.https.window.js +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: shared-worker.window.js + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.window.js new file mode 100644 index 0000000000..ffa8a360c7 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/shared-worker.tentative.window.js @@ -0,0 +1,34 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror `Worker` tests, except using `SharedWorker`. +// See also: shared-worker.window.js +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: shared-worker.https.window.js + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.https.window.js new file mode 100644 index 0000000000..0731896098 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.https.window.js @@ -0,0 +1,40 @@ +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that websocket connections behave similarly to fetches. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: websocket.https.window.js + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "local to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "private to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "public to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "treat-as-public to local: websocket success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.window.js new file mode 100644 index 0000000000..a44cfaedec --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/websocket.tentative.window.js @@ -0,0 +1,40 @@ +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch + +// These tests verify that websocket connections behave similarly to fetches. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: websocket.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "local to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "private to local: websocket failure."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "public to local: websocket failure."); + +promise_test(t => websocketTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "treat-as-public to local: websocket failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.https.window.js new file mode 100644 index 0000000000..6a2a624fc8 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.https.window.js @@ -0,0 +1,209 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: variant=?include=from-treat-as-public +// +// These tests verify that secure contexts can navigate to less-public address +// spaces via window.open to an existing window iff the target server responds +// affirmatively to preflight requests. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey( + 'from-local', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_LOCAL}, + expected: NavigationTestResult.SUCCESS, + }), + 'local to local: no preflight required.'); + +subsetTestByKey( + 'from-local', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PRIVATE}, + expected: NavigationTestResult.SUCCESS, + }), + 'local to private: no preflight required.'); + +subsetTestByKey( + 'from-local', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'local to public: no preflight required.'); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + key, + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = + `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel(t => windowOpenExistingTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "failed preflight."); + + promise_test_parallel(t => windowOpenExistingTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing CORS headers."); + + promise_test_parallel(t => windowOpenExistingTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing PNA header."); + + promise_test_parallel(t => windowOpenExistingTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + expected: NavigationTestResult.SUCCESS, + }), prefix + "success."); +} + +// Source: private secure context. +// +// Navigating to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey('from-private', makePreflightTests, { + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +subsetTestByKey( + 'from-private', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PRIVATE}, + expected: NavigationTestResult.SUCCESS, + }), + 'private to private: no preflight required.'); + +subsetTestByKey( + 'from-private', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'private to public: no preflight required.'); + +// Source: public secure context. +// +// Navigating to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_PRIVATE, + targetName: "private", +}); + +subsetTestByKey( + 'from-public', promise_test_parallel, + t => windowOpenExistingTest(t, { + source: {server: Server.HTTPS_PUBLIC}, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'public to public: no preflight required.'); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => windowOpenExistingTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to local (same-origin): no preflight required.'); + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => windowOpenExistingTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.'); diff --git a/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.window.js new file mode 100644 index 0000000000..5a6cd4c5cf --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/window-open-existing.tentative.window.js @@ -0,0 +1,95 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/ +// +// These tests verify that non-secure contexts cannot navigate to less-public +// address spaces via window.open to an existing window. + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "private to local: failure."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "public to local: failure."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "public to private: failure."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "public to public: no preflight required."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test_parallel(t => windowOpenExistingTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.https.window.js new file mode 100644 index 0000000000..6793d1f3b4 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.https.window.js @@ -0,0 +1,205 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: variant=?include=from-treat-as-public +// +// These tests verify that secure contexts can navigate to less-public address +// spaces via window.open iff the target server responds affirmatively to +// preflight requests. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey("from-local", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: NavigationTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "local to private: no preflight required."); + +subsetTestByKey("from-local", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + key, + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = + `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel(t => windowOpenTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "failed preflight."); + + promise_test_parallel(t => windowOpenTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing CORS headers."); + + promise_test_parallel(t => windowOpenTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + expected: NavigationTestResult.FAILURE, + }), prefix + "missing PNA header."); + + promise_test_parallel(t => windowOpenTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.navigation(token()) }, + }, + expected: NavigationTestResult.SUCCESS, + }), prefix + "success."); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey('from-private', makePreflightTests, { + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +subsetTestByKey("from-private", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "private to public: no preflight required."); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey('from-public', makePreflightTests, { + sourceServer: Server.HTTPS_PUBLIC, + sourceName: "public", + targetServer: Server.HTTPS_PRIVATE, + targetName: "private", +}); + +subsetTestByKey("from-public", promise_test_parallel, t => windowOpenTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: "local", +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => windowOpenTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to local (same-origin): no preflight required.'); + +subsetTestByKey('from-treat-as-public', makePreflightTests, { + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); + +subsetTestByKey("from-treat-as-public", promise_test_parallel, + t => windowOpenTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.'); + +promise_test_parallel( + t => windowOpenTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {preflight: PreflightBehavior.optionalSuccess(token())} + }, + expected: NavigationTestResult.SUCCESS, + }), + 'treat-as-public-address to local: optional preflight'); diff --git a/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.window.js new file mode 100644 index 0000000000..5e2313d60a --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/window-open.tentative.window.js @@ -0,0 +1,95 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/ +// +// These tests verify that non-secure contexts cannot open a new window to +// less-public address spaces. + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "private to local: failure."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "public to local: failure."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "public to private: failure."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "public to public: no preflight required."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: NavigationTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test_parallel(t => windowOpenTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PUBLIC }, + expected: NavigationTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js new file mode 100644 index 0000000000..e119746b8a --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js @@ -0,0 +1,155 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts loaded from blob +// URLs are subject to Private Network Access checks, just like fetches from +// within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: worker-blob-fetch.https.window.js + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local https to local https: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local https: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to private https: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local https: failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js new file mode 100644 index 0000000000..89e0c3cf1f --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js @@ -0,0 +1,151 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts are subject to +// Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: worker-fetch.window.js + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.window.js new file mode 100644 index 0000000000..4d6b12f067 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/worker-fetch.tentative.window.js @@ -0,0 +1,154 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts are subject to +// Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: worker-fetch.https.window.js + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local https to local https: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local https: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to private https: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local https: failure."); diff --git a/testing/web-platform/tests/fetch/private-network-access/worker.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/worker.tentative.https.window.js new file mode 100644 index 0000000000..a0f19314ee --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/worker.tentative.https.window.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches in secure contexts are +// exempt from Private Network Access checks because workers can only be fetched +// same-origin and the origin is potentially trustworthy. The only way to test +// this is using the `treat-as-public` CSP directive to artificially place the +// parent document in the `public` IP address space. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: worker.window.js + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => workerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/worker.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/worker.tentative.window.js new file mode 100644 index 0000000000..118c099254 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/worker.tentative.window.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches are subject to Private +// Network Access checks, just like a regular `fetch()`. The main difference is +// that workers can only be fetched same-origin, so the only way to test this +// is using the `treat-as-public` CSP directive to artificially place the parent +// document in the `public` IP address space. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: worker.https.window.js + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/testing/web-platform/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js b/testing/web-platform/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js new file mode 100644 index 0000000000..3aae3050d9 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js @@ -0,0 +1,83 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that documents fetched from the `local` address space yet +// carrying the `treat-as-public-address` CSP directive are treated as if they +// had been fetched from the `public` address space. + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.OTHER_HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to local (same-origin): no preflight required."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/xhr.https.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/xhr.https.tentative.window.js new file mode 100644 index 0000000000..4dc5da9912 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/xhr.https.tentative.window.js @@ -0,0 +1,142 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror fetch.https.window.js, but use `XmlHttpRequest` instead of +// `fetch()` to perform subresource fetches. Preflights are tested less +// extensively due to coverage being already provided by `fetch()`. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: xhr.window.js + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey("from-local", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: XhrTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to private: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "private to local: failed preflight."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "private to local: success."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: XhrTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "private to public: no preflight required."); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "public to local: failed preflight."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "public to local: success."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "public to private: failed preflight."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "public to private: success."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: XhrTestResult.SUCCESS, +}), "public to public: no preflight required."); diff --git a/testing/web-platform/tests/fetch/private-network-access/xhr.tentative.window.js b/testing/web-platform/tests/fetch/private-network-access/xhr.tentative.window.js new file mode 100644 index 0000000000..fa307dc559 --- /dev/null +++ b/testing/web-platform/tests/fetch/private-network-access/xhr.tentative.window.js @@ -0,0 +1,195 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror fetch.window.js, but use `XmlHttpRequest` instead of +// `fetch()` to perform subresource fetches. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: xhr.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: XhrTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: XhrTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: XhrTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// These tests verify that documents fetched from the `local` address space yet +// carrying the `treat-as-public-address` CSP directive are treated as if they +// had been fetched from the `public` address space. + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// These tests verify that HTTPS iframes embedded in an HTTP top-level document +// cannot fetch subresources from less-public address spaces. Indeed, even +// though the iframes have HTTPS origins, they are non-secure contexts because +// their parent is a non-secure context. + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "local https to local: success."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public https to private: failure."); diff --git a/testing/web-platform/tests/fetch/range/blob.any.js b/testing/web-platform/tests/fetch/range/blob.any.js new file mode 100644 index 0000000000..7bcd4b9d11 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/blob.any.js @@ -0,0 +1,233 @@ +// META: script=/common/utils.js + +const supportedBlobRange = [ + { + name: "A simple blob range request.", + data: ["A simple Hello, World! example"], + type: "text/plain", + range: "bytes=9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "A blob range request with no type.", + data: ["A simple Hello, World! example"], + type: undefined, + range: "bytes=9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "A blob range request with no end.", + data: ["Range with no end"], + type: "text/plain", + range: "bytes=11-", + content_length: 6, + content_range: "bytes 11-16/17", + result: "no end", + }, + { + name: "A blob range request with no start.", + data: ["Range with no start"], + type: "text/plain", + range: "bytes=-8", + content_length: 8, + content_range: "bytes 11-18/19", + result: "no start", + }, + { + name: "A simple blob range request with whitespace.", + data: ["A simple Hello, World! example"], + type: "text/plain", + range: "bytes= \t9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "Blob content with short content and a large range end", + data: ["Not much here"], + type: "text/plain", + range: "bytes=4-100000000000", + content_length: 9, + content_range: "bytes 4-12/13", + result: "much here", + }, + { + name: "Blob content with short content and a range end matching content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=4-13", + content_length: 9, + content_range: "bytes 4-12/13", + result: "much here", + }, + { + name: "Blob range with whitespace before and after hyphen", + data: ["Valid whitespace #1"], + type: "text/plain", + range: "bytes=5 - 10", + content_length: 6, + content_range: "bytes 5-10/19", + result: " white", + }, + { + name: "Blob range with whitespace after hyphen", + data: ["Valid whitespace #2"], + type: "text/plain", + range: "bytes=-\t 5", + content_length: 5, + content_range: "bytes 14-18/19", + result: "ce #2", + }, + { + name: "Blob range with whitespace around equals sign", + data: ["Valid whitespace #3"], + type: "text/plain", + range: "bytes \t =\t 6-", + content_length: 13, + content_range: "bytes 6-18/19", + result: "whitespace #3", + }, +]; + +const unsupportedBlobRange = [ + { + name: "Blob range with no value", + data: ["Blob range should have a value"], + type: "text/plain", + range: "", + }, + { + name: "Blob range with incorrect range header", + data: ["A"], + type: "text/plain", + range: "byte=0-" + }, + { + name: "Blob range with incorrect range header #2", + data: ["A"], + type: "text/plain", + range: "bytes" + }, + { + name: "Blob range with incorrect range header #3", + data: ["A"], + type: "text/plain", + range: "bytes\t \t" + }, + { + name: "Blob range request with multiple range values", + data: ["Multiple ranges are not currently supported"], + type: "text/plain", + range: "bytes=0-5,15-", + }, + { + name: "Blob range request with multiple range values and whitespace", + data: ["Multiple ranges are not currently supported"], + type: "text/plain", + range: "bytes=0-5, 15-", + }, + { + name: "Blob range request with trailing comma", + data: ["Range with invalid trailing comma"], + type: "text/plain", + range: "bytes=0-5,", + }, + { + name: "Blob range with no start or end", + data: ["Range with no start or end"], + type: "text/plain", + range: "bytes=-", + }, + { + name: "Blob range request with short range end", + data: ["Range end should be greater than range start"], + type: "text/plain", + range: "bytes=10-5", + }, + { + name: "Blob range start should be an ASCII digit", + data: ["Range start must be an ASCII digit"], + type: "text/plain", + range: "bytes=x-5", + }, + { + name: "Blob range should have a dash", + data: ["Blob range should have a dash"], + type: "text/plain", + range: "bytes=5", + }, + { + name: "Blob range end should be an ASCII digit", + data: ["Range end must be an ASCII digit"], + type: "text/plain", + range: "bytes=5-x", + }, + { + name: "Blob range should include '-'", + data: ["Range end must include '-'"], + type: "text/plain", + range: "bytes=x", + }, + { + name: "Blob range should include '='", + data: ["Range end must include '='"], + type: "text/plain", + range: "bytes 5-", + }, + { + name: "Blob range should include 'bytes='", + data: ["Range end must include 'bytes='"], + type: "text/plain", + range: "5-", + }, + { + name: "Blob content with short content and a large range start", + data: ["Not much here"], + type: "text/plain", + range: "bytes=100000-", + }, + { + name: "Blob content with short content and a range start matching the content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=13-", + }, +]; + +supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => { + promise_test(async t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + const resp = await fetch(blobURL, { + "headers": { + "Range": range + } + }); + assert_equals(resp.status, 206, "HTTP status is 206"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), type || "", "Content-Type is " + resp.headers.get("Content-Type")); + assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length")); + assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range")); + const text = await resp.text(); + assert_equals(text, result, "Response's body is correct"); + }, name); +}); + +unsupportedBlobRange.forEach(({ name, data, type, range }) => { + promise_test(t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + const promise = fetch(blobURL, { + "headers": { + "Range": range + } + }); + return promise_rejects_js(t, TypeError, promise); + }, name); +}); diff --git a/testing/web-platform/tests/fetch/range/data.any.js b/testing/web-platform/tests/fetch/range/data.any.js new file mode 100644 index 0000000000..22ef11e931 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/data.any.js @@ -0,0 +1,29 @@ +// META: script=/common/utils.js + +promise_test(async () => { + return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20World%21padding", { + "method": "GET", + "Range": "bytes=13-26" + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(text) { + assert_equals(text, 'paddingHello, World!padding', "Response's body ignores range"); + }); +}, "data: URL and Range header"); + +promise_test(async () => { + return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20paddingWorld%21padding", { + "method": "GET", + "Range": "bytes=7-14,21-27" + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(text) { + assert_equals(text, 'paddingHello, paddingWorld!padding', "Response's body ignores range"); + }); +}, "data: URL and Range header with multiple ranges"); diff --git a/testing/web-platform/tests/fetch/range/general.any.js b/testing/web-platform/tests/fetch/range/general.any.js new file mode 100644 index 0000000000..64b225a60b --- /dev/null +++ b/testing/web-platform/tests/fetch/range/general.any.js @@ -0,0 +1,140 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js + +// Helpers that return headers objects with a particular guard +function headersGuardNone(fill) { + if (fill) return new Headers(fill); + return new Headers(); +} + +function headersGuardResponse(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Response('', opts).headers; +} + +function headersGuardRequest(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +function headersGuardRequestNoCors(fill) { + const opts = { mode: 'no-cors' }; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +const headerGuardTypes = [ + ['none', headersGuardNone], + ['response', headersGuardResponse], + ['request', headersGuardRequest] +]; + +for (const [guardType, createHeaders] of headerGuardTypes) { + test(() => { + // There are three ways to set headers. + // Filling, appending, and setting. Test each: + let headers = createHeaders({ Range: 'foo' }); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + }, `Range header setting allowed for guard type: ${guardType}`); +} + +test(() => { + let headers = headersGuardRequestNoCors({ Range: 'foo' }); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.append('Range', 'foo'); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.set('Range', 'foo'); + assert_false(headers.has('Range')); +}, `Privileged header not allowed for guard type: request-no-cors`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=0-10', + 'foo=0-10', + 'foo', + '' + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + + await fetch(wavURL, { + headers: { Range: rangeHeader } + }); + + const response = await fetch(stashTakeURL); + + assert_regexp_match(await response.json(), + /.*\bidentity\b.*/, + `Expect identity accept-encoding if range header is ${JSON.stringify(rangeHeader)}`); + } +}, `Fetch with range header will be sent with Accept-Encoding: identity`); + +promise_test(async () => { + const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py'); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=10-9', + 'bytes=-0', + 'bytes=0000000000000000000000000000000000000000000000000000000000011-0000000000000000000000000000000000000000000000000000000000111', + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + await fetch(wavURL, { headers: { Range : rangeHeader} }).then(() => { throw "loaded with range header " + rangeHeader }, () => { }); + } +}, `Cross Origin Fetch with non safe range header`); + +promise_test(async () => { + const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py'); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=0-10', + 'bytes=0-', + 'bytes=00000000000000000000000000000000000000000000000000000000011-00000000000000000000000000000000000000000000000000000000000111', + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + await fetch(wavURL, { headers: { Range: rangeHeader } }).then(() => { }, () => { throw "failed load with range header " + rangeHeader }); + } +}, `Cross Origin Fetch with safe range header`); diff --git a/testing/web-platform/tests/fetch/range/general.window.js b/testing/web-platform/tests/fetch/range/general.window.js new file mode 100644 index 0000000000..afe80d63a6 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/general.window.js @@ -0,0 +1,29 @@ +// META: script=resources/utils.js +// META: script=/common/utils.js + +const onload = new Promise(r => window.addEventListener('load', r)); + +// It's weird that browsers do this, but it should continue to work. +promise_test(async t => { + await loadScript('resources/partial-script.py?pretend-offset=90000'); + assert_true(self.scriptExecuted); +}, `Script executed from partial response`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + + // The testing framework waits for window onload. If the audio element + // is appended before onload, it extends it, and the test times out. + await onload; + + const audio = appendAudio(document, wavURL); + await new Promise(r => audio.addEventListener('progress', r)); + audio.remove(); + + const response = await fetch(stashTakeURL); + assert_equals(await response.json(), 'identity', `Expect identity accept-encoding on media request`); +}, `Fetch with range header will be sent with Accept-Encoding: identity`); diff --git a/testing/web-platform/tests/fetch/range/non-matching-range-response.html b/testing/web-platform/tests/fetch/range/non-matching-range-response.html new file mode 100644 index 0000000000..ba76c36766 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/non-matching-range-response.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +function range_rewrite_test(rewrites, expect, label) { + promise_test(async t => { + const url = new URL('resources/video-with-range.py', location.href); + const params = new URLSearchParams(); + params.set('rewrites', JSON.stringify(rewrites)); + url.search = params.toString(); + const video = document.createElement('video'); + video.autoplay = true; + video.muted = true; + video.src = url.toString(); + const timeout = new Promise(resolve => t.step_timeout(() => resolve('timeout'), 10000)); + const ok = new Promise(resolve => video.addEventListener('play', () => resolve('ok'))); + t.add_cleanup(() => video.remove()); + document.body.appendChild(video); + const result = await Promise.any([timeout, ok]); + assert_equals(result, 'ok'); + }, `${label} should ${expect === 'ok' ? 'succeed' : 'fail'}`); +} + +range_rewrite_test([], 'ok', 'Range requests with no rewrites'); +range_rewrite_test( + [ + {request: ['0', '*'], response: [0, 100]}, + {request: ['100', '*'], response: [50, 2000]} + ], 'ok', 'Range response out of range of request'); +range_rewrite_test([{request: ['0', '*'], status: 200}], 'ok', 'Range requests ignored (200 status)'); +</script> +</body> diff --git a/testing/web-platform/tests/fetch/range/resources/basic.html b/testing/web-platform/tests/fetch/range/resources/basic.html new file mode 100644 index 0000000000..0e76edd65b --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/basic.html @@ -0,0 +1 @@ +<!DOCTYPE html> diff --git a/testing/web-platform/tests/fetch/range/resources/long-wav.py b/testing/web-platform/tests/fetch/range/resources/long-wav.py new file mode 100644 index 0000000000..acfc81a718 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/long-wav.py @@ -0,0 +1,134 @@ +""" +This generates a 30 minute silent wav, and is capable of +responding to Range requests. +""" +import time +import re +import struct + +from wptserve.utils import isomorphic_decode + +def create_wav_header(sample_rate, bit_depth, channels, duration): + bytes_per_sample = int(bit_depth / 8) + block_align = bytes_per_sample * channels + byte_rate = sample_rate * block_align + sub_chunk_2_size = duration * byte_rate + + data = b'' + # ChunkID + data += b'RIFF' + # ChunkSize + data += struct.pack('<L', 36 + sub_chunk_2_size) + # Format + data += b'WAVE' + # Subchunk1ID + data += b'fmt ' + # Subchunk1Size + data += struct.pack('<L', 16) + # AudioFormat + data += struct.pack('<H', 1) + # NumChannels + data += struct.pack('<H', channels) + # SampleRate + data += struct.pack('<L', sample_rate) + # ByteRate + data += struct.pack('<L', byte_rate) + # BlockAlign + data += struct.pack('<H', block_align) + # BitsPerSample + data += struct.pack('<H', bit_depth) + # Subchunk2ID + data += b'data' + # Subchunk2Size + data += struct.pack('<L', sub_chunk_2_size) + + return data + + +def main(request, response): + if request.method == u"OPTIONS": + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"Preflight not accepted" + + response.headers.set(b"Content-Type", b"audio/wav") + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Cache-Control", b"no-cache") + response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b'Origin', b'')) + + range_header = request.headers.get(b'Range', b'') + range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header)) + range_received_key = request.GET.first(b'range-received-key', b'') + accept_encoding_key = request.GET.first(b'accept-encoding-key', b'') + + if range_received_key and range_header: + # Remove any current value + request.server.stash.take(range_received_key, b'/fetch/range/') + # This is later collected using stash-take.py + request.server.stash.put(range_received_key, u'range-header-received', b'/fetch/range/') + + if accept_encoding_key: + # Remove any current value + request.server.stash.take( + accept_encoding_key, + b'/fetch/range/' + ) + # This is later collected using stash-take.py + request.server.stash.put( + accept_encoding_key, + isomorphic_decode(request.headers.get(b'Accept-Encoding', b'')), + b'/fetch/range/' + ) + + # Audio details + sample_rate = 8000 + bit_depth = 8 + channels = 1 + duration = 60 * 5 + + total_length = int((sample_rate * bit_depth * channels * duration) / 8) + bytes_remaining_to_send = total_length + initial_write = b'' + + if range_header_match: + response.status = 206 + start, end = range_header_match.groups() + + start = int(start) + end = int(end) if end else 0 + + if end: + bytes_remaining_to_send = (end + 1) - start + else: + bytes_remaining_to_send = total_length - start + + wav_header = create_wav_header(sample_rate, bit_depth, channels, duration) + + if start < len(wav_header): + initial_write = wav_header[start:] + + if bytes_remaining_to_send < len(initial_write): + initial_write = initial_write[0:bytes_remaining_to_send] + + content_range = b"bytes %d-%d/%d" % (start, end or total_length - 1, total_length) + + response.headers.set(b"Content-Range", content_range) + else: + initial_write = create_wav_header(sample_rate, bit_depth, channels, duration) + + response.headers.set(b"Content-Length", bytes_remaining_to_send) + + response.write_status_headers() + response.writer.write(initial_write) + + bytes_remaining_to_send -= len(initial_write) + + while bytes_remaining_to_send > 0: + to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate) + bytes_remaining_to_send -= len(to_send) + + if not response.writer.write(to_send): + break + + # Throttle the stream + time.sleep(0.5) diff --git a/testing/web-platform/tests/fetch/range/resources/partial-script.py b/testing/web-platform/tests/fetch/range/resources/partial-script.py new file mode 100644 index 0000000000..a9570ec355 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/partial-script.py @@ -0,0 +1,29 @@ +""" +This generates a partial response containing valid JavaScript. +""" + +def main(request, response): + require_range = request.GET.first(b'require-range', b'') + pretend_offset = int(request.GET.first(b'pretend-offset', b'0')) + range_header = request.headers.get(b'Range', b'') + + if require_range and not range_header: + response.set_error(412, u"Range header required") + response.write() + return + + response.headers.set(b"Content-Type", b"text/plain") + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Cache-Control", b"no-cache") + response.status = 206 + + to_send = b'self.scriptExecuted = true;' + length = len(to_send) + + content_range = b"bytes %d-%d/%d" % ( + pretend_offset, pretend_offset + length - 1, pretend_offset + length) + + response.headers.set(b"Content-Range", content_range) + response.headers.set(b"Content-Length", length) + + response.content = to_send diff --git a/testing/web-platform/tests/fetch/range/resources/partial-text.py b/testing/web-platform/tests/fetch/range/resources/partial-text.py new file mode 100644 index 0000000000..fa3d1171b6 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/partial-text.py @@ -0,0 +1,53 @@ +""" +This generates a partial response for a 100-byte text file. +""" +import re + +from wptserve.utils import isomorphic_decode + +def main(request, response): + total_length = int(request.GET.first(b'length', b'100')) + partial_code = int(request.GET.first(b'partial', b'206')) + content_type = request.GET.first(b'type', b'text/plain') + range_header = request.headers.get(b'Range', b'') + + # Send a 200 if there is no range request + if not range_header: + to_send = ''.zfill(total_length) + response.headers.set(b"Content-Type", content_type) + response.headers.set(b"Cache-Control", b"no-cache") + response.headers.set(b"Content-Length", total_length) + response.content = to_send + return + + # Simple range parsing, requires specifically "bytes=xxx-xxxx" + range_header_match = re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header)) + start, end = range_header_match.groups() + start = int(start) + end = int(end) if end else total_length + length = end - start + + # Error the request if the range goes beyond the length + if length <= 0 or end > total_length: + response.set_error(416, u"Range Not Satisfiable") + # set_error sets the MIME type to application/json, which - for a + # no-cors media request - will be blocked by ORB. We'll just force + # the expected MIME type here, whichfixes the test, but doesn't make + # sense in general. + response.headers = [(b"Content-Type", content_type)] + response.write() + return + + # Generate a partial response of the requested length + to_send = ''.zfill(length) + response.headers.set(b"Content-Type", content_type) + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Cache-Control", b"no-cache") + response.status = partial_code + + content_range = b"bytes %d-%d/%d" % (start, end, total_length) + + response.headers.set(b"Content-Range", content_range) + response.headers.set(b"Content-Length", length) + + response.content = to_send diff --git a/testing/web-platform/tests/fetch/range/resources/range-sw.js b/testing/web-platform/tests/fetch/range/resources/range-sw.js new file mode 100644 index 0000000000..b47823f03b --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/range-sw.js @@ -0,0 +1,218 @@ +importScripts('/resources/testharness.js'); + +setup({ explicit_done: true }); + +function assert_range_request(request, expectedRangeHeader, name) { + assert_equals(request.headers.get('Range'), expectedRangeHeader, name); +} + +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', async event => { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'range-header-filter-test': + rangeHeaderFilterTest(request); + return; + case 'range-header-passthrough-test': + rangeHeaderPassthroughTest(event); + return; + case 'store-ranged-response': + storeRangedResponse(event); + return; + case 'use-stored-ranged-response': + useStoredRangeResponse(event); + return; + case 'broadcast-accept-encoding': + broadcastAcceptEncoding(event); + return; + case 'record-media-range-request': + return recordMediaRangeRequest(event); + case 'use-media-range-request': + useMediaRangeRequest(event); + return; + } +}); + +/** + * @param {Request} request + */ +function rangeHeaderFilterTest(request) { + const rangeValue = request.headers.get('Range'); + + test(() => { + assert_range_request(new Request(request), rangeValue, `Untampered`); + assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`); + assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`); + assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`); + assert_range_request(request.clone(), rangeValue, `Clone`); + }, "Range headers correctly preserved"); + + test(() => { + assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`); + assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`); + assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`); + assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`); + }, "Range headers correctly removed"); + + test(() => { + let headers; + + headers = new Request(request).headers; + headers.delete('does-not-exist'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`); + + headers = new Request(request).headers; + headers.append('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`); + + headers = new Request(request).headers; + headers.set('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully set`); + + headers = new Request(request).headers; + headers.delete('Accept'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`); + + headers = new Request(request).headers; + headers.delete('Range'); + assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`); + }, "Headers correctly filtered"); + + done(); +} + +function rangeHeaderPassthroughTest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const key = url.searchParams.get('range-received-key'); + + event.waitUntil(new Promise(resolve => { + promise_test(async () => { + await fetch(event.request); + const response = await fetch('stash-take.py?key=' + key); + assert_equals(await response.json(), 'range-header-received'); + resolve(); + }, `Include range header in network request`); + + done(); + })); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +let storedRangeResponseP; + +function storeRangedResponse(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + storedRangeResponseP = fetch(event.request); + broadcast({ id }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +function useStoredRangeResponse(event) { + event.respondWith(async function() { + const response = await storedRangeResponseP; + if (!response) throw Error("Expected stored range response"); + return response.clone(); + }()); +} + +function broadcastAcceptEncoding(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + broadcast({ + id, + acceptEncoding: request.headers.get('Accept-Encoding') + }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +let rangeResponse = {}; + +async function recordMediaRangeRequest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const urlParams = new URLSearchParams(url.search); + const size = urlParams.get("size"); + const id = urlParams.get('id'); + const key = 'size' + size; + + if (key in rangeResponse) { + // Don't re-fetch ranges we already have. + const clonedResponse = rangeResponse[key].clone(); + event.respondWith(clonedResponse); + } else if (event.request.headers.get("range") === "bytes=0-") { + // Generate a bogus 206 response to trigger subsequent range requests + // of the desired size. + const length = urlParams.get("length") + 100; + const body = "A".repeat(Number(size)); + event.respondWith(new Response(body, {status: 206, headers: { + "Content-Type": "audio/mp4", + "Content-Range": `bytes 0-1/${length}` + }})); + } else if (event.request.headers.get("range") === `bytes=${Number(size)}-`) { + // Pass through actual range requests which will attempt to fetch up to the + // length in the original response which is bigger than the actual resource + // to make sure 206 and 416 responses are treated the same. + rangeResponse[key] = await fetch(event.request); + + // Let the client know we have the range response for the given ID + broadcast({id}); + } else { + event.respondWith(Promise.reject(Error("Invalid Request"))); + } +} + +function useMediaRangeRequest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const urlParams = new URLSearchParams(url.search); + const size = urlParams.get("size"); + const key = 'size' + size; + + // Send a clone of the range response to preload. + if (key in rangeResponse) { + const clonedResponse = rangeResponse[key].clone(); + event.respondWith(clonedResponse); + } else { + event.respondWith(Promise.reject(Error("Invalid Request"))); + } +} diff --git a/testing/web-platform/tests/fetch/range/resources/stash-take.py b/testing/web-platform/tests/fetch/range/resources/stash-take.py new file mode 100644 index 0000000000..6cf6ff585b --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/stash-take.py @@ -0,0 +1,7 @@ +from wptserve.handlers import json_handler + + +@json_handler +def main(request, response): + key = request.GET.first(b"key") + return request.server.stash.take(key, b'/fetch/range/') diff --git a/testing/web-platform/tests/fetch/range/resources/utils.js b/testing/web-platform/tests/fetch/range/resources/utils.js new file mode 100644 index 0000000000..ad2853b33d --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/utils.js @@ -0,0 +1,36 @@ +function loadScript(url, { doc = document }={}) { + return new Promise((resolve, reject) => { + const script = doc.createElement('script'); + script.onload = () => resolve(); + script.onerror = () => reject(Error("Script load failed")); + script.src = url; + doc.body.appendChild(script); + }) +} + +function preloadImage(url, { doc = document }={}) { + return new Promise((resolve, reject) => { + const preload = doc.createElement('link'); + preload.rel = 'preload'; + preload.as = 'image'; + preload.onload = () => resolve(); + preload.onerror = () => resolve(); + preload.href = url; + doc.body.appendChild(preload); + }) +} + +/** + * + * @param {Document} document + * @param {string|URL} url + * @returns {HTMLAudioElement} + */ +function appendAudio(document, url) { + const audio = document.createElement('audio'); + audio.muted = true; + audio.src = url; + audio.preload = true; + document.body.appendChild(audio); + return audio; +} diff --git a/testing/web-platform/tests/fetch/range/resources/video-with-range.py b/testing/web-platform/tests/fetch/range/resources/video-with-range.py new file mode 100644 index 0000000000..2d15ccf3c4 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/resources/video-with-range.py @@ -0,0 +1,43 @@ +import re +import os +import json +from wptserve.utils import isomorphic_decode + +def main(request, response): + path = os.path.join(request.doc_root, u"media", "sine440.mp3") + total_size = os.path.getsize(path) + rewrites = json.loads(request.GET.first(b'rewrites', '[]')) + range_header = request.headers.get(b'Range') + range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header)) + start = None + end = None + if range_header_match: + response.status = 206 + start, end = range_header_match.groups() + if range_header: + status = 206 + else: + status = 200 + for rewrite in rewrites: + req_start, req_end = rewrite['request'] + if start == req_start or req_start == '*': + if end == req_end or req_end == '*': + if 'response' in rewrite: + start, end = rewrite['response'] + if 'status' in rewrite: + status = rewrite['status'] + + start = int(start or 0) + end = int(end or total_size) + headers = [] + if status == 206: + headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end - 1, total_size))) + headers.append((b"Accept-Ranges", b"bytes")) + + headers.append((b"Content-Type", b"audio/mp3")) + headers.append((b"Content-Length", str(end - start))) + headers.append((b"Cache-Control", b"no-cache")) + video_file = open(path, "rb") + video_file.seek(start) + content = video_file.read(end) + return status, headers, content diff --git a/testing/web-platform/tests/fetch/range/sw.https.window.js b/testing/web-platform/tests/fetch/range/sw.https.window.js new file mode 100644 index 0000000000..62ad894da3 --- /dev/null +++ b/testing/web-platform/tests/fetch/range/sw.https.window.js @@ -0,0 +1,228 @@ +// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/utils.js + +const { REMOTE_HOST } = get_host_info(); +const BASE_SCOPE = 'resources/basic.html?'; + +async function cleanup() { + for (const iframe of document.querySelectorAll('.test-iframe')) { + iframe.parentNode.removeChild(iframe); + } + + for (const reg of await navigator.serviceWorker.getRegistrations()) { + await reg.unregister(); + } +} + +async function setupRegistration(t, scope) { + await cleanup(); + const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope }); + await wait_for_state(t, reg.installing, 'activated'); + return reg; +} + +function awaitMessage(obj, id) { + return new Promise(resolve => { + obj.addEventListener('message', function listener(event) { + if (event.data.id !== id) return; + obj.removeEventListener('message', listener); + resolve(event.data); + }); + }); +} + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py?action=range-header-filter-test', w.location); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderFilterTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header filter tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'range-header-passthrough-test'); + url.searchParams.set('range-received-key', token()); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderPassthroughTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header passthrough tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a cross-origin range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + url.hostname = REMOTE_HOST; + + appendAudio(w.document, url); + + await storedRangeResponse; + + // Fetching should reject + const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' }); + await promise_rejects_js(t, w.TypeError, fetchPromise); + + // Script loading should error too + const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document }); + await promise_rejects_js(t, Error, loadScriptPromise); + + await loadScriptPromise.catch(() => {}); + + assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`); +}, `Ranged response not allowed following no-cors ranged request`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + + appendAudio(w.document, url); + + await storedRangeResponse; + + // This should not throw + await w.fetch('?action=use-stored-ranged-response'); + + // This shouldn't throw either + await loadScript('?action=use-stored-ranged-response', { doc: w.document }); + + assert_true(w.scriptExecuted, `Partial response should be executed`); +}, `Non-opaque ranged response executed`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const fetchId = Math.random() + ''; + const fetchBroadcast = awaitMessage(w.navigator.serviceWorker, fetchId); + const audioId = Math.random() + ''; + const audioBroadcast = awaitMessage(w.navigator.serviceWorker, audioId); + + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'broadcast-accept-encoding'); + url.searchParams.set('id', fetchId); + + await w.fetch(url, { + headers: { Range: 'bytes=0-10' } + }); + + assert_equals((await fetchBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for fetch"); + + url.searchParams.set('id', audioId); + appendAudio(w.document, url); + + assert_equals((await audioBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for media"); +}, `Accept-Encoding should not appear in a service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const length = 100; + const count = 3; + const counts = {}; + + // test a single range request size + async function testSizedRange(size, partialResponseCode) { + const rangeId = Math.random() + ''; + const rangeBroadcast = awaitMessage(w.navigator.serviceWorker, rangeId); + + // Create a bogus audio element to trick the browser into sending + // cross-origin range requests that can be manipulated by the service worker. + const sound_url = new URL('partial-text.py', w.location); + sound_url.hostname = REMOTE_HOST; + sound_url.searchParams.set('action', 'record-media-range-request'); + sound_url.searchParams.set('length', length); + sound_url.searchParams.set('size', size); + sound_url.searchParams.set('partial', partialResponseCode); + sound_url.searchParams.set('id', rangeId); + sound_url.searchParams.set('type', 'audio/mp4'); + appendAudio(w.document, sound_url); + + // wait for the range requests to happen + await rangeBroadcast; + + // Create multiple preload requests and count the number of resource timing + // entries that get created to make sure 206 and 416 range responses are treated + // the same. + const url = new URL('partial-text.py', w.location); + url.searchParams.set('action', 'use-media-range-request'); + url.searchParams.set('size', size); + url.searchParams.set('type', 'audio/mp4'); + counts['size' + size] = 0; + for (let i = 0; i < count; i++) { + await preloadImage(url, { doc: w.document }); + } + } + + // Test range requests from 1 smaller than the correct size to 1 larger than + // the correct size to exercise the various permutations using the default 206 + // response code for successful range requests. + for (let size = length - 1; size <= length + 1; size++) { + await testSizedRange(size, '206'); + } + + // Test a successful range request using a 200 response. + await testSizedRange(length - 2, '200'); + + // Check the resource timing entries and count the reported number of fetches of each type + const resources = w.performance.getEntriesByType("resource"); + for (const entry of resources) { + const url = new URL(entry.name); + if (url.searchParams.has('action') && + url.searchParams.get('action') == 'use-media-range-request' && + url.searchParams.has('size')) { + counts['size' + url.searchParams.get('size')]++; + } + } + + // Make sure there are a non-zero number of preload requests and they are all the same + let counts_valid = true; + const first = 'size' + (length - 2); + for (let size = length - 2; size <= length + 1; size++) { + let key = 'size' + size; + if (!(key in counts) || counts[key] <= 0 || counts[key] != counts[first]) { + counts_valid = false; + break; + } + } + + assert_true(counts_valid, `Opaque range request preloads were different for error and success`); +}, `Opaque range preload successes and failures should be indistinguishable`); diff --git a/testing/web-platform/tests/fetch/redirect-navigate/302-found-post-handler.py b/testing/web-platform/tests/fetch/redirect-navigate/302-found-post-handler.py new file mode 100644 index 0000000000..40a224f656 --- /dev/null +++ b/testing/web-platform/tests/fetch/redirect-navigate/302-found-post-handler.py @@ -0,0 +1,15 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + if request.method == u"POST": + response.add_required_headers = False + response.writer.write_status(302) + response.writer.write_header(b"Location", isomorphic_encode(request.url)) + response.writer.end_headers() + response.writer.write(b"") + elif request.method == u"GET": + return ([(b"Content-Type", b"text/plain")], + b"OK") + else: + return ([(b"Content-Type", b"text/plain")], + b"FAIL")
\ No newline at end of file diff --git a/testing/web-platform/tests/fetch/redirect-navigate/302-found-post.html b/testing/web-platform/tests/fetch/redirect-navigate/302-found-post.html new file mode 100644 index 0000000000..854cd329a8 --- /dev/null +++ b/testing/web-platform/tests/fetch/redirect-navigate/302-found-post.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<!-- Step 1: send POST request to a URL which will then 302 Found redirect --> +<title>HTTP 302 Found POST Navigation Test</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +async_test(function(t) { + window.addEventListener("load", function() { + var frame = document.getElementById("frame"); + var link = new URL("302-found-post-handler.py", window.location.href); + frame.contentWindow.document.body.innerHTML = '<form action="' + link.href + '" method="POST" id="form"><input name="n"></form>'; + frame.contentWindow.document.getElementById("form").submit(); + frame.addEventListener("load", t.step_func_done(function() { + assert_equals(frame.contentWindow.document.body.textContent, "OK"); + })); + }); +}, "HTTP 302 Found POST Navigation"); +</script> +<body> +<iframe id="frame" src="about:blank"></iframe> diff --git a/testing/web-platform/tests/fetch/redirect-navigate/preserve-fragment.html b/testing/web-platform/tests/fetch/redirect-navigate/preserve-fragment.html new file mode 100644 index 0000000000..682539a744 --- /dev/null +++ b/testing/web-platform/tests/fetch/redirect-navigate/preserve-fragment.html @@ -0,0 +1,202 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <title>Ensure fragment is kept across redirects</title> + <meta name="timeout" content="long"> + <link rel=help href="https://www.w3.org/TR/cuap/#uri"> + <link rel=help href="https://tools.ietf.org/html/rfc7231#section-7.1.2"> + <link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=158420"> + <link rel=help href="https://bugs.webkit.org/show_bug.cgi?id=24175"> + <script src="/common/get-host-info.sub.js"></script> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script> + let frame; + let message; + + const HTTP_SAME_ORIGIN = "HTTP - SameOrigin"; + const HTTPS_SAME_ORIGIN = "HTTPS - SameOrigin"; + const HTTP_CROSS_ORIGIN = "HTTP - CrossOrigin"; + const HTTPS_CROSS_ORIGIN = "HTTPS - CrossOrigin"; + + function messageReceived(f) { + return new Promise((resolve) => { + window.addEventListener("message", (e) => { + message = e.data; + resolve(); + }, {once: true}); + f(); + }); + } + + function getHostname(navigation_type) { + switch (navigation_type) { + case HTTP_SAME_ORIGIN: + return get_host_info().HTTP_ORIGIN; + case HTTPS_SAME_ORIGIN: + return get_host_info().HTTPS_ORIGIN + case HTTP_CROSS_ORIGIN: + return get_host_info().HTTP_REMOTE_ORIGIN + case HTTPS_CROSS_ORIGIN: + return get_host_info().HTTPS_REMOTE_ORIGIN + } + + return 'nonexistent' + } + + // Turns |path| from a relative to this file path into a full URL, with + // the host being determined by one of the ORIGIN strings above. + function relativePathToFull(path, navigation_type) { + let host = getHostname(navigation_type); + + const pathname = window.location.pathname; + const base_path = pathname.substring(0, pathname.lastIndexOf('/') + 1); + + return host + base_path + path; + } + + // Constructs a URL to redirect.py which will respond with the given + // redirect status |code| to the provided |to_url|. Optionally adds on a + // |fragment|, if provided, to use in the initial request to redirect.py + function buildRedirectUrl(to_url, code, fragment) { + to_url = encodeURIComponent(to_url); + let dest = `/common/redirect.py?status=${code}&location=${to_url}`; + if (fragment) + dest = dest + '#' + fragment; + return dest; + } + + async function redirectTo(url, code, navigation_type, fragment) { + const dest = buildRedirectUrl(url, code, fragment); + await messageReceived( () => { + frame.contentWindow.location = getHostname(navigation_type) + dest; + }); + } + + async function doubleRedirectTo(url, code, navigation_type, fragment, intermediate_fragment) { + const second_redirection = buildRedirectUrl(url, code, intermediate_fragment); + const first_redirection = buildRedirectUrl(second_redirection, code, fragment); + await messageReceived( () => { + frame.contentWindow.location = getHostname(navigation_type) + first_redirection; + }); + } + + onload = () => { + frame = document.getElementById("frame"); + + // The tests in this file verify fragments are correctly propagated in + // a number of HTTP redirect scenarios. Each test is run for every + // relevant redirect status code. We also run each scenario under each + // combination of navigating to cross/same origin and using http/https. + const status_codes = [301, 302, 303, 307, 308]; + const navigation_types = [HTTP_SAME_ORIGIN, + HTTPS_SAME_ORIGIN, + HTTP_CROSS_ORIGIN, + HTTPS_CROSS_ORIGIN]; + + for (let navigation_type of navigation_types) { + // Navigate to a URL with a fragment. The URL redirects to a different + // page. Ensure we land on the redirected page with the fragment + // specified in the initial navigation's URL. + // + // Redirect chain: urlA#target -> urlB + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html', navigation_type); + await redirectTo(to_url, code, navigation_type, "target"); + assert_true(message.url.endsWith('#target')); + assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation."); + }, `[${navigation_type}] Preserve fragment in ${code} redirect`); + } + + // Navigate to a URL with a fragment. The URL redirects to a different + // URL that also contains a fragment. Ensure we land on the redirected + // page using the fragment specified in the redirect response and not + // the one in the initial navigation. + // + // Redirect chain: urlA#target -> urlB#fromRedirect + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type); + await redirectTo(to_url, code, navigation_type, "target"); + assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`); + assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect."); + }, `[${navigation_type}] Redirect URL fragment takes precedence in ${code} redirect`); + } + + // Perform two redirects. The initial navigation has a fragment and + // will redirect to a URL that also responds with a redirect. Ensure we + // land on the final page with the fragment from the original + // navigation. + // + // Redirect chain: urlA#target -> urlB -> urlC + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html', navigation_type); + await doubleRedirectTo(to_url, code, navigation_type, "target"); + assert_true(message.url.endsWith('#target'), `Unexpected fragment: ${message.url}`); + assert_equals(message.scrollY, 2000, "scrolls to fragment from initial navigation."); + }, `[${navigation_type}] Preserve fragment in multiple ${code} redirects`); + } + + // Perform two redirects. The initial navigation has a fragment and + // will redirect to a URL that also responds with a redirect. The + // second redirection to the final page also has a fragment. Ensure we + // land on the final page with the fragment from the redirection + // response URL. + // + // Redirect chain: urlA#target -> urlB -> urlC#fromRedirect + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type); + await doubleRedirectTo(to_url, code, navigation_type, "target"); + assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`); + assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect."); + }, `[${navigation_type}] Destination URL fragment takes precedence in multiple ${code} redirects`); + } + + // Perform two redirects. The initial navigation has a fragment and + // will redirect to a URL that also responds with a redirect. This + // time, both redirect response have a fragment. Ensure we land on the + // final page with the fragment from the last redirection response URL. + // + // Redirect chain: urlA#target -> urlB#intermediate -> urlC#fromRedirect + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html#fromRedirect', navigation_type); + await doubleRedirectTo(to_url, code, navigation_type, "target", "intermediate"); + assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`); + assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect."); + }, `[${navigation_type}] Final redirect fragment takes precedence over intermediate in multiple ${code} redirects`); + } + + // Perform two redirects. The initial navigation has a fragment and + // will redirect to a URL that also responds with a redirect. The first + // redirect response has a fragment but the second doesn't. Ensure we + // land on the final page with the fragment from the first redirection + // response URL. + // + // Redirect chain: urlA#target -> urlB#fromRedirect -> urlC + // + for (let code of status_codes) { + promise_test(async () => { + const to_url = relativePathToFull('resources/destination.html', navigation_type); + await doubleRedirectTo(to_url, code, navigation_type, "target", "fromRedirect"); + assert_true(message.url.endsWith('#fromRedirect'), `Unexpected fragment: ${message.url}`); + assert_equals(message.scrollY, 4000, "scrolls to fragment from redirect."); + }, `[${navigation_type}] Preserve intermediate fragment in multiple ${code} redirects`); + } + } + } + </script> + </head> + <body> + <iframe id="frame" src=""></iframe> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/redirect-navigate/resources/destination.html b/testing/web-platform/tests/fetch/redirect-navigate/resources/destination.html new file mode 100644 index 0000000000..f98c5a8cd7 --- /dev/null +++ b/testing/web-platform/tests/fetch/redirect-navigate/resources/destination.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <style> + body { + height: 10000px; + margin: 0; + } + p { + position: absolute; + margin: 0; + } + </style> + <script> + window.onload = () => { + window.parent.postMessage({ + url: window.location.toString(), + scrollY: window.scrollY + }, "*"); + } + </script> + </head> + <body> + <p style="top: 2000px" id="target">Target</p> + <p style="top: 4000px" id="fromRedirect">Target</p> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/redirects/data.window.js b/testing/web-platform/tests/fetch/redirects/data.window.js new file mode 100644 index 0000000000..eeb41966b4 --- /dev/null +++ b/testing/web-platform/tests/fetch/redirects/data.window.js @@ -0,0 +1,25 @@ +// See ../api/redirect/redirect-to-dataurl.any.js for fetch() tests + +async_test(t => { + const img = document.createElement("img"); + img.onload = t.unreached_func(); + img.onerror = t.step_func_done(); + img.src = "../api/resources/redirect.py?location=data:image/png%3Bbase64,iVBORw0KGgoAAAANSUhEUgAAAIUAAABqCAIAAAAdqgU8AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVHhe7dNBDQAADIPA%2Bje92eBxSQUQSLedlQzo0TLQonFWPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceJQMPIOzeGc0PIDEAAAAASUVORK5CYII"; +}, "<img> fetch that redirects to data: URL"); + +globalThis.globalTest = null; +async_test(t => { + globalThis.globalTest = t; + const script = document.createElement("script"); + script.src = "../api/resources/redirect.py?location=data:text/javascript,(globalThis.globalTest.unreached_func())()"; + script.onerror = t.step_func_done(); + document.body.append(script); +}, "<script> fetch that redirects to data: URL"); + +async_test(t => { + const client = new XMLHttpRequest(); + client.open("GET", "../api/resources/redirect.py?location=data:,"); + client.send(); + client.onload = t.unreached_func(); + client.onerror = t.step_func_done(); +}, "XMLHttpRequest fetch that redirects to data: URL"); diff --git a/testing/web-platform/tests/fetch/redirects/subresource-fragments.html b/testing/web-platform/tests/fetch/redirects/subresource-fragments.html new file mode 100644 index 0000000000..0bd74d7bfb --- /dev/null +++ b/testing/web-platform/tests/fetch/redirects/subresource-fragments.html @@ -0,0 +1,39 @@ +<!doctype html> +<meta charset=utf-8> +<title>Subresources and fragment preservation</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/html/canvas/resources/canvas-tests.js></script> +<div id=log></div> +<!-- + The source image is 50h x 100w and its color depends on the fragment. + + This image is then drawn on a 50h x 100w transparent black canvas. +--> +<img data-desc="Control" + src="/images/colors.svg#green"> +<img data-desc="Redirect with the original URL containing a fragment" + src="../api/resources/redirect.py?simple&location=/images/colors.svg#green"> +<img data-desc="Redirect with the response Location header containing a fragment" + src="../api/resources/redirect.py?simple&location=/images/colors.svg%23green"> +<img data-desc="Redirect with both the original URL and response Location header containing a fragment" + src="../api/resources/redirect.py?simple&location=/images/colors.svg%23green#red"> +<canvas width=100 height=50></canvas> +<script> +setup({ explicit_done:true }); +onload = () => { + const canvas = document.querySelector("canvas"); + const ctx = canvas.getContext("2d"); + document.querySelectorAll("img").forEach(img => { + test(t => { + t.add_cleanup(() => { + ctx.clearRect(0, 0, 100, 50); + }); + ctx.drawImage(img, 0, 0); + // canvas, pixelX, pixelY, r, g, b, alpha, ?, ?, tolerance + _assertPixelApprox(canvas, 40, 40, 0, 255, 0, 255, 4); + }, img.dataset.desc); + }); + done(); +}; +</script> diff --git a/testing/web-platform/tests/fetch/security/1xx-response.any.js b/testing/web-platform/tests/fetch/security/1xx-response.any.js new file mode 100644 index 0000000000..df4dafcd80 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/1xx-response.any.js @@ -0,0 +1,28 @@ +promise_test(async (t) => { + // The 100 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(100)')); +}, 'Status(100) should be ignored.'); + +// This behavior is being discussed at https://github.com/whatwg/fetch/issues/1397. +promise_test(async (t) => { + const res = await fetch('/common/text-plain.txt?pipe=status(101)'); + assert_equals(res.status, 101); + const body = await res.text(); + assert_equals(body, ''); +}, 'Status(101) should be accepted, with removing body.'); + +promise_test(async (t) => { + // The 103 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(103)')); +}, 'Status(103) should be ignored.'); + +promise_test(async (t) => { + // The 199 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(199)')); +}, 'Status(199) should be ignored.'); diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation-data-url.tentative.sub.html b/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation-data-url.tentative.sub.html new file mode 100644 index 0000000000..f27735daa1 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation-data-url.tentative.sub.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function readableURL(url) { + return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); + } + + // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at + // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape + // the content we'd like to treat as "raw" (e.g. `\n` => ` `, `<` => `<`), and + // double-escape the "escaped" content. + var rawBrace = "<"; + var escapedBrace = "&lt;"; + var doubleEscapedBrace = "&amp;lt;"; + var rawNewline = " "; + var escapedNewline = "&#10;"; + // doubleEscapedNewline is used inside a data URI, and so must have its '#' escaped. + var doubleEscapedNewline = "&amp;%2310;"; + + function appendFrameAndGetElement(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + resolve(frame.contentDocument.querySelector('#dangling')); + }); + document.body.appendChild(frame); + }); + } + + function assert_img_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 1, "Height"); + frame.remove(); + })); + } + + function assert_img_not_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 0, "Height"); + assert_equals(img.naturalWidth, 0, "Width"); + })); + } + + function assert_nested_img_not_loaded(test, frame) { + window.addEventListener('message', test.step_func(e => { + if (e.source != frame.contentWindow) + return; + + assert_equals(e.data, 'error'); + test.done(); + })); + appendFrameAndGetElement(test, frame); + } + + function assert_nested_img_loaded(test, frame) { + window.addEventListener('message', test.step_func(e => { + if (e.source != frame.contentWindow) + return; + + assert_equals(e.data, 'loaded'); + test.done(); + })); + appendFrameAndGetElement(test, frame); + } + + function createFrame(markup) { + var i = document.createElement('iframe'); + i.srcdoc = `${markup}sekrit`; + return i; + } + + // Subresource requests: + [ + // Data URLs don't themselves trigger blocking: + `<img id="dangling" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + `<img id="dangling" src="data:image/png;base64,${rawNewline}iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + `<img id="dangling" src="data:image/png;base64,i${rawNewline}VBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + + // Data URLs with visual structure don't trigger blocking + `<img id="dangling" src="data:image/svg+xml;utf8, + <svg width='1' height='1' xmlns='http://www.w3.org/2000/svg'> + <rect width='100%' height='100%' fill='rebeccapurple'/> + <rect x='10%' y='10%' width='80%' height='80%' fill='lightgreen'/> + </svg>">` + ].forEach(markup => { + async_test(t => { + var i = createFrame(`${markup} <element attr="" another=''>`); + assert_img_loaded(t, i); + }, readableURL(markup)); + }); + + // Nested subresource requests: + // + // The following tests load a given HTML string into `<iframe srcdoc="...">`, so we'll + // end up with a frame with an ID of `dangling` inside the srcdoc frame. That frame's + // `src` is a `data:` URL that resolves to an HTML document containing an `<img>`. The + // error/load handlers on that image are piped back up to the top-level document to + // determine whether the tests' expectations were met. *phew* + + // Allowed: + [ + // Just a newline: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png'> + "> + </iframe>`, + + // Just a brace: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Newline and escaped brace. + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${doubleEscapedBrace}'> + "> + </iframe>`, + + // Brace and escaped newline: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${doubleEscapedNewline}${rawBrace}'> + "> + </iframe>`, + ].forEach(markup => { + async_test(t => { + var i = createFrame(` + <script> + // Repeat the message so that the parent can track this frame as the source. + window.onmessage = e => window.parent.postMessage(e.data, '*'); + </scr`+`ipt> + ${markup} + `); + assert_nested_img_loaded(t, i); + }, readableURL(markup)); + }); + + // Nested requests that should fail: + [ + // Newline and brace: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading whitespace: + `<iframe id="dangling" + src=" data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading newline: + `<iframe id="dangling" + src="\ndata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + `<iframe id="dangling" + src="${rawNewline}data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading tab: + `<iframe id="dangling" + src="\tdata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading carrige return: + `<iframe id="dangling" + src="\rdata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + ].forEach(markup => { + async_test(t => { + var i = createFrame(` + <script> + // Repeat the message so that the parent can track this frame as the source. + window.onmessage = e => window.parent.postMessage(e.data, '*'); + </scr`+`ipt> + ${markup} + `); + assert_nested_img_not_loaded(t, i); + }, readableURL(markup)); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation.tentative.html b/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation.tentative.html new file mode 100644 index 0000000000..61a931608b --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/dangling-markup-mitigation.tentative.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function readableURL(url) { + return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); + } + + var should_load = [ + `/images/green-1x1.png`, + `/images/gre\nen-1x1.png`, + `/images/gre\ten-1x1.png`, + `/images/gre\ren-1x1.png`, + `/images/green-1x1.png?img=<`, + `/images/green-1x1.png?img=<`, + `/images/green-1x1.png?img=%3C`, + `/images/gr\neen-1x1.png?img=%3C`, + `/images/gr\reen-1x1.png?img=%3C`, + `/images/gr\teen-1x1.png?img=%3C`, + `/images/green-1x1.png?img= `, + `/images/gr\neen-1x1.png?img= `, + `/images/gr\reen-1x1.png?img= `, + `/images/gr\teen-1x1.png?img= `, + ]; + should_load.forEach(url => async_test(t => { + fetch(url) + .then(t.step_func_done(r => { + assert_equals(r.status, 200); + })) + .catch(t.unreached_func("Fetch should succeed.")); + }, "Fetch: " + readableURL(url))); + + var should_block = [ + `/images/gre\nen-1x1.png?img=<`, + `/images/gre\ren-1x1.png?img=<`, + `/images/gre\ten-1x1.png?img=<`, + `/images/green-1x1.png?<\n=block`, + `/images/green-1x1.png?<\r=block`, + `/images/green-1x1.png?<\t=block`, + ]; + should_block.forEach(url => async_test(t => { + fetch(url) + .then(t.unreached_func("Fetch should fail.")) + .catch(t.step_func_done()); + }, "Fetch: " + readableURL(url))); + + + // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at + // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape + // the content we'd like to treat as "raw" (e.g. `\n` => ` `, `<` => `<`), and + // double-escape the "escaped" content. + var rawBrace = "<"; + var escapedBrace = "&lt;"; + var rawNewline = " "; + var escapedNewline = "&#10;"; + + function appendFrameAndGetElement(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + resolve(frame.contentDocument.querySelector('#dangling')); + }); + document.body.appendChild(frame); + }); + } + + function assert_img_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 1, "Height"); + frame.remove(); + })); + } + + function assert_img_not_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 0, "Height"); + assert_equals(img.naturalWidth, 0, "Width"); + })); + } + + function createFrame(markup) { + var i = document.createElement('iframe'); + i.srcdoc = `${markup}sekrit`; + return i; + } + + // The following resources should not be blocked, as their URLs do not contain both a `\n` and `<` + // character in the body of the URL. + var should_load = [ + // Brace alone doesn't block: + `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}b">`, + + // Newline alone doesn't block: + `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}b">`, + + // Entity-escaped characters don't trigger blocking: + `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${escapedBrace}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b${escapedBrace}c">`, + + // Leading and trailing whitespace is stripped: + ` + <img id="dangling" src=" + /images/green-1x1.png?img= + "> + `, + ` + <img id="dangling" src=" + /images/green-1x1.png?img=${escapedBrace} + "> + `, + ` + <img id="dangling" src=" + /images/green-1x1.png?img=${escapedNewline} + "> + `, + ]; + + should_load.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup} <element attr="" another=''>`); + assert_img_loaded(t, i); + }, readableURL(markup)); + }); + + // The following resources should be blocked, as their URLs contain both `\n` and `<` characters: + var should_block = [ + `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}${rawBrace}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}${rawNewline}b">`, + ` + <img id="dangling" src="/images/green-1x1.png?img= + ${rawBrace} + ${rawNewline}b + "> + `, + ]; + + should_block.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup}`); + assert_img_not_loaded(t, i); + }, readableURL(markup)); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/media.html b/testing/web-platform/tests/fetch/security/dangling-markup/media.html new file mode 100644 index 0000000000..2649edcf32 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/media.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + var resources = {"audio": "/media/sound_5.mp3", "video":"/media/test.mp4"}; + + for (const key in resources){ + async_test(t => { + let elem = document.body.appendChild(document.createElement(key)); + elem.onerror = t.unreached_func(`${key} should load`); + elem.oncanplay = t.step_func(() => { + t.done(); + }); + elem.src = resources[key]; + }, `Should load ${key}`); + + async_test(t => { + let elem = document.body.appendChild(document.createElement(key)); + elem.onerror = t.step_func(() => { + t.done(); + }); + elem.oncanplay = t.unreached_func(`${key} should not load`); + elem.src = resources[key] + "?\n<"; + }, `Should not load ${key} with dangling markup in URL`); + } +</script> diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/option.html b/testing/web-platform/tests/fetch/security/dangling-markup/option.html new file mode 100644 index 0000000000..f528bed999 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/option.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./resources/helper.js"></script> +<body> +<script> + + var tests = [ + ` + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <select name="dangling"><option> + `, + ` + <div> + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <select name="dangling"><option> + `, + ` + <form action="/resource-timing/resources/document-navigated.html" method="post" id="form"> + <input type="submit"> + </form> + <select name="dangling" form="form"><option> + `, + ` + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <select name="dangling"><option label="yay"> + `, + ` + <div> + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <select name="dangling"><option label="yay"> + `, + ` + <form action="/resource-timing/resources/document-navigated.html" method="post" id="form"> + <input type="submit"> + </form> + <select name="dangling" form="form"><option label="yay"> + ` + ]; + + tests.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup}sekrit<element attribute></element>`); + assert_no_submission(t, i); + }, markup.replace(/[\n\r]/g, '')); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/resources/helper.js b/testing/web-platform/tests/fetch/security/dangling-markup/resources/helper.js new file mode 100644 index 0000000000..100bcba7b5 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/resources/helper.js @@ -0,0 +1,63 @@ +function assert_no_message_from_frame(test, frame) { + window.addEventListener("message", test.step_func(e => { + assert_not_equals(e.source, frame.contentWindow); + })); +} + +function appendFrameAndGetElement(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + resolve(frame.contentDocument.querySelector('#dangling')); + }); + document.body.appendChild(frame); + }); +} + +function appendAndSubmit(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + frame.contentDocument.querySelector('form').addEventListener("error", _ => { + resolve("error"); + }); + frame.contentDocument.querySelector('form').addEventListener("submit", _ => { + resolve("submit"); + }); + frame.contentDocument.querySelector('[type=submit]').click(); + }); + document.body.appendChild(frame); + }); +} + +function assert_no_submission(test, frame) { + assert_no_message_from_frame(test, frame); + + appendAndSubmit(test, frame) + .then(test.step_func_done(result => { + assert_equals(result, "error"); + frame.remove(); + })); +} + +function assert_img_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 103, "Height"); + assert_equals(img.naturalWidth, 76, "Width"); + })); +} + +function assert_img_not_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 0, "Height"); + assert_equals(img.naturalWidth, 0, "Width"); + })); +} + +function createFrame(markup) { + var i = document.createElement('iframe'); + i.srcdoc = `${markup}sekrit`; + return i; +} diff --git a/testing/web-platform/tests/fetch/security/dangling-markup/textarea.html b/testing/web-platform/tests/fetch/security/dangling-markup/textarea.html new file mode 100644 index 0000000000..c4b334edc9 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup/textarea.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="./resources/helper.js"></script> +<body> +<script> + + var tests = [ + ` + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <textarea name="dangling"> + `, + ` + <div> + <form action="/resource-timing/resources/document-navigated.html" method="post"> + <input type="submit"> + <textarea name="dangling"> + `, + ` + <form action="/resource-timing/resources/document-navigated.html" method="post" id="form"> + <input type="submit"> + </form> + <textarea name="dangling" form="form"> + ` + ]; + + tests.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup}sekrit<element attribute></element>`); + assert_no_submission(t, i); + }, markup.replace(/[\n\r]/g, '')); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html b/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html new file mode 100644 index 0000000000..ca5ee1c87b --- /dev/null +++ b/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + async_test(t => { + var i = document.createElement('img'); + i.onerror = t.step_func_done(); + i.onload = t.unreached_func("'onload' should not fire."); + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png"; + }, "Embedded credentials are treated as network errors."); + + async_test(t => { + var i = document.createElement('iframe'); + i.src = "./support/embedded-credential-window.sub.html"; + i.onload = t.step_func(_ => { + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + assert_equals(e.data, "Error", "The image should not load."); + i.remove(); + }); + i.contentWindow.postMessage("Hi!", "*", [c.port2]); + }); + document.body.appendChild(i); + }, "Embedded credentials are treated as network errors in frames."); + + async_test(t => { + var w = window.open("./support/embedded-credential-window.sub.html"); + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Error", "The image should not load."); + }); + w.postMessage("absolute", "*", [c.port2]); + })); + }, "Embedded credentials are treated as network errors in new windows."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Load", "The image should load."); + }); + w.postMessage("relative", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are not treated as network errors for relative URLs."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Load", "The image should load."); + }); + w.postMessage("same-origin-matching", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are not treated as network errors for same-origin URLs."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Error", "The image should load."); + }); + w.postMessage("cross-origin-matching", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are treated as network errors for cross-origin URLs."); +</script> diff --git a/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html b/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html new file mode 100644 index 0000000000..b06464805c --- /dev/null +++ b/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html @@ -0,0 +1,68 @@ +<html> +<header> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</header> +<body> +<script> +var host = get_host_info(); + +var sameOriginImageURL = "/common/redirect.py?location=" + host.HTTPS_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true"; +var imageURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true"; +var frameURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/common/blank.html"; + +promise_test((test) => { + return fetch(imageURL, {mode: "no-cors"}); +}, "No CORS fetch after a redirect with an URL containing credentials"); + +promise_test((test) => { + return promise_rejects_js(test, TypeError, fetch(imageURL, {mode: "cors"})); +}, "CORS fetch after a redirect with a cross origin URL containing credentials"); + +promise_test((test) => { + return fetch(sameOriginImageURL, {mode: "cors"}); +}, "CORS fetch after a redirect with a same origin URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.onload = resolve; + image.onerror = (e) => reject(e); + image.src = imageURL; + }); +}, "Image loading after a redirect with an URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.crossOrigin = "use-credentials"; + image.onerror = resolve; + image.onload = () => reject("Image should not load"); + image.src = imageURL; + }); +}, "CORS Image loading after a redirect with a cross origin URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.crossOrigin = "use-credentials"; + image.onload = resolve; + image.onerror = (e) => reject(e); + image.src = sameOriginImageURL; + }); +}, "CORS Image loading after a redirect with a same origin URL containing credentials"); + +promise_test(async (test) => { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise((resolve, reject) => { + iframe.src = frameURL; + iframe.onload = resolve; + iframe.onerror = (e) => reject(e); + }); + document.body.removeChild(iframe); +}, "Frame loading after a redirect with an URL containing credentials"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html b/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html new file mode 100644 index 0000000000..20d307e918 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script> + window.addEventListener("message", e => { + var i = document.createElement('img'); + i.onload = () => { e.ports[0].postMessage("Load"); } + i.onerror = () => { e.ports[0].postMessage("Error"); } + if (e.data == "relative") { + i.src = "/images/green.png"; + } else if (e.data == "same-origin-matching") { + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/green.png"; + } else if (e.data == "cross-origin-matching") { + i.src = "http://user:pass@{{domains[élève]}}:{{ports[http][0]}}/images/red.png"; + } else { + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png"; + } + }); + + (window.opener || window.parent).postMessage("Hi!", "*"); +</script> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/fetch-sw.https.html b/testing/web-platform/tests/fetch/stale-while-revalidate/fetch-sw.https.html new file mode 100644 index 0000000000..efcebc24a6 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/fetch-sw.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Stale Revalidation Requests don't get sent to service worker</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../../service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script src="/common/utils.js"></script> +</head> +<body> +<script> + + // Duplicating this resource to make service worker scoping simpler. + async function setupRegistrationAndWaitToBeControlled(t, scope) { + const controlled = new Promise((resolve) => { + navigator.serviceWorker.oncontrollerchange = () => { resolve(); }; + }); + const reg = await navigator.serviceWorker.register('sw-intercept.js'); + await wait_for_state(t, reg.installing, 'activated'); + await controlled; + add_completion_callback(_ => reg.unregister()); + return reg; + } + + // Using 250ms polling interval to provide enough 'network calmness' to give + // the background low priority revalidation request a chance to kick in. + function wait250ms(test) { + return new Promise(resolve => { + test.step_timeout(() => { + resolve(); + }, 250); + }); + } + + promise_test(async (test) => { + var request_token = token(); + const uri = 'resources/stale-script.py?token=' + request_token; + + await setupRegistrationAndWaitToBeControlled(test, 'resources/stale-script.py'); + + var service_worker_count = 0; + navigator.serviceWorker.addEventListener('message', function once(event) { + if (event.data.endsWith(uri)) { + service_worker_count++; + } + }); + + const response = await fetch(uri); + const response2 = await fetch(uri); + assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id')); + while(true) { + const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token); + if (revalidation_check.headers.get('Count') == '2') { + // The service worker should not see the revalidation request. + assert_equals(service_worker_count, 2); + break; + } + await wait250ms(test); + } + }, 'Second fetch returns same response'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/fetch.any.js b/testing/web-platform/tests/fetch/stale-while-revalidate/fetch.any.js new file mode 100644 index 0000000000..3682b9d2c3 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/fetch.any.js @@ -0,0 +1,32 @@ +// META: global=window,worker +// META: title=Tests Stale While Revalidate is executed for fetch API +// META: script=/common/utils.js + +function wait25ms(test) { + return new Promise(resolve => { + test.step_timeout(() => { + resolve(); + }, 25); + }); +} + +promise_test(async (test) => { + var request_token = token(); + + const response = await fetch(`resources/stale-script.py?token=` + request_token); + // Wait until resource is completely fetched to allow caching before next fetch. + const body = await response.text(); + const response2 = await fetch(`resources/stale-script.py?token=` + request_token); + + assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id')); + const body2 = await response2.text(); + assert_equals(body, body2); + + while(true) { + const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token); + if (revalidation_check.headers.get('Count') == '2') { + break; + } + await wait25ms(test); + } +}, 'Second fetch returns same response'); diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-css.py b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-css.py new file mode 100644 index 0000000000..b87668373a --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-css.py @@ -0,0 +1,28 @@ +def main(request, response): + + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + if is_query: + headers = [(b"Count", count)] + content = b"" + return 200, headers, content + else: + content = b"body { background: rgb(0, 128, 0); }" + if count > 1: + content = b"body { background: rgb(255, 0, 0); }" + + headers = [(b"Content-Type", b"text/css"), + (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60")] + + return 200, headers, content diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-image.py b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-image.py new file mode 100644 index 0000000000..36e6fc0c9b --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-image.py @@ -0,0 +1,40 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + + if is_query: + headers = [(b"Count", count)] + content = b"" + return 200, headers, content + else: + filename = u"green-16x16.png" + if count > 1: + filename = u"green-256x256.png" + + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"../../../images", filename) + body = open(path, "rb").read() + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"content-length", len(body)) + response.writer.write_header(b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60") + response.writer.write_header(b"content-type", b"image/png") + response.writer.end_headers() + + response.writer.write(body) diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-script.py b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-script.py new file mode 100644 index 0000000000..731cd80565 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/resources/stale-script.py @@ -0,0 +1,32 @@ +import random, string + +def id_token(): + letters = string.ascii_lowercase + return b''.join(random.choice(letters).encode("utf-8") for i in range(20)) + +def main(request, response): + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + + if is_query: + headers = [(b"Count", count)] + content = u"" + return 200, headers, content + else: + unique_id = id_token() + headers = [(b"Content-Type", b"text/javascript"), + (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60"), + (b"Unique-Id", unique_id)] + content = b"report('%s')" % unique_id + return 200, headers, content diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html b/testing/web-platform/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html new file mode 100644 index 0000000000..ea70b9a9c7 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test revalidations requests aren't blocked by CSP.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<body> +<script> + +// Regression test for https://crbug.com/1070117. +var request_token = token(); +let image_src = "resources/stale-image.py?token=" + request_token; + +let loadImage = async () => { + let img = document.createElement("img"); + img.src = image_src; + let loaded = new Promise(r => img.onload = r); + document.body.appendChild(img); + await loaded; + return img; +}; + +promise_test(async t => { + await new Promise(r => window.onload = r); + + // No CSP report must be sent from now. + // + // TODO(arthursonzogni): Some browser implementations do not support the + // ReportingObserver yet. Ideally, another way to access the reports should be + // used to test them. + const observer = new ReportingObserver(t.unreached_func( + "CSP reports aren't sent for revalidation requests")); + if (observer) + observer.observe(); + + let img1 = await loadImage(); // Load initial resource. + let img2 = loadImage(); // Request stale resource. + + // Insert a <meta> CSP. This will block any image load starting from now. + const metaCSP = document.createElement("meta"); + metaCSP.httpEquiv = "Content-Security-Policy"; + metaCSP.content = "img-src 'none'"; + document.getElementsByTagName("head")[0].appendChild(metaCSP) + + // The images were requested before the <meta> CSP above was added. So they + // will load. Nevertheless, the resource will be stale. A revalidation request + // is going to be made after that. + assert_equals(img1.width, 16, "(initial version loaded)"); + assert_equals((await img2).width, 16, "(stale version loaded)"); + + // At some point, the <img> resource is going to be revalidated. It must not + // be blocked nor trigger a CSP violation report. + + // Query the server again and again. At some point it must have received the + // revalidation request. We poll, because we don't know when the revalidation + // will occur. + let query = false; + while(true) { + await new Promise(r => step_timeout(r, 25)); + let response = await fetch(`${image_src}${query ? "&query" : ""}`); + let count = response.headers.get("Count"); + if (count == "2") + break; + query ^= true; + } +}, "Request revalidation aren't blocked by CSP"); + +</script> +</body> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/stale-css.html b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-css.html new file mode 100644 index 0000000000..603a60c8bb --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-css.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests Stale While Revalidate works for css</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<body> +<script> + +var request_token = token(); +let link_src = "./resources/stale-css.py?token=" + request_token; + +let loadLink = async() => { + let link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = "resources/stale-css.py?token=" + request_token; + let loaded = new Promise(r => link.onload = r); + document.body.appendChild(link); + await loaded; + return window + .getComputedStyle(document.body) + .getPropertyValue('background-color'); +}; + +promise_test(async t => { + await new Promise(r => window.onload = r); + + let bgColor1 = await loadLink(); + assert_equals(bgColor1, "rgb(0, 128, 0)", "(initial version loaded)"); + + let bgColor2 = await loadLink(); + assert_equals(bgColor2, "rgb(0, 128, 0)", "(stale version loaded)"); + + // Query the server again and again. At some point it must have received the + // revalidation request. We poll, because we don't know when the revalidation + // will occur. + while(true) { + await new Promise(r => step_timeout(r, 25)); + let response = await fetch(link_src + "&query"); + let count = response.headers.get("Count"); + if (count == '2') + break; + } + + let bgColor3 = await loadLink(); + assert_equals(bgColor3, "rgb(255, 0, 0)", "(revalidated version loaded)"); +}, 'Cache returns stale resource'); + +</script> +</body> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/stale-image.html b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-image.html new file mode 100644 index 0000000000..d86bdfbde2 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-image.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests Stale While Revalidate works for images</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<body> +<!-- +Use a child document to load the second stale image into because +an image loaded into the same document will skip cache-control headers. +See: https://html.spec.whatwg.org/#the-list-of-available-images +--> +<iframe id="child1" srcdoc=""></iframe> +<iframe id="child2" srcdoc=""></iframe> +<script> + +var request_token = token(); +let image_src = "resources/stale-image.py?token=" + request_token; + +let loadImage = async (document) => { + let img = document.createElement("img"); + img.src = image_src; + let loaded = new Promise(r => img.onload = r); + document.body.appendChild(img); + await loaded; + return img; +}; + +promise_test(async t => { + await new Promise(r => window.onload = r); + + let img1 = await loadImage(document); + assert_equals(img1.width, 16, "(initial version loaded)"); + + let img2 = await loadImage(child1.contentDocument); + assert_equals(img2.width, 16, "(stale version loaded)"); + + // Query the server again and again. At some point it must have received the + // revalidation request. We poll, because we don't know when the revalidation + // will occur. + while(true) { + await new Promise(r => step_timeout(r, 25)); + let response = await fetch(image_src + "&query"); + let count = response.headers.get("Count"); + if (count == '2') + break; + } + + let img3 = await loadImage(child2.contentDocument); + assert_equals(img3.width, 256, "(revalidated version loaded)"); + +}, 'Cache returns stale resource'); + +</script> +</body> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/stale-script.html b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-script.html new file mode 100644 index 0000000000..f5317482c4 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/stale-script.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests Stale While Revalidate works for scripts</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<body> +<script> + +const request_token = token(); +const script_src = "./resources/stale-script.py?token=" + request_token; + +// The script above will call report() via a uniquely generated ID on the +// subresource. If it is a cache hit, the ID will be the same and +// |last_modified_count| won't be incremented. +let last_modified; +let last_modified_count = 0; +function report(mod) { + if (last_modified == mod) + return; + last_modified = mod; + last_modified_count++; +} + +let loadScript = async () => { + let script = document.createElement("script"); + let script_loaded = new Promise(r => script.onload = r); + script.src = script_src; + document.body.appendChild(script); + await script_loaded; +}; + +promise_test(async t => { + await new Promise(r => window.onload = r); + + await loadScript(); + assert_equals(last_modified_count, 1, '(initial version loaded)'); + + await loadScript(); + assert_equals(last_modified_count, 1, '(stale version loaded)'); + + // Query the server again and again. At some point it must have received the + // revalidation request. We poll, because we don't know when the revalidation + // will occur. + while(true) { + await new Promise(r => step_timeout(r, 25)); + let response = await fetch(script_src + "&query"); + let count = response.headers.get("Count"); + if (count == '2') + break; + } + + await loadScript(); + assert_equals(last_modified_count, 2, '(revalidated version loaded)'); + +}, 'Cache returns stale resource'); + +</script> +</body> diff --git a/testing/web-platform/tests/fetch/stale-while-revalidate/sw-intercept.js b/testing/web-platform/tests/fetch/stale-while-revalidate/sw-intercept.js new file mode 100644 index 0000000000..dca7de51b0 --- /dev/null +++ b/testing/web-platform/tests/fetch/stale-while-revalidate/sw-intercept.js @@ -0,0 +1,14 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +self.addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', event => { + self.clients.claim(); +}); |