diff options
Diffstat (limited to 'dom/tests/mochitest/fetch')
61 files changed, 6576 insertions, 0 deletions
diff --git a/dom/tests/mochitest/fetch/common_readableStreams.js b/dom/tests/mochitest/fetch/common_readableStreams.js new file mode 100644 index 0000000000..0a101cc580 --- /dev/null +++ b/dom/tests/mochitest/fetch/common_readableStreams.js @@ -0,0 +1,414 @@ +const SAME_COMPARTMENT = "same-compartment"; +const IFRAME_COMPARTMENT = "iframe-compartment"; +const BIG_BUFFER_SIZE = 1000000; +const ITER_MAX = 10; + +function makeBuffer(size) { + let buffer = new Uint8Array(size); + buffer.fill(42); + + let value = 0; + for (let i = 0; i < 1000000; i += 1000) { + buffer.set([++value % 255], i); + } + + return buffer; +} + +function apply_compartment(compartment, data) { + if (compartment == SAME_COMPARTMENT) { + return self[data.func](data.args, self); + } + + if (compartment == IFRAME_COMPARTMENT) { + const iframe = document.querySelector("#iframe").contentWindow; + return iframe[data.func](data.args, self); + } + + ok(false, "Invalid compartment value"); + return undefined; +} + +async function test_nativeStream(compartment) { + info("test_nativeStream"); + + let r = await fetch("/"); + + return apply_compartment(compartment, { + func: "test_nativeStream_continue", + args: r, + }); +} + +async function test_nativeStream_continue(r, that) { + that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let a = r.clone(); + that.ok(a instanceof that.Response, "We have a cloned Response"); + that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let b = a.clone(); + that.ok(b instanceof that.Response, "We have a cloned Response"); + that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let blob = await r.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + let d = await a.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + blob = await b.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); +} + +async function test_timeout(compartment) { + info("test_timeout"); + + let blob = new Blob([""]); + let r = await fetch(URL.createObjectURL(blob)); + + return apply_compartment(compartment, { + func: "test_timeout_continue", + args: r, + }); +} + +async function test_timeout_continue(r, that) { + await r.body.getReader().read(); + + await new Promise(resolve => setTimeout(resolve, 0)); + + try { + await r.blob(); + that.ok(false, "We cannot have a blob here!"); + } catch (exc) { + that.ok(true, "We cannot have a blob here!"); + } +} + +async function test_nonNativeStream(compartment) { + info("test_nonNativeStream"); + + let buffer = makeBuffer(BIG_BUFFER_SIZE); + info("Buffer size: " + buffer.byteLength); + + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(buffer); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_nonNativeStream_continue", + args: { r, buffer }, + }); +} + +async function test_nonNativeStream_continue(data, that) { + that.ok( + data.r.body instanceof that.ReadableStream, + "We have a ReadableStream" + ); + + let a = data.r.clone(); + that.ok(a instanceof that.Response, "We have a cloned Response"); + that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let b = a.clone(); + that.ok(b instanceof that.Response, "We have a cloned Response"); + that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let blob = await data.r.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + let d = await a.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + blob = await b.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + that.is(blob.size, data.buffer.byteLength, "Blob size matches"); +} + +async function test_noUint8Array(compartment) { + info("test_noUint8Array"); + + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue("hello world!"); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_noUint8Array_continue", + args: r, + }); +} + +async function test_noUint8Array_continue(r, that) { + that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream"); + + try { + await r.blob(); + that.ok(false, "We cannot have a blob here!"); + } catch (ex) { + that.ok(true, "We cannot have a blob here!"); + } +} + +async function test_pendingStream(compartment) { + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(makeBuffer(BIG_BUFFER_SIZE)); + // Let's keep this controler open. + self.ccc = controller; + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_pendingStream_continue", + args: r, + }); +} + +async function test_pendingStream_continue(r, that) { + let d = await r.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + + if ("close" in that) { + that.close(); + } +} + +async function test_nativeStream_cache(compartment) { + info("test_nativeStream_cache"); + + let origBody = "123456789abcdef"; + let url = "/nativeStream"; + + let cache = await caches.open("nativeStream"); + + info("Storing a body as a string"); + await cache.put(url, new Response(origBody)); + + return apply_compartment(compartment, { + func: "test_nativeStream_cache_continue", + args: { caches, cache, url, origBody }, + }); +} + +async function test_nativeStream_cache_continue(data, that) { + that.info("Retrieving the stored value"); + let cacheResponse = await data.cache.match(data.url); + + that.info("Converting the response to text"); + let cacheBody = await cacheResponse.text(); + + that.is(data.origBody, cacheBody, "Bodies match"); + + await data.caches.delete("nativeStream"); +} + +async function test_nonNativeStream_cache(compartment) { + info("test_nonNativeStream_cache"); + + let url = "/nonNativeStream"; + + let cache = await caches.open("nonNativeStream"); + let buffer = makeBuffer(BIG_BUFFER_SIZE); + info("Buffer size: " + buffer.byteLength); + + info("Storing a body as a string"); + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(buffer); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_nonNativeStream_cache_continue", + args: { caches, cache, buffer, r }, + }); +} + +async function test_nonNativeStream_cache_continue(data, that) { + await data.cache.put(data.url, data.r); + + that.info("Retrieving the stored value"); + let cacheResponse = await data.cache.match(data.url); + + that.info("Converting the response to text"); + let cacheBody = await cacheResponse.arrayBuffer(); + + that.ok(cacheBody instanceof that.ArrayBuffer, "Body is an array buffer"); + that.is(cacheBody.byteLength, BIG_BUFFER_SIZE, "Body length is correct"); + + let value = 0; + for (let i = 0; i < 1000000; i += 1000) { + that.is( + new Uint8Array(cacheBody)[i], + ++value % 255, + "byte in position " + i + " is correct" + ); + } + + await data.caches.delete("nonNativeStream"); +} + +async function test_codeExecution(compartment) { + info("test_codeExecution"); + + let r = new Response( + new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + console.log("pull called"); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_codeExecution_continue", + args: r, + }); +} + +// This is intended to just be a drop-in replacement for an old observer +// notification. +function addConsoleStorageListener(listener) { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + listener.__handler = (message, id) => { + listener.observe(message, id); + }; + ConsoleAPIStorage.addLogEventListener( + listener.__handler, + SpecialPowers.wrap(document).nodePrincipal + ); +} + +function removeConsoleStorageListener(listener) { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + ConsoleAPIStorage.removeLogEventListener(listener.__handler); +} + +async function test_codeExecution_continue(r, that) { + function consoleListener() { + addConsoleStorageListener(this); + } + + var promise = new Promise(resolve => { + consoleListener.prototype = { + observe(aSubject) { + that.ok(true, "Something has been received"); + + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] && obj.arguments[0] === "pull called") { + that.ok(true, "Message received!"); + removeConsoleStorageListener(this); + resolve(); + } + }, + }; + }); + + var cl = new consoleListener(); + + r.body.getReader().read(); + await promise; +} + +async function test_global(compartment) { + info("test_global: " + compartment); + + self.foo = 42; + self.iter = ITER_MAX; + + let r = new Response( + new ReadableStream({ + start(c) { + self.controller = c; + }, + pull() { + if (!("iter" in self) || self.iter < 0 || self.iter > ITER_MAX) { + throw "Something bad is happening here!"; + } + + let buffer = new Uint8Array(1); + buffer.fill(self.foo); + self.controller.enqueue(buffer); + + if (--self.iter == 0) { + controller.close(); + } + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_global_continue", + args: r, + }); +} + +async function test_global_continue(r, that) { + let a = await r.arrayBuffer(); + + that.is( + Object.getPrototypeOf(a), + that.ArrayBuffer.prototype, + "Body is an array buffer" + ); + that.is(a.byteLength, ITER_MAX, "Body length is correct"); + + for (let i = 0; i < ITER_MAX; ++i) { + that.is(new Uint8Array(a)[i], 42, "Byte " + i + " is correct"); + } +} + +function workify(func) { + info("Workifying " + func); + + return new Promise((resolve, reject) => { + let worker = new Worker("worker_readableStreams.js"); + worker.postMessage(func); + worker.onmessage = function (e) { + if (e.data.type == "done") { + resolve(); + return; + } + + if (e.data.type == "error") { + reject(e.data.message); + return; + } + + if (e.data.type == "test") { + ok(e.data.test, e.data.message); + return; + } + + if (e.data.type == "info") { + info(e.data.message); + } + }; + }); +} diff --git a/dom/tests/mochitest/fetch/common_temporaryFileBlob.js b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js new file mode 100644 index 0000000000..51aed67cf2 --- /dev/null +++ b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js @@ -0,0 +1,146 @@ +var data = new Array(256).join("1234567890ABCDEF"); + +function test_fetch_basic() { + info("Simple fetch test"); + + fetch("/tests/dom/xhr/tests/temporaryFileBlob.sjs", { + method: "POST", + body: data, + }) + .then(response => { + return response.blob(); + }) + .then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_fetch_worker() { + generic_worker_test("fetch in workers", "fetch"); +} + +function test_xhr_basic() { + info("Simple XHR test"); + + let xhr = new XMLHttpRequest(); + xhr.responseType = "blob"; + xhr.open("POST", "/tests/dom/xhr/tests/temporaryFileBlob.sjs"); + xhr.send(data); + + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + let blob = xhr.response; + + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + } + }; +} + +function test_xhr_worker() { + generic_worker_test("XHR in workers", "xhr"); +} + +function test_response_basic() { + info("Response"); + + let r = new Response(data); + r.blob().then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_response_worker() { + generic_worker_test("Response in workers", "response"); +} + +function test_request_basic() { + info("Request"); + + let r = new Request("https://example.com", { body: data, method: "POST" }); + r.blob().then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_request_worker() { + generic_worker_test("Request in workers", "request"); +} + +function generic_worker_test(title, what) { + info(title); + + var w = new Worker("worker_temporaryFileBlob.js"); + w.onmessage = function (e) { + if (e.data.type == "info") { + info(e.data.msg); + } else if (e.data.type == "check") { + ok(e.data.what, e.data.msg); + } else if (e.data.type == "finish") { + next(); + } else { + ok(false, "Something wrong happened"); + } + }; + + w.postMessage(what); +} diff --git a/dom/tests/mochitest/fetch/empty.js b/dom/tests/mochitest/fetch/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/tests/mochitest/fetch/empty.js diff --git a/dom/tests/mochitest/fetch/empty.js^headers^ b/dom/tests/mochitest/fetch/empty.js^headers^ new file mode 100644 index 0000000000..d0b9633bb0 --- /dev/null +++ b/dom/tests/mochitest/fetch/empty.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/fetch_test_framework.js b/dom/tests/mochitest/fetch/fetch_test_framework.js new file mode 100644 index 0000000000..71401ba921 --- /dev/null +++ b/dom/tests/mochitest/fetch/fetch_test_framework.js @@ -0,0 +1,166 @@ +function testScript(script) { + var mode = location.href.includes("http2") ? "?mode=http2&" : "?"; + function makeWrapperUrl(wrapper) { + return wrapper + mode + "script=" + script; + } + let workerWrapperUrl = makeWrapperUrl("worker_wrapper.js"); + + // The framework runs the entire test in many different configurations. + // On slow platforms and builds this can make the tests likely to + // timeout while they are still running. Lengthen the timeout to + // accomodate this. + SimpleTest.requestLongerTimeout(4); + + // reroute.html should have set this variable if a service worker is present! + if (!("isSWPresent" in window)) { + window.isSWPresent = false; + } + + function setupPrefs() { + return new Promise(function (resolve, reject) { + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.idle_timeout", 60000], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ], + }, + resolve + ); + }); + } + + function workerTest() { + return new Promise(function (resolve, reject) { + var worker = new Worker(workerWrapperUrl); + worker.onmessage = function (event) { + if (event.data.context != "Worker") { + return; + } + if (event.data.type == "finish") { + resolve(); + } else if (event.data.type == "status") { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }; + worker.onerror = function (event) { + reject("Worker error: " + event.message); + }; + + worker.postMessage({ script }); + }); + } + + function nestedWorkerTest() { + return new Promise(function (resolve, reject) { + var worker = new Worker(makeWrapperUrl("nested_worker_wrapper.js")); + worker.onmessage = function (event) { + if (event.data.context != "NestedWorker") { + return; + } + if (event.data.type == "finish") { + resolve(); + } else if (event.data.type == "status") { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }; + worker.onerror = function (event) { + reject("Nested Worker error: " + event.message); + }; + + worker.postMessage({ script }); + }); + } + + function serviceWorkerTest() { + var isB2G = + !navigator.userAgent.includes("Android") && + /Mobile|Tablet/.test(navigator.userAgent); + if (isB2G) { + // TODO B2G doesn't support running service workers for now due to bug 1137683. + dump("Skipping running the test in SW until bug 1137683 gets fixed.\n"); + return Promise.resolve(); + } + return new Promise(function (resolve, reject) { + function setupSW(registration) { + var worker = + registration.installing || + registration.waiting || + registration.active; + var iframe; + + window.addEventListener("message", function onMessage(event) { + if (event.data.context != "ServiceWorker") { + return; + } + if (event.data.type == "finish") { + window.removeEventListener("message", onMessage); + iframe.remove(); + registration.unregister().then(resolve).catch(reject); + } else if (event.data.type == "status") { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }); + + worker.onerror = reject; + + iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function () { + worker.postMessage({ script }); + }; + document.body.appendChild(iframe); + } + + navigator.serviceWorker + .register(workerWrapperUrl, { scope: "." }) + .then(setupSW); + }); + } + + function windowTest() { + return new Promise(function (resolve, reject) { + var scriptEl = document.createElement("script"); + scriptEl.setAttribute("src", script); + scriptEl.onload = function () { + runTest().then(resolve, reject); + }; + document.body.appendChild(scriptEl); + }); + } + + SimpleTest.waitForExplicitFinish(); + // We have to run the window, worker and service worker tests sequentially + // since some tests set and compare cookies and running in parallel can lead + // to conflicting values. + setupPrefs() + .then(function () { + return windowTest(); + }) + .then(function () { + return workerTest(); + }) + .then(function () { + return nestedWorkerTest(); + }) + .then(function () { + return serviceWorkerTest(); + }) + .catch(function (e) { + ok(false, "Some test failed in " + script); + info(e); + info(e.message); + return Promise.resolve(); + }) + .then(function () { + try { + if (parent && parent.finishTest) { + parent.finishTest(); + return; + } + } catch {} + SimpleTest.finish(); + }); +} diff --git a/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html new file mode 100644 index 0000000000..64e3289892 --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html @@ -0,0 +1 @@ +<html><body>My contents don't matter. Only my header matters!</body></html> diff --git a/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ new file mode 100644 index 0000000000..eee464d0eb --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ @@ -0,0 +1,3 @@ +HTTP 302 Redirect +Location: //example.org/target_does_not_matter.html +Cache-Control: max-age=10 diff --git a/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html new file mode 100644 index 0000000000..793575f45c --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<script> +addEventListener('message', evt => { + let url = '/tests/dom/security/test/csp/file_redirects_resource.sjs?redir=other&res=xhr-resp'; + fetch(url).then(response => { + parent.postMessage('RESOLVED', '*'); + }).catch(error => { + parent.postMessage('REJECTED', '*'); + }); +}, { once: true }); +</script> +</html> diff --git a/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ new file mode 100644 index 0000000000..4c43573eb7 --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Security-Policy: connect-src 'self' diff --git a/dom/tests/mochitest/fetch/file_fetch_observer.html b/dom/tests/mochitest/fetch/file_fetch_observer.html new file mode 100644 index 0000000000..480198fa6f --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_observer.html @@ -0,0 +1,146 @@ +<script> +function ok(a, msg) { + parent.postMessage({ type: "check", status: !!a, message: msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function testObserver() { + ok("FetchObserver" in self, "We have a FetchObserver prototype"); + + fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', { observe: o => { + ok(!!o, "We have an observer"); + ok(o instanceof FetchObserver, "The correct object has been passed"); + is(o.state, "requesting", "By default the state is requesting"); + next(); + }}); +} + +function testObserveAbort() { + var ac = new AbortController(); + + fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', { + signal: ac.signal, + observe: o => { + o.onstatechange = () => { + ok(true, "StateChange event dispatched"); + if (o.state == "aborted") { + ok(true, "Aborted!"); + next(); + } + } + ac.abort(); + } + }); +} + +function testObserveComplete() { + var ac = new AbortController(); + + fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', { + signal: ac.signal, + observe: o => { + o.onstatechange = () => { + ok(true, "StateChange event dispatched"); + if (o.state == "complete") { + ok(true, "Operation completed"); + next(); + } + } + } + }); +} + +function testObserveErrored() { + var ac = new AbortController(); + + fetch('foo: bar', { + signal: ac.signal, + observe: o => { + o.onstatechange = () => { + ok(true, "StateChange event dispatched"); + if (o.state == "errored") { + ok(true, "Operation completed"); + next(); + } + } + } + }); +} + +function testObserveResponding() { + var ac = new AbortController(); + + fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', { + signal: ac.signal, + observe: o => { + o.onstatechange = () => { + if (o.state == "responding") { + ok(true, "We have responding events"); + next(); + } + } + } + }); +} + +function workify(worker) { + function methods() { + function ok(a, msg) { + postMessage( { type: 'check', state: !!a, message: msg }); + }; + function is(a, b, msg) { + postMessage( { type: 'check', state: a === b, message: msg }); + }; + function next() { + postMessage( { type: 'finish' }); + }; + } + + var str = methods.toString(); + var methodsContent = str.substring(0, str.length - 1).split('\n').splice(1).join('\n'); + + str = worker.toString(); + var workerContent = str.substring(0, str.length - 1).split('\n').splice(1).join('\n'); + + var content = methodsContent + workerContent; + var url = URL.createObjectURL(new Blob([content], { type: "application/javascript" })); + var w = new Worker(url); + w.onmessage = e => { + if (e.data.type == 'check') { + ok(e.data.state, "WORKER: " + e.data.message); + } else if (e.data.type == 'finish') { + next(); + } else { + ok(false, "Something went wrong"); + } + } +} + +var steps = [ + testObserver, + testObserveAbort, + function() { workify(testObserveAbort); }, + testObserveComplete, + function() { workify(testObserveComplete); }, + testObserveErrored, + function() { workify(testObserveErrored); }, + testObserveResponding, + function() { workify(testObserveResponding); }, +]; + +function next() { + if (!steps.length) { + parent.postMessage({ type: "finish" }, "*"); + return; + } + + var step = steps.shift(); + step(); +} + +next(); + +</script> diff --git a/dom/tests/mochitest/fetch/iframe_readableStreams.html b/dom/tests/mochitest/fetch/iframe_readableStreams.html new file mode 100644 index 0000000000..11a3838789 --- /dev/null +++ b/dom/tests/mochitest/fetch/iframe_readableStreams.html @@ -0,0 +1,4 @@ +<script type="application/javascript" src="common_readableStreams.js"></script> +<script> +parent.runTests(); +</script> diff --git a/dom/tests/mochitest/fetch/message_receiver.html b/dom/tests/mochitest/fetch/message_receiver.html new file mode 100644 index 0000000000..82cb587c72 --- /dev/null +++ b/dom/tests/mochitest/fetch/message_receiver.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> diff --git a/dom/tests/mochitest/fetch/mochitest.toml b/dom/tests/mochitest/fetch/mochitest.toml new file mode 100644 index 0000000000..ec4aaf4b1a --- /dev/null +++ b/dom/tests/mochitest/fetch/mochitest.toml @@ -0,0 +1,165 @@ +[DEFAULT] +tags = "condprof" +support-files = [ + "fetch_test_framework.js", + "file_fetch_cached_redirect.html", + "file_fetch_cached_redirect.html^headers^", + "file_fetch_csp_block_frame.html", + "file_fetch_csp_block_frame.html^headers^", + "test_fetch_basic.js", + "test_fetch_basic_http.js", + "test_fetch_cached_redirect.js", + "test_fetch_cors.js", + "file_fetch_observer.html", + "test_formdataparsing.js", + "test_headers_common.js", + "test_request.js", + "test_response.js", + "utils.js", + "nested_worker_wrapper.js", + "worker_wrapper.js", + "message_receiver.html", + "reroute.html", + "reroute.js", + "reroute.js^headers^", + "slow.sjs", + "sw_reroute.js", + "empty.js", + "empty.js^headers^", + "worker_temporaryFileBlob.js", + "common_temporaryFileBlob.js", + "common_readableStreams.js", + "worker_readableStreams.js", + "iframe_readableStreams.html", + "!/dom/xhr/tests/file_XHR_binary1.bin", + "!/dom/xhr/tests/file_XHR_binary1.bin^headers^", + "!/dom/xhr/tests/file_XHR_binary2.bin", + "!/dom/xhr/tests/file_XHR_pass1.xml", + "!/dom/xhr/tests/file_XHR_pass2.txt", + "!/dom/xhr/tests/file_XHR_pass3.txt", + "!/dom/xhr/tests/file_XHR_pass3.txt^headers^", + "!/dom/xhr/tests/responseIdentical.sjs", + "!/dom/xhr/tests/temporaryFileBlob.sjs", + "!/dom/html/test/form_submit_server.sjs", + "!/dom/security/test/cors/file_CrossSiteXHR_server.sjs", + "!/dom/security/test/csp/file_redirects_resource.sjs", + "!/dom/base/test/referrer_helper.js", + "!/dom/base/test/referrer_testserver.sjs", +] + +["test_fetch_basic.html"] + +["test_fetch_basic_http.html"] +skip-if = [ + "http2", + "http3", +] + +["test_fetch_basic_http2.html"] +run-if = [ + "http2", + "http3", +] + +["test_fetch_basic_http2_sw_empty_reroute.html"] +run-if = [ + "http2", + "http3", +] + +["test_fetch_basic_http2_sw_reroute.html"] +run-if = [ + "http2", + "http3", +] + +["test_fetch_basic_http_sw_empty_reroute.html"] +skip-if = [ + "http2", + "http3", +] + +["test_fetch_basic_http_sw_reroute.html"] +skip-if = [ + "http2", + "http3", +] + +["test_fetch_basic_sw_empty_reroute.html"] + +["test_fetch_basic_sw_reroute.html"] + +["test_fetch_cached_redirect.html"] + +["test_fetch_cors.html"] +skip-if = [ + "http3", + "http2", +] + +["test_fetch_cors_sw_empty_reroute.html"] +skip-if = [ + "os == 'android'", # Bug 1623134 + "http3", + "http2", +] + +["test_fetch_cors_sw_reroute.html"] +skip-if = [ + "os == 'android'", # Bug 1623134 + "http3", + "http2", +] + +["test_fetch_csp_block.html"] + +["test_fetch_observer.html"] +skip-if = [ + "http3", + "http2", +] + +["test_fetch_user_control_rp.html"] +skip-if = [ + "http3", + "http2", +] + +["test_formdataparsing.html"] + +["test_formdataparsing_sw_reroute.html"] + +["test_headers.html"] + +["test_headers_mainthread.html"] + +["test_headers_sw_reroute.html"] + +["test_readableStreams.html"] +scheme = "https" +skip-if = [ + "http3", + "http2", +] + +["test_request.html"] + +["test_request_context.html"] + +["test_request_sw_reroute.html"] + +["test_response.html"] +skip-if = [ + "http3", + "http2", +] + +["test_responseReadyForWasm.html"] + +["test_response_sw_reroute.html"] +skip-if = [ + "http3", + "http2", +] + +["test_temporaryFileBlob.html"] diff --git a/dom/tests/mochitest/fetch/nested_worker_wrapper.js b/dom/tests/mochitest/fetch/nested_worker_wrapper.js new file mode 100644 index 0000000000..00cf627a25 --- /dev/null +++ b/dom/tests/mochitest/fetch/nested_worker_wrapper.js @@ -0,0 +1,32 @@ +// Hold the nested worker alive until this parent worker closes. +var worker; + +var searchParams = new URL(location.href).searchParams; + +addEventListener("message", function nestedWorkerWrapperOnMessage(evt) { + removeEventListener("message", nestedWorkerWrapperOnMessage); + + var mode = searchParams.get("mode"); + var script = searchParams.get("script"); + worker = new Worker(`worker_wrapper.js?mode=${mode}&script=${script}`); + + worker.addEventListener("message", function (evt) { + self.postMessage({ + context: "NestedWorker", + type: evt.data.type, + status: evt.data.status, + msg: evt.data.msg, + }); + }); + + worker.addEventListener("error", function (evt) { + self.postMessage({ + context: "NestedWorker", + type: "status", + status: false, + msg: "Nested worker error: " + evt.message, + }); + }); + + worker.postMessage(evt.data); +}); diff --git a/dom/tests/mochitest/fetch/reroute.html b/dom/tests/mochitest/fetch/reroute.html new file mode 100644 index 0000000000..087d640946 --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script> +["SimpleTest", "ok", "info", "is", "$"] + .forEach((v) => window[v] = window.parent[v]); +</script> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script> +// If we are using the empty service worker then requests won't actually +// get intercepted and response URLs will reflect redirects. This means +// all our checks should use the "no sw" logic. Otherwise we need to +// note that interceptions are taking place so we can adjust our +// response URL expectations. +if (!navigator.serviceWorker.controller.scriptURL.endsWith('empty.js')) { + window.isSWPresent = true; +} + +var searchParams = new URL(location.href).searchParams; +var mode = searchParams.get("mode"); +var script = searchParams.get("script"); +testScript(`${script}.js?mode=${mode}`); +</script> diff --git a/dom/tests/mochitest/fetch/reroute.js b/dom/tests/mochitest/fetch/reroute.js new file mode 100644 index 0000000000..a4f309d780 --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js @@ -0,0 +1,27 @@ +onfetch = function (e) { + if (e.request.url.includes("Referer")) { + // Silently rewrite the referrer so the referrer test passes since the + // document/worker isn't aware of this service worker. + var url = e.request.url.substring(0, e.request.url.indexOf("?")); + url += "?headers=" + JSON.stringify({ Referer: self.location.href }); + + e.respondWith( + e.request.text().then(function (text) { + var body = text === "" ? undefined : text; + var mode = + e.request.mode == "navigate" ? "same-origin" : e.request.mode; + return fetch(url, { + method: e.request.method, + headers: e.request.headers, + body, + mode, + credentials: e.request.credentials, + redirect: e.request.redirect, + cache: e.request.cache, + }); + }) + ); + return; + } + e.respondWith(fetch(e.request)); +}; diff --git a/dom/tests/mochitest/fetch/reroute.js^headers^ b/dom/tests/mochitest/fetch/reroute.js^headers^ new file mode 100644 index 0000000000..d0b9633bb0 --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/slow.sjs b/dom/tests/mochitest/fetch/slow.sjs new file mode 100644 index 0000000000..8f44798ad5 --- /dev/null +++ b/dom/tests/mochitest/fetch/slow.sjs @@ -0,0 +1,13 @@ +function handleRequest(request, response) { + response.processAsync(); + + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + function () { + response.write("Here the content. But slowly."); + response.finish(); + }, + 1000, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/tests/mochitest/fetch/sw_reroute.js b/dom/tests/mochitest/fetch/sw_reroute.js new file mode 100644 index 0000000000..4bcd709c07 --- /dev/null +++ b/dom/tests/mochitest/fetch/sw_reroute.js @@ -0,0 +1,47 @@ +var gRegistration; +var iframe; + +function testScript(script) { + var mode = location.href.includes("http2") ? "?mode=http2&" : "?"; + var scope = "./reroute.html" + mode + "script=" + script.replace(".js", ""); + function setupSW(registration) { + gRegistration = registration; + + iframe = document.createElement("iframe"); + iframe.src = scope; + document.body.appendChild(iframe); + } + + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.idle_timeout", 60000], + ], + }, + function () { + var scriptURL = location.href.includes("sw_empty_reroute.html") + ? "empty.js" + : "reroute.js"; + if (location.href.includes("http2")) { + scriptURL += "?mode=http2"; + } + navigator.serviceWorker + .register(scriptURL, { scope }) + .then(swr => waitForState(swr.installing, "activated", swr)) + .then(setupSW); + } + ); +} + +function finishTest() { + iframe.remove(); + gRegistration.unregister().then(SimpleTest.finish, function (e) { + dump("unregistration failed: " + e + "\n"); + SimpleTest.finish(); + }); +} + +SimpleTest.waitForExplicitFinish(); diff --git a/dom/tests/mochitest/fetch/test_fetch_basic.html b/dom/tests/mochitest/fetch/test_fetch_basic.html new file mode 100644 index 0000000000..7f3536c92e --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() function in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic.js b/dom/tests/mochitest/fetch/test_fetch_basic.js new file mode 100644 index 0000000000..27343d8662 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic.js @@ -0,0 +1,179 @@ +function testAboutURL() { + var p1 = fetch("about:blank").then( + function (res) { + ok(false, "about:blank should fail"); + }, + function (e) { + ok(e instanceof TypeError, "about:blank should fail"); + } + ); + + var p2 = fetch("about:config").then( + function (res) { + ok(false, "about:config should fail"); + }, + function (e) { + ok(e instanceof TypeError, "about:config should fail"); + } + ); + + return Promise.all([p1, p2]); +} + +function testDataURL() { + return Promise.all( + [ + [ + "data:text/plain;charset=UTF-8,Hello", + "text/plain;charset=UTF-8", + "Hello", + ], + [ + "data:text/plain;charset=utf-8;base64,SGVsbG8=", + "text/plain;charset=utf-8", + "Hello", + ], + [ + "data:text/xml,%3Cres%3Ehello%3C/res%3E%0A", + "text/xml", + "<res>hello</res>\n", + ], + ["data:text/plain,hello%20pass%0A", "text/plain", "hello pass\n"], + ["data:,foo", "text/plain;charset=US-ASCII", "foo"], + ["data:text/plain;base64,Zm9v", "text/plain", "foo"], + ["data:text/plain,foo#bar", "text/plain", "foo"], + ["data:text/plain,foo%23bar", "text/plain", "foo#bar"], + ].map(test => { + var uri = test[0], + contentType = test[1], + expectedBody = test[2]; + return fetch(uri).then(res => { + ok(true, "Data URL fetch should resolve"); + if (res.type == "error") { + ok(false, "Data URL fetch should not fail."); + return Promise.reject(); + } + ok(res instanceof Response, "Fetch should resolve to a Response"); + is(res.status, 200, "Data URL status should be 200"); + is(res.statusText, "OK", "Data URL statusText should be OK"); + ok( + res.headers.has("content-type"), + "Headers must have Content-Type header" + ); + is( + res.headers.get("content-type"), + contentType, + "Content-Type header should match specified value" + ); + return res + .text() + .then(body => is(body, expectedBody, "Data URL Body should match")); + }); + }) + ); +} + +function testSameOriginBlobURL() { + var blob = new Blob(["english ", "sentence"], { type: "text/plain" }); + var url = URL.createObjectURL(blob); + return fetch(url).then(function (res) { + URL.revokeObjectURL(url); + ok(true, "Blob URL fetch should resolve"); + if (res.type == "error") { + ok(false, "Blob URL fetch should not fail."); + return Promise.reject(); + } + ok(res instanceof Response, "Fetch should resolve to a Response"); + is(res.status, 200, "Blob fetch status should be 200"); + is(res.statusText, "OK", "Blob fetch statusText should be OK"); + ok( + res.headers.has("content-type"), + "Headers must have Content-Type header" + ); + is( + res.headers.get("content-type"), + blob.type, + "Content-Type header should match specified value" + ); + ok( + res.headers.has("content-length"), + "Headers must have Content-Length header" + ); + is( + parseInt(res.headers.get("content-length")), + 16, + "Content-Length should match Blob's size" + ); + return res.text().then(function (body) { + is(body, "english sentence", "Blob fetch body should match"); + }); + }); +} + +function testNonGetBlobURL() { + var blob = new Blob(["english ", "sentence"], { type: "text/plain" }); + var url = URL.createObjectURL(blob); + return Promise.all( + ["HEAD", "POST", "PUT", "DELETE"].map(method => { + var req = new Request(url, { method }); + return fetch(req) + .then(function (res) { + ok(false, "Blob URL with non-GET request should not succeed"); + }) + .catch(function (e) { + ok( + e instanceof TypeError, + "Blob URL with non-GET request should get a TypeError" + ); + }); + }) + ).then(function () { + URL.revokeObjectURL(url); + }); +} + +function testMozErrors() { + // mozErrors shouldn't be available to content and be ignored. + return fetch("http://localhost:4/should/fail", { mozErrors: true }) + .then(res => { + ok(false, "Request should not succeed"); + }) + .catch(err => { + ok(err instanceof TypeError); + }); +} + +function testRequestMozErrors() { + // mozErrors shouldn't be available to content and be ignored. + const r = new Request("http://localhost:4/should/fail", { mozErrors: true }); + return fetch(r) + .then(res => { + ok(false, "Request should not succeed"); + }) + .catch(err => { + ok(err instanceof TypeError); + }); +} + +function testViewSourceURL() { + var p2 = fetch("view-source:/").then( + function (res) { + ok(false, "view-source: URL should fail"); + }, + function (e) { + ok(e instanceof TypeError, "view-source: URL should fail"); + } + ); +} + +function runTest() { + return Promise.resolve() + .then(testAboutURL) + .then(testDataURL) + .then(testSameOriginBlobURL) + .then(testNonGetBlobURL) + .then(testMozErrors) + .then(testRequestMozErrors) + .then(testViewSourceURL); + // Put more promise based tests here. +} diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http.html b/dom/tests/mochitest/fetch/test_fetch_basic_http.html new file mode 100644 index 0000000000..f6916501d7 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http.js b/dom/tests/mochitest/fetch/test_fetch_basic_http.js new file mode 100644 index 0000000000..e3b631610b --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.js @@ -0,0 +1,280 @@ +var path = "/tests/dom/xhr/tests/"; + +var is_http2 = location.href.includes("http2"); + +var passFiles = [ + ["file_XHR_pass1.xml", "GET", 200, "OK", "text/xml"], + ["file_XHR_pass2.txt", "GET", 200, "OK", "text/plain"], + ["file_XHR_pass3.txt", "GET", 200, "OK", "text/plain"], +]; + +function testURL() { + var promises = []; + passFiles.forEach(function (entry) { + var p = fetch(path + entry[0]).then(function (res) { + ok( + res.type !== "error", + "Response should not be an error for " + entry[0] + ); + is(res.status, entry[2], "Status should match expected for " + entry[0]); + is( + res.statusText, + is_http2 ? "" : entry[3], + "Status text should match expected for " + + entry[0] + + " " + + is_http2 + + " " + + location.href + ); + if (entry[0] != "file_XHR_pass3.txt") { + ok( + res.url.endsWith(path + entry[0]), + "Response url should match request for simple fetch for " + entry[0] + ); + } else { + ok( + res.url.endsWith(path + "file_XHR_pass2.txt"), + "Response url should match request for simple fetch for " + entry[0] + ); + } + is( + res.headers.get("content-type"), + entry[4], + "Response should have content-type for " + entry[0] + ); + }); + promises.push(p); + }); + + return Promise.all(promises); +} + +var failFiles = [["ftp://localhost" + path + "file_XHR_pass1.xml", "GET"]]; + +function testURLFail() { + var promises = []; + failFiles.forEach(function (entry) { + var p = fetch(entry[0]).then( + function (res) { + ok(false, "Response should be an error for " + entry[0]); + }, + function (e) { + ok( + e instanceof TypeError, + "Response should be an error for " + entry[0] + ); + } + ); + promises.push(p); + }); + + return Promise.all(promises); +} + +function testRequestGET() { + var promises = []; + passFiles.forEach(function (entry) { + var req = new Request(path + entry[0], { method: entry[1] }); + var p = fetch(req).then(function (res) { + ok( + res.type !== "error", + "Response should not be an error for " + entry[0] + ); + is(res.status, entry[2], "Status should match expected for " + entry[0]); + is( + res.statusText, + is_http2 ? "" : entry[3], + "Status text should match expected for " + + entry[0] + + " " + + is_http2 + + " " + + location.href + ); + if (entry[0] != "file_XHR_pass3.txt") { + ok( + res.url.endsWith(path + entry[0]), + "Response url should match request for simple fetch for " + entry[0] + ); + } else { + ok( + res.url.endsWith(path + "file_XHR_pass2.txt"), + "Response url should match request for simple fetch for " + entry[0] + ); + } + is( + res.headers.get("content-type"), + entry[4], + "Response should have content-type for " + entry[0] + ); + }); + promises.push(p); + }); + + return Promise.all(promises); +} + +function arraybuffer_equals_to(ab, s) { + is(ab.byteLength, s.length, "arraybuffer byteLength should match"); + + var u8v = new Uint8Array(ab); + is( + String.fromCharCode.apply(String, u8v), + s, + "arraybuffer bytes should match" + ); +} + +function testResponses() { + var fetches = [ + fetch(path + "file_XHR_pass2.txt").then(res => { + is(res.status, 200, "status should match"); + return res + .text() + .then(v => is(v, "hello pass\n", "response should match")); + }), + + fetch(path + "file_XHR_binary1.bin").then(res => { + is(res.status, 200, "status should match"); + return res + .arrayBuffer() + .then(v => + arraybuffer_equals_to( + v, + "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb" + ) + ); + }), + + new Promise((resolve, reject) => { + var jsonBody = JSON.stringify({ title: "aBook", author: "john" }); + var req = new Request(path + "responseIdentical.sjs", { + method: "POST", + body: jsonBody, + }); + var p = fetch(req).then(res => { + is(res.status, 200, "status should match"); + return res.json().then(v => { + is(JSON.stringify(v), jsonBody, "json response should match"); + }); + }); + resolve(p); + }), + + new Promise((resolve, reject) => { + var req = new Request(path + "responseIdentical.sjs", { + method: "POST", + body: "{", + }); + var p = fetch(req).then(res => { + is(res.status, 200, "wrong status"); + return res.json().then( + v => ok(false, "expected json parse failure"), + e => ok(true, "expected json parse failure") + ); + }); + resolve(p); + }), + ]; + + return Promise.all(fetches); +} + +function testBlob() { + return fetch(path + "/file_XHR_binary2.bin").then(r => { + is(r.status, 200, "status should match"); + return r.blob().then(b => { + is(b.size, 65536, "blob should have size 65536"); + return readAsArrayBuffer(b).then(function (ab) { + var u8 = new Uint8Array(ab); + for (var i = 0; i < 65536; i++) { + if (u8[i] !== (i & 255)) { + break; + } + } + is(i, 65536, "wrong value at offset " + i); + }); + }); + }); +} + +// This test is a copy of dom/html/test/formData_test.js testSend() modified to +// use the fetch API. Please change this if you change that. +function testFormDataSend() { + var file, + blob = new Blob(["hey"], { type: "text/plain" }); + + var fd = new FormData(); + fd.append("string", "hey"); + fd.append("empty", blob); + fd.append("explicit", blob, "explicit-file-name"); + fd.append("explicit-empty", blob, ""); + file = new File([blob], "testname", { type: "text/plain" }); + fd.append("file-name", file); + file = new File([blob], "", { type: "text/plain" }); + fd.append("empty-file-name", file); + file = new File([blob], "testname", { type: "text/plain" }); + fd.append("file-name-overwrite", file, "overwrite"); + + var req = new Request("/tests/dom/html/test/form_submit_server.sjs", { + method: "POST", + body: fd, + }); + + return fetch(req).then(r => { + is(r.status, 200, "status should match"); + return r.json().then(response => { + for (var entry of response) { + if ( + entry.headers["Content-Disposition"] != 'form-data; name="string"' + ) { + is(entry.headers["Content-Type"], "text/plain"); + } + + is(entry.body, "hey"); + } + + is( + response[1].headers["Content-Disposition"], + 'form-data; name="empty"; filename="blob"' + ); + + is( + response[2].headers["Content-Disposition"], + 'form-data; name="explicit"; filename="explicit-file-name"' + ); + + is( + response[3].headers["Content-Disposition"], + 'form-data; name="explicit-empty"; filename=""' + ); + + is( + response[4].headers["Content-Disposition"], + 'form-data; name="file-name"; filename="testname"' + ); + + is( + response[5].headers["Content-Disposition"], + 'form-data; name="empty-file-name"; filename=""' + ); + + is( + response[6].headers["Content-Disposition"], + 'form-data; name="file-name-overwrite"; filename="overwrite"' + ); + }); + }); +} + +function runTest() { + return Promise.resolve() + .then(testURL) + .then(testURLFail) + .then(testRequestGET) + .then(testResponses) + .then(testBlob) + .then(testFormDataSend); + // Put more promise based tests here. +} diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http2.html b/dom/tests/mochitest/fetch/test_fetch_basic_http2.html new file mode 100644 index 0000000000..f6916501d7 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http2.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_empty_reroute.html new file mode 100644 index 0000000000..5ea6a6227c --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_empty_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_reroute.html new file mode 100644 index 0000000000..5ea6a6227c --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http2_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html new file mode 100644 index 0000000000..5ea6a6227c --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html new file mode 100644 index 0000000000..5ea6a6227c --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() http fetching in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic_http.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html new file mode 100644 index 0000000000..c5cb02571a --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() function in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html new file mode 100644 index 0000000000..c5cb02571a --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() function in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html new file mode 100644 index 0000000000..d172957bab --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html @@ -0,0 +1,22 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1374943 - Test fetch cached redirects</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_cached_redirect.js"); +</script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js new file mode 100644 index 0000000000..48d9b2231f --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js @@ -0,0 +1,17 @@ +async function testCachedRedirectErrorMode() { + // This is a file that returns a 302 to someplace else and will be cached. + const REDIRECTING_URL = "file_fetch_cached_redirect.html"; + + let firstResponse = await fetch(REDIRECTING_URL, { redirect: "manual" }); + // okay, now it should be in the cahce. + try { + let secondResponse = await fetch(REDIRECTING_URL, { redirect: "error" }); + } catch (ex) {} + + ok(true, "didn't crash"); +} + +function runTest() { + return Promise.resolve().then(testCachedRedirectErrorMode); + // Put more promise based tests here. +} diff --git a/dom/tests/mochitest/fetch/test_fetch_cors.html b/dom/tests/mochitest/fetch/test_fetch_cors.html new file mode 100644 index 0000000000..b079df8cba --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() CORS mode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_cors.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_cors.js b/dom/tests/mochitest/fetch/test_fetch_cors.js new file mode 100644 index 0000000000..05ce221435 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors.js @@ -0,0 +1,1883 @@ +var path = "/tests/dom/base/test/"; + +function isOpaqueResponse(response) { + return ( + response.type == "opaque" && + response.status === 0 && + response.statusText === "" + ); +} + +function testModeSameOrigin() { + // Fetch spec Section 4, step 4, "request's mode is same-origin". + var req = new Request("http://example.com", { mode: "same-origin" }); + return fetch(req).then( + function (res) { + ok( + false, + "Attempting to fetch a resource from a different origin with mode same-origin should fail." + ); + }, + function (e) { + ok( + e instanceof TypeError, + "Attempting to fetch a resource from a different origin with mode same-origin should fail." + ); + } + ); +} + +function testNoCorsCtor() { + // Request constructor Step 19.1 + var simpleMethods = ["GET", "HEAD", "POST"]; + for (var i = 0; i < simpleMethods.length; ++i) { + var r = new Request("http://example.com", { + method: simpleMethods[i], + mode: "no-cors", + }); + ok( + true, + "no-cors Request with simple method " + simpleMethods[i] + " is allowed." + ); + } + + var otherMethods = ["DELETE", "OPTIONS", "PUT"]; + for (var i = 0; i < otherMethods.length; ++i) { + try { + var r = new Request("http://example.com", { + method: otherMethods[i], + mode: "no-cors", + }); + ok( + false, + "no-cors Request with non-simple method " + + otherMethods[i] + + " is not allowed." + ); + } catch (e) { + ok( + true, + "no-cors Request with non-simple method " + + otherMethods[i] + + " is not allowed." + ); + } + } + + // Request constructor Step 19.2, check guarded headers. + var r = new Request(".", { mode: "no-cors" }); + r.headers.append("Content-Type", "multipart/form-data"); + is( + r.headers.get("content-type"), + "multipart/form-data", + "Appending simple header should succeed" + ); + r.headers.append("custom", "value"); + ok(!r.headers.has("custom"), "Appending custom header should fail"); + r.headers.append("DNT", "value"); + ok(!r.headers.has("DNT"), "Appending forbidden header should fail"); +} + +var corsServerPath = + "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?"; +function testModeNoCors() { + // Fetch spec, section 4, step 4, response tainting should be set opaque, so + // that fetching leads to an opaque filtered response in step 8. + var r = new Request("http://example.com" + corsServerPath + "status=200", { + mode: "no-cors", + }); + return fetch(r).then( + function (res) { + ok( + isOpaqueResponse(res), + "no-cors Request fetch should result in opaque response" + ); + }, + function (e) { + ok(false, "no-cors Request fetch should not error"); + } + ); +} + +function testSameOriginCredentials() { + var cookieStr = "type=chocolatechip"; + var tests = [ + { + // Initialize by setting a cookie. + pass: 1, + setCookie: cookieStr, + withCred: "same-origin", + }, + { + // Default mode is "same-origin". + pass: 1, + cookie: cookieStr, + }, + { + pass: 1, + noCookie: 1, + withCred: "omit", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "same-origin", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "include", + }, + ]; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + req = { + // Add a default query param just to make formatting the actual params + // easier. + url: corsServerPath + "a=b", + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + return new Request(req.url, { + method: req.method, + headers: req.headers, + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + ok(test.pass, "Expected test to pass " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong text in test for " + JSON.stringify(test) + ); + }); + } + + function runATest(tests, i) { + var test = tests[i]; + var request = makeRequest(test); + console.log(request.url); + fetch(request).then( + function (res) { + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test to fail " + JSON.stringify(test)); + ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test)); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testModeCors() { + var tests = [ + // Plain request + { pass: 1, method: "GET", noAllowPreflight: 1 }, + + // undefined username + { pass: 1, method: "GET", noAllowPreflight: 1, username: undefined }, + + // undefined username and password + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + username: undefined, + password: undefined, + }, + + // nonempty username + { pass: 0, method: "GET", noAllowPreflight: 1, username: "user" }, + + // nonempty password + { pass: 0, method: "GET", noAllowPreflight: 1, password: "password" }, + + // Default allowed headers + { + pass: 1, + method: "GET", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { + "Content-Type": "foo/bar", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // Custom headers + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "X-My-Header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": + "secondValue", + }, + allowHeaders: + "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my%-header": "myValue" }, + allowHeaders: "x-my%-header", + }, + { pass: 0, method: "GET", headers: { "x-my-header": "myValue" } }, + { pass: 0, method: "GET", headers: { "x-my-header": "" } }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-header z", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-he(ader", + }, + { + pass: 0, + method: "GET", + headers: { myheader: "" }, + allowMethods: "myheader", + }, + { + pass: 1, + method: "GET", + headers: { "User-Agent": "myValue" }, + allowHeaders: "User-Agent", + }, + { pass: 0, method: "GET", headers: { "User-Agent": "myValue" } }, + + // Multiple custom headers + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header, second-header, third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header,second-header,third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header ,second-header ,third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header , second-header , third-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", "second-header": "secondValue" }, + allowHeaders: ", x-my-header, , ,, second-header, , ", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", "second-header": "secondValue" }, + allowHeaders: "x-my-header, second-header, unused-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue", "y-my-header": "secondValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "", "y-my-header": "" }, + allowHeaders: "x-my-header", + }, + + // HEAD requests + { pass: 1, method: "HEAD", noAllowPreflight: 1 }, + + // HEAD with safe headers + { + pass: 1, + method: "HEAD", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { + "Content-Type": "foo/bar", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // HEAD with custom headers + { + pass: 1, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 0, method: "HEAD", headers: { "x-my-header": "myValue" } }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + + // POST tests + { pass: 1, method: "POST", body: "hi there", noAllowPreflight: 1 }, + { pass: 1, method: "POST" }, + { pass: 1, method: "POST", noAllowPreflight: 1 }, + + // POST with standard headers + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "multipart/form-data" }, + noAllowPreflight: 1, + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + }, + { pass: 0, method: "POST", headers: { "Content-Type": "foo/bar" } }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // POST with custom headers + { + pass: 1, + method: "POST", + body: "hi there", + headers: { + Accept: "foo/bar", + "Accept-Language": "sv-SE", + "x-my-header": "myValue", + }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + headers: { "Content-Type": "text/plain", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", "x-my-header": "myValue" }, + allowHeaders: "x-my-header, content-type", + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, $_%", + }, + + // Other methods + { pass: 1, method: "DELETE", allowMethods: "DELETE" }, + { pass: 0, method: "DELETE", allowHeaders: "DELETE" }, + { pass: 0, method: "DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "" }, + { pass: 1, method: "DELETE", allowMethods: "POST, PUT, DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST, DELETE, PUT" }, + { pass: 1, method: "DELETE", allowMethods: "DELETE, POST, PUT" }, + { pass: 1, method: "DELETE", allowMethods: "POST ,PUT ,DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST,PUT,DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST , PUT , DELETE" }, + { + pass: 1, + method: "DELETE", + allowMethods: " ,, PUT ,, , , DELETE , ,", + }, + { pass: 0, method: "DELETE", allowMethods: "PUT" }, + { pass: 0, method: "DELETE", allowMethods: "DELETEZ" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE PUT" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE, PUT Z" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE, PU(T" }, + { pass: 0, method: "DELETE", allowMethods: "PUT DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "PUT Z, DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "PU(T, DELETE" }, + { pass: 0, method: "PUT", allowMethods: "put" }, + + // Status messages + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 404, + statusMessage: "nothin' here", + }, + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 401, + statusMessage: "no can do", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + allowHeaders: "content-type", + status: 500, + statusMessage: "server boo", + }, + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 200, + statusMessage: "Yes!!", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 400, + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 200, + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 204, + }, + + // exposed headers + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: ["x-my-header"], + }, + { + pass: 0, + method: "GET", + origin: "http://invalid", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header y", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "y x-my-header", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-header z", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-hea(er", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header", "y-my-header": "y header" }, + exposeHeaders: " , ,,y-my-header,z-my-header, ", + expectedResponseHeaders: ["y-my-header"], + }, + { + pass: 1, + method: "GET", + responseHeaders: { + "Cache-Control": "cacheControl header", + "Content-Language": "contentLanguage header", + Expires: "expires header", + "Last-Modified": "lastModified header", + Pragma: "pragma header", + Unexpected: "unexpected header", + }, + expectedResponseHeaders: [ + "Cache-Control", + "Content-Language", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma", + ], + }, + // Check that sending a body in the OPTIONS response works + { + pass: 1, + method: "DELETE", + allowMethods: "DELETE", + preflightBody: "I'm a preflight response body", + }, + ]; + + var baseURL = "http://example.org" + corsServerPath; + var origin = "http://mochi.test:8888"; + var fetches = []; + for (test of tests) { + var req = { + url: baseURL + "allowOrigin=" + escape(test.origin || origin), + method: test.method, + headers: test.headers, + uploadProgress: test.uploadProgress, + body: test.body, + responseHeaders: test.responseHeaders, + }; + + if (test.pass) { + req.url += "&origin=" + escape(origin) + "&requestMethod=" + test.method; + } + + if ("username" in test) { + var u = new URL(req.url); + u.username = test.username || ""; + req.url = u.href; + } + + if ("password" in test) { + var u = new URL(req.url); + u.password = test.password || ""; + req.url = u.href; + } + + if (test.noAllowPreflight) { + req.url += "&noAllowPreflight"; + } + + if (test.pass && "headers" in test) { + function isUnsafeHeader(name) { + lName = name.toLowerCase(); + return ( + lName != "accept" && + lName != "accept-language" && + (lName != "content-type" || + ![ + "text/plain", + "multipart/form-data", + "application/x-www-form-urlencoded", + ].includes(test.headers[name].toLowerCase())) + ); + } + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + reqHeaders = escape( + Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(s => s.toLowerCase()) + .sort() + .join(",") + ); + req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : ""; + } + if ("allowHeaders" in test) { + req.url += "&allowHeaders=" + escape(test.allowHeaders); + } + if ("allowMethods" in test) { + req.url += "&allowMethods=" + escape(test.allowMethods); + } + if (test.body) { + req.url += "&body=" + escape(test.body); + } + if (test.status) { + req.url += "&status=" + test.status; + req.url += "&statusMessage=" + escape(test.statusMessage); + } + if (test.preflightStatus) { + req.url += "&preflightStatus=" + test.preflightStatus; + } + if (test.responseHeaders) { + req.url += + "&responseHeaders=" + escape(JSON.stringify(test.responseHeaders)); + } + if (test.exposeHeaders) { + req.url += "&exposeHeaders=" + escape(test.exposeHeaders); + } + if (test.preflightBody) { + req.url += "&preflightBody=" + escape(test.preflightBody); + } + + fetches.push( + (function (test) { + return new Promise(function (resolve) { + resolve( + new Request(req.url, { + method: req.method, + mode: "cors", + headers: req.headers, + body: req.body, + }) + ); + }) + .then(function (request) { + return fetch(request); + }) + .then(function (res) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + if (test.status) { + is( + res.status, + test.status, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + test.statusMessage, + "wrong status text for " + JSON.stringify(test) + ); + } else { + is( + res.status, + 200, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "OK", + "wrong status text for " + JSON.stringify(test) + ); + } + if (test.responseHeaders) { + for (header in test.responseHeaders) { + if (!test.expectedResponseHeaders.includes(header)) { + is( + res.headers.has(header), + false, + "|Headers.has()|wrong response header (" + + header + + ") in test for " + + JSON.stringify(test) + ); + } else { + is( + res.headers.get(header), + test.responseHeaders[header], + "|Headers.get()|wrong response header (" + + header + + ") in test for " + + JSON.stringify(test) + ); + } + } + } + + return res.text(); + }) + .then(function (v) { + if (test.method !== "HEAD") { + is( + v, + "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test) + ); + } else { + is( + v, + "", + "wrong responseText in HEAD test for " + JSON.stringify(test) + ); + } + }) + .catch(function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + }); + })(test) + ); + } + + return Promise.all(fetches); +} + +function testCrossOriginCredentials() { + var origin = "http://mochi.test:8888"; + var tests = [ + { pass: 1, method: "GET", withCred: "include", allowCred: 1 }, + { pass: 0, method: "GET", withCred: "include", allowCred: 0 }, + { pass: 0, method: "GET", withCred: "include", allowCred: 1, origin: "*" }, + { pass: 1, method: "GET", withCred: "omit", allowCred: 1, origin: "*" }, + { + pass: 1, + method: "GET", + setCookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { pass: 1, method: "GET", noCookie: 1, withCred: "omit", allowCred: 1 }, + { pass: 0, method: "GET", noCookie: 1, withCred: "include", allowCred: 1 }, + { + pass: 1, + method: "GET", + setCookie: "a=2", + withCred: "omit", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + setCookie: "a=2", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=2", + withCred: "include", + allowCred: 1, + }, + { + // When credentials mode is same-origin, but mode is cors, no + // cookie should be sent cross origin. + pass: 0, + method: "GET", + cookie: "a=2", + withCred: "same-origin", + allowCred: 1, + }, + { + // When credentials mode is same-origin, but mode is cors, no + // cookie should be sent cross origin. This test checks the same + // thing as above, but uses the noCookie check on the server + // instead, and expects a valid response. + pass: 1, + method: "GET", + noCookie: 1, + withCred: "same-origin", + }, + { + // Initialize by setting a cookies for same- and cross- origins. + pass: 1, + hops: [ + { server: origin, setCookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + setCookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { server: "http://example.com", allowOrigin: origin, noCookie: 1 }, + ], + withCred: "same-origin", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { server: "http://example.com", allowOrigin: "*", noCookie: 1 }, + ], + withCred: "same-origin", + }, + { + pass: 0, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: "*", + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + // fails because allow-credentials CORS header is not set by server + { + pass: 0, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, noCookie: 1 }, + { server: origin, noCookie: 1 }, + { server: "http://example.com", allowOrigin: origin, noCookie: 1 }, + ], + withCred: "omit", + }, + ]; + + var baseURL = "http://example.org" + corsServerPath; + var origin = "http://mochi.test:8888"; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + var url; + if (test.hops) { + url = + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)); + } else { + url = baseURL + "allowOrigin=" + escape(test.origin || origin); + } + req = { + url, + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + if (test.allowCred) { + req.url += "&allowCred"; + } + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + if ("allowHeaders" in test) { + req.url += "&allowHeaders=" + escape(test.allowHeaders); + } + if ("allowMethods" in test) { + req.url += "&allowMethods=" + escape(test.allowMethods); + } + + return new Request(req.url, { + method: req.method, + headers: req.headers, + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong text in test for " + JSON.stringify(test) + ); + }); + } + + function runATest(tests, i) { + var test = tests[i]; + var request = makeRequest(test); + fetch(request).then( + function (res) { + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testModeNoCorsCredentials() { + var cookieStr = "type=chocolatechip"; + var tests = [ + { + // Initialize by setting a cookie. + pass: 1, + setCookie: cookieStr, + withCred: "include", + }, + { + pass: 1, + noCookie: 1, + withCred: "omit", + }, + { + pass: 1, + noCookie: 1, + withCred: "same-origin", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "include", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "omit", + status: 500, + }, + { + pass: 1, + cookie: cookieStr, + withCred: "same-origin", + status: 500, + }, + { + pass: 1, + noCookie: 1, + withCred: "include", + status: 500, + }, + ]; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + req = { + url: "http://example.org" + corsServerPath + "a+b", + withCred: test.withCred, + }; + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + return new Request(req.url, { + method: "GET", + mode: "no-cors", + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + is(res.type, "opaque", "wrong response type for " + JSON.stringify(test)); + + // Get unfiltered response + var chromeResponse = SpecialPowers.wrap(res); + var unfiltered = chromeResponse.cloneUnfiltered(); + + var status = test.status ? test.status : 200; + is( + unfiltered.status, + status, + "wrong status in test for " + JSON.stringify(test) + ); + + return unfiltered.text().then(function (v) { + if (test.status === 200) { + const expected = + SpecialPowers.getIntPref( + "browser.opaqueResponseBlocking.filterFetchResponse" + ) > 0 + ? "" + : "<res>hello pass</res>\n"; + is(v, expected, "wrong text in test for " + JSON.stringify(test)); + } + }); + } + + function runATest(tests, i) { + if (typeof SpecialPowers !== "object") { + finalPromiseResolve(); + return; + } + + var test = tests[i]; + var request = makeRequest(test); + fetch(request).then( + function (res) { + ok(test.pass, "Expected test to pass " + JSON.stringify(test)); + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test to fail " + JSON.stringify(test)); + ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test)); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testCORSRedirects() { + var origin = "http://mochi.test:8888"; + + var tests = [ + { + pass: 1, + method: "GET", + hops: [{ server: "http://example.com", allowOrigin: origin }], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888" }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { + server: "http://sub2.xn--lt-uia.mochi.test:8888", + allowOrigin: origin, + }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "x" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888" }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + noAllowPreflight: 1, + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://test1.example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { + server: "http://test2.example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 1, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + noAllowPreflight: 1, + }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://test1.example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { + server: "http://test2.example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { + server: "http://sub1.test1.mochi.test:8888", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://example.com" }, + { + server: "http://sub1.test1.mochi.test:8888", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { + server: "http://mochi.test:8888", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + ]; + + var fetches = []; + for (test of tests) { + req = { + url: + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + } + + if (test.pass) { + if (test.body) { + req.url += "&body=" + escape(test.body); + } + } + + var request = new Request(req.url, { + method: req.method, + headers: req.headers, + body: req.body, + }); + fetches.push( + (function (request, test) { + return fetch(request).then( + function (res) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + is( + res.status, + 200, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "OK", + "wrong status text for " + JSON.stringify(test) + ); + is( + res.type, + "cors", + "wrong response type for " + JSON.stringify(test) + ); + var reqHost = new URL(req.url).host; + // If there is a service worker present, the redirections will be + // transparent, assuming that the original request is to the current + // site and would be intercepted. + if (isSWPresent) { + if (reqHost === location.host) { + is( + new URL(res.url).host, + reqHost, + "Response URL should be original URL with a SW present" + ); + } + } else { + is( + new URL(res.url).host, + new URL(test.hops[test.hops.length - 1].server).host, + "Response URL should be redirected URL" + ); + } + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test) + ); + }); + }, + function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + } + ); + })(request, test) + ); + } + + return Promise.all(fetches); +} + +function testNoCORSRedirects() { + var origin = "http://mochi.test:8888"; + + var tests = [ + { pass: 1, method: "GET", hops: [{ server: "http://example.com" }] }, + { + pass: 1, + method: "GET", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 1, + method: "GET", + // Must use a simple header due to no-cors header restrictions. + headers: { "accept-language": "en-us" }, + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin }, + { server: "http://example.com" }, + { server: origin }, + ], + }, + { + pass: 1, + method: "POST", + body: "upload body here", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 0, + method: "DELETE", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + ]; + + var fetches = []; + for (test of tests) { + req = { + url: + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + } + + if (test.pass) { + if (test.body) { + req.url += "&body=" + escape(test.body); + } + } + + fetches.push( + (function (req, test) { + return new Promise(function (resolve, reject) { + resolve( + new Request(req.url, { + mode: "no-cors", + method: req.method, + headers: req.headers, + body: req.body, + }) + ); + }) + .then(function (request) { + return fetch(request); + }) + .then( + function (res) { + ok( + test.pass, + "Expected test to pass for " + JSON.stringify(test) + ); + // All requests are cross-origin no-cors, we should always have + // an opaque response here. All values on the opaque response + // should be hidden. + is( + res.type, + "opaque", + "wrong response type for " + JSON.stringify(test) + ); + is( + res.status, + 0, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "", + "wrong status text for " + JSON.stringify(test) + ); + is(res.url, "", "wrong response url for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "", + "wrong responseText in test for " + JSON.stringify(test) + ); + }); + }, + function (e) { + ok( + !test.pass, + "Expected test failure for " + JSON.stringify(test) + ); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + } + ); + })(req, test) + ); + } + + return Promise.all(fetches); +} + +function testReferrer() { + var referrer; + if (self && self.location) { + referrer = self.location.href; + } else { + referrer = document.documentURI; + } + + var dict = { + Referer: referrer, + }; + return fetch( + corsServerPath + "headers=" + encodeURIComponent(JSON.stringify(dict)) + ).then( + function (res) { + is(res.status, 200, "expected correct referrer header to be sent"); + dump(res.statusText); + }, + function (e) { + ok(false, "expected correct referrer header to be sent"); + } + ); +} + +function runTest() { + testNoCorsCtor(); + let promise = Promise.resolve(); + if (typeof SpecialPowers === "object") { + promise = SpecialPowers.pushPrefEnv({ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + set: [["network.cookie.sameSite.laxByDefault", false]], + }); + } + + return promise + .then(testModeSameOrigin) + .then(testModeNoCors) + .then(testModeCors) + .then(testSameOriginCredentials) + .then(testCrossOriginCredentials) + .then(testModeNoCorsCredentials) + .then(testCORSRedirects) + .then(testNoCORSRedirects) + .then(testReferrer); + // Put more promise based tests here. +} diff --git a/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html new file mode 100644 index 0000000000..ab519de573 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() CORS mode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_cors.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html new file mode 100644 index 0000000000..ab519de573 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test fetch() CORS mode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_cors.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_csp_block.html b/dom/tests/mochitest/fetch/test_fetch_csp_block.html new file mode 100644 index 0000000000..0347c7319e --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_csp_block.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test fetch() rejects when CSP blocks</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +function withFrame(url) { + return new Promise(resolve => { + let frame = document.createElement('iframe'); + frame.addEventListener('load', _ => { + resolve(frame); + }, { once: true }); + frame.src = url; + document.body.appendChild(frame); + }); +} + +function asyncTest(frame) { + return new Promise((resolve, reject) => { + addEventListener('message', evt => { + if (evt.data === 'REJECTED') { + resolve(); + } else { + reject(); + } + }, { once: true }); + frame.contentWindow.postMessage('GO', '*'); + }); +} + +withFrame('file_fetch_csp_block_frame.html').then(frame => { + asyncTest(frame).then(_ => { + ok(true, 'fetch rejected correctly'); + }).catch(e => { + ok(false, 'fetch resolved when it should have been CSP blocked'); + }).then(_ => { + frame.remove(); + SimpleTest.finish(); + }); +}); + +</script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_fetch_observer.html b/dom/tests/mochitest/fetch/test_fetch_observer.html new file mode 100644 index 0000000000..7d9c80f4c8 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_observer.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test FetchObserver</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +SpecialPowers.pushPrefEnv({"set": [["dom.fetchObserver.enabled", true ]]}, () => { + let ifr = document.createElement('iframe'); + ifr.src = "file_fetch_observer.html"; + document.body.appendChild(ifr); + + onmessage = function(e) { + if (e.data.type == "finish") { + SimpleTest.finish(); + return; + } + + if (e.data.type == "check") { + ok(e.data.status, e.data.message); + return; + } + + ok(false, "Something when wrong."); + } +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html b/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html new file mode 100644 index 0000000000..67ad489191 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test fetch user control referrer policy Bug 1304623</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const SJS = "://example.com/tests/dom/base/test/referrer_testserver.sjs?"; + const PARAMS = ["SCHEME_FROM", "SCHEME_TO", "CROSS_ORIGIN"]; + + const testCases = [ + {ACTION: ["generate-fetch-user-control-policy-test"], + PREFS: [['network.http.referer.defaultPolicy', 0]], + TESTS: [ + // 0. No referrer. + {NAME: 'default-policy-value-no-referrer-https-http', + DESC: 'default-policy-value-no-referrer-https-http', + SCHEME_FROM: 'https', + SCHEME_TO: 'http', + RESULT: 'none'}, + {NAME: 'default-policy-value-no-referrer-https-https', + DESC: 'default-policy-value-no-referrer-https-https', + SCHEME_FROM: 'https', + SCHEME_TO: 'https', + RESULT: 'none'}], + }, + {ACTION: ["generate-fetch-user-control-policy-test"], + PREFS: [['network.http.referer.defaultPolicy', 1]], + TESTS: [ + // 1. Same origin. + {NAME: 'default-policy-value-same-origin-https-http', + DESC: 'default-policy-value-same-origin-https-http', + SCHEME_FROM: 'https', + SCHEME_TO: 'http', + RESULT: 'none'}, + {NAME: 'default-policy-value-same-origin-http-https', + DESC: 'default-policy-value-same-origin-http-https', + SCHEME_FROM: 'http', + SCHEME_TO: 'https', + RESULT: 'none'}, + {NAME: 'default-policy-value-same-origin-https-https', + DESC: 'default-policy-value-same-origin-https-https', + SCHEME_FROM: 'https', + SCHEME_TO: 'https', + RESULT: 'full'}], + }, + {ACTION: ["generate-fetch-user-control-policy-test"], + PREFS: [['network.http.referer.defaultPolicy', 2]], + TESTS: [ + // 2. strict-origin-when-cross-origin. + {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-http', + DESC: 'default-policy-value-strict-origin-when-cross-origin-https-http', + SCHEME_FROM: 'https', + SCHEME_TO: 'http', + RESULT: 'none'}, + {NAME: 'default-policy-value-strict-origin-when-cross-origin-http-https', + DESC: 'default-policy-value-strict-origin-when-cross-origin-http-https', + SCHEME_FROM: 'http', + SCHEME_TO: 'https', + RESULT: 'origin'}, + {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-https-same-origin', + DESC: 'default-policy-value-strict-origin-when-cross-origin-https-https-same-origin', + SCHEME_FROM: 'https', + SCHEME_TO: 'https', + RESULT: 'full'}, + {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-https-cross-origin', + DESC: 'default-policy-value-strict-origin-when-cross-origin-https-https-cross-origin', + SCHEME_FROM: 'https', + SCHEME_TO: 'https', + CROSS_ORIGIN: 'true', + RESULT: 'origin'}], + }, + {ACTION: ["generate-fetch-user-control-policy-test"], + PREFS: [['network.http.referer.defaultPolicy', 3]], + TESTS: [ + // 3. Default no-referrer-when-downgrade. + {NAME: 'default-policy-value-no-referrer-when-downgrade-https-http', + DESC: 'default-policy-value-no-referrer-when-downgrade-https-http', + SCHEME_FROM: 'https', + SCHEME_TO: 'http', + RESULT: 'none'}, + {NAME: 'default-policy-value-no-referrer-when-downgrade-http-https', + DESC: 'default-policy-value-no-referrer-when-downgrade-http-https', + SCHEME_FROM: 'http', + SCHEME_TO: 'https', + RESULT: 'full'}, + {NAME: 'default-policy-value-no-referrer-when-downgrade-https-https', + DESC: 'default-policy-value-no-referrer-when-downgrade-https-https', + SCHEME_FROM: 'https', + SCHEME_TO: 'https', + RESULT: 'full'}], + }, + ]; + + </script> + <script type="application/javascript" src="/tests/dom/base/test/referrer_helper.js"></script> + +</head> +<body onload="tests.next();"> + <iframe id="testframe"></iframe> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_formdataparsing.html b/dom/tests/mochitest/fetch/test_formdataparsing.html new file mode 100644 index 0000000000..d82f9425ad --- /dev/null +++ b/dom/tests/mochitest/fetch/test_formdataparsing.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1109751 - Test FormData parsing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_formdataparsing.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_formdataparsing.js b/dom/tests/mochitest/fetch/test_formdataparsing.js new file mode 100644 index 0000000000..df33beb1cc --- /dev/null +++ b/dom/tests/mochitest/fetch/test_formdataparsing.js @@ -0,0 +1,369 @@ +var boundary = "1234567891011121314151617"; + +// fn(body) should create a Body subclass with content body treated as +// FormData and return it. +function testFormDataParsing(fn) { + function makeTest(shouldPass, input, testFn) { + var obj = fn(input); + return obj.formData().then( + function (fd) { + ok(shouldPass, "Expected test to be valid FormData for " + input); + if (testFn) { + return testFn(fd); + } + return undefined; + }, + function (e) { + if (shouldPass) { + ok(false, "Expected test to pass for " + input); + } else { + ok(e.name == "TypeError", "Error should be a TypeError."); + } + } + ); + } + + // [shouldPass?, input, testFn] + var tests = [ + [ + true, + + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + + function (fd) { + is(fd.get("greeting"), '"hello"'); + }, + ], + [ + false, + + // Invalid disposition. + boundary + + '\r\nContent-Disposition: form-datafoobar; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + true, + + "--" + + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + + function (fd) { + is(fd.get("greeting"), '"hello"'); + }, + ], + [false, boundary + "\r\n\r\n" + boundary + "-"], + [ + false, + // No valid ending. + boundary + "\r\n\r\n" + boundary, + ], + [ + false, + + // One '-' prefix is not allowed. 2 or none. + "-" + + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + "invalid" + + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + "suffix" + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + "suffix" + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + // Partial boundary + boundary.substr(3) + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // Missing '\n' at beginning. + '\rContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // No form-data. + '\r\nContent-Disposition: mixed; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // No headers. + '\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // No content-disposition. + '\r\nContent-Dispositypo: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // No name. + '\r\nContent-Disposition: form-data;\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // Missing empty line between headers and body. + '\r\nContent-Disposition: form-data; name="greeting"\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + // Empty entry followed by valid entry. + boundary + + "\r\n\r\n" + + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-", + ], + [ + false, + + boundary + + // Header followed by empty line, but empty body not followed by + // newline. + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' + + boundary + + "-", + ], + [ + true, + + boundary + + // Empty body followed by newline. + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n\r\n' + + boundary + + "-", + + function (fd) { + is(fd.get("greeting"), "", "Empty value is allowed."); + }, + ], + [ + false, + boundary + + // Value is boundary itself. + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' + + boundary + + "\r\n" + + boundary + + "-", + ], + [ + false, + boundary + + // Variant of above with no valid ending boundary. + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' + + boundary, + ], + [ + true, + boundary + + // Unquoted filename with empty body. + '\r\nContent-Disposition: form-data; name="file"; filename=file1.txt\r\n\r\n\r\n' + + boundary + + "-", + + function (fd) { + var f = fd.get("file"); + ok( + f instanceof File, + "Entry with filename attribute should be read as File." + ); + is(f.name, "file1.txt", "Filename should match."); + is(f.type, "text/plain", "Default content-type should be text/plain."); + return readAsText(f).then(function (text) { + is(text, "", "File should be empty."); + }); + }, + ], + [ + true, + boundary + + // Quoted filename with empty body. + '\r\nContent-Disposition: form-data; name="file"; filename="file1.txt"\r\n\r\n\r\n' + + boundary + + "-", + + function (fd) { + var f = fd.get("file"); + ok( + f instanceof File, + "Entry with filename attribute should be read as File." + ); + is(f.name, "file1.txt", "Filename should match."); + is(f.type, "text/plain", "Default content-type should be text/plain."); + return readAsText(f).then(function (text) { + is(text, "", "File should be empty."); + }); + }, + ], + [ + false, + boundary + + // Invalid filename + '\r\nContent-Disposition: form-data; name="file"; filename="[\n@;xt"\r\n\r\n\r\n' + + boundary + + "-", + ], + [ + true, + boundary + + '\r\nContent-Disposition: form-data; name="file"; filename="[@;xt"\r\n\r\n\r\n' + + boundary + + "-", + + function (fd) { + var f = fd.get("file"); + ok( + f instanceof File, + "Entry with filename attribute should be read as File." + ); + is(f.name, "[@", "Filename should match."); + }, + ], + [ + true, + boundary + + '\r\nContent-Disposition: form-data; name="file"; filename="file with spaces"\r\n\r\n\r\n' + + boundary + + "-", + + function (fd) { + var f = fd.get("file"); + ok( + f instanceof File, + "Entry with filename attribute should be read as File." + ); + is(f.name, "file with spaces", "Filename should match."); + }, + ], + [ + true, + boundary + + "\r\n" + + 'Content-Disposition: form-data; name="file"; filename="xml.txt"\r\n' + + "content-type : application/xml\r\n" + + "\r\n" + + "<body>foobar\r\n\r\n</body>\r\n" + + boundary + + "-", + + function (fd) { + var f = fd.get("file"); + ok( + f instanceof File, + "Entry with filename attribute should be read as File." + ); + is(f.name, "xml.txt", "Filename should match."); + is( + f.type, + "application/xml", + "content-type should be application/xml." + ); + return readAsText(f).then(function (text) { + is( + text, + "<body>foobar\r\n\r\n</body>", + "File should have correct text." + ); + }); + }, + ], + ]; + + var promises = []; + for (var i = 0; i < tests.length; ++i) { + var test = tests[i]; + promises.push(makeTest(test[0], test[1], test[2])); + } + + return Promise.all(promises); +} + +function makeRequest(body) { + var req = new Request("", { + method: "post", + body, + headers: { + "Content-Type": "multipart/form-data; boundary=" + boundary, + }, + }); + return req; +} + +function makeResponse(body) { + var res = new Response(body, { + headers: { + "Content-Type": "multipart/form-data; boundary=" + boundary, + }, + }); + return res; +} + +function runTest() { + return Promise.all([ + testFormDataParsing(makeRequest), + testFormDataParsing(makeResponse), + ]); +} diff --git a/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html new file mode 100644 index 0000000000..0e25c404a9 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1109751 - Test FormData parsing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_formdataparsing.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_headers.html b/dom/tests/mochitest/fetch/test_headers.html new file mode 100644 index 0000000000..b9cad02c2f --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers.html @@ -0,0 +1,17 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test Fetch Headers - Basic</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_headers_common.js"); +</script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_headers_common.js b/dom/tests/mochitest/fetch/test_headers_common.js new file mode 100644 index 0000000000..3b2604f88f --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers_common.js @@ -0,0 +1,327 @@ +// +// Utility functions +// + +function shouldThrow(func, expected, msg) { + var err; + try { + func(); + } catch (e) { + err = e; + } finally { + ok(err instanceof expected, msg); + } +} + +function recursiveArrayCompare(actual, expected) { + is( + Array.isArray(actual), + Array.isArray(expected), + "Both should either be arrays, or not" + ); + if (Array.isArray(actual) && Array.isArray(expected)) { + var diff = actual.length !== expected.length; + + for (var i = 0, n = actual.length; !diff && i < n; ++i) { + diff = recursiveArrayCompare(actual[i], expected[i]); + } + + return diff; + } else { + return actual !== expected; + } +} + +function arrayEquals(actual, expected, msg) { + if (actual === expected) { + return; + } + + var diff = recursiveArrayCompare(actual, expected); + + ok(!diff, msg); + if (diff) { + is(actual, expected, msg); + } +} + +function checkHas(headers, name, msg) { + function doCheckHas(n) { + return headers.has(n); + } + return _checkHas(doCheckHas, headers, name, msg); +} + +function checkNotHas(headers, name, msg) { + function doCheckNotHas(n) { + return !headers.has(n); + } + return _checkHas(doCheckNotHas, headers, name, msg); +} + +function _checkHas(func, headers, name, msg) { + ok(func(name), msg); + ok(func(name.toLowerCase()), msg); + ok(func(name.toUpperCase()), msg); +} + +function checkGet(headers, name, expected, msg) { + is(headers.get(name), expected, msg); + is(headers.get(name.toLowerCase()), expected, msg); + is(headers.get(name.toUpperCase()), expected, msg); +} + +// +// Test Cases +// + +function TestCoreBehavior(headers, name) { + var start = headers.get(name); + + headers.append(name, "bar"); + + checkHas(headers, name, "Has the header"); + var expected = start ? start.concat(", bar") : "bar"; + checkGet(headers, name, expected, "Retrieve all headers for name"); + + headers.append(name, "baz"); + checkHas(headers, name, "Has the header"); + expected = start ? start.concat(", bar, baz") : "bar, baz"; + checkGet(headers, name, expected, "Retrieve all headers for name"); + + headers.set(name, "snafu"); + checkHas(headers, name, "Has the header after set"); + checkGet(headers, name, "snafu", "Retrieve all headers after set"); + + const value_bam = "boom"; + let testHTTPWhitespace = ["\t", "\n", "\r", " "]; + while (testHTTPWhitespace.length) { + headers.delete(name); + + let char = testHTTPWhitespace.shift(); + headers.set(name, char); + checkGet( + headers, + name, + "", + "Header value " + + char + + " should be normalized before checking and throwing" + ); + headers.delete(name); + + let valueFront = char + value_bam; + headers.set(name, valueFront); + checkGet( + headers, + name, + value_bam, + "Header value " + + valueFront + + " should be normalized before checking and throwing" + ); + + headers.delete(name); + + let valueBack = value_bam + char; + headers.set(name, valueBack); + checkGet( + headers, + name, + value_bam, + "Header value " + + valueBack + + " should be normalized before checking and throwing" + ); + } + + headers.delete(name.toUpperCase()); + checkNotHas(headers, name, "Does not have the header after delete"); + checkGet(headers, name, null, "Retrieve all headers after delete"); + + // should be ok to delete non-existent name + headers.delete(name); + + shouldThrow( + function () { + headers.append("foo,", "bam"); + }, + TypeError, + "Append invalid header name should throw TypeError." + ); + + shouldThrow( + function () { + headers.append(name, "ba\nm"); + }, + TypeError, + "Append invalid header value should throw TypeError." + ); + + shouldThrow( + function () { + headers.append(name, "ba\rm"); + }, + TypeError, + "Append invalid header value should throw TypeError." + ); + + ok(!headers.guard, "guard should be undefined in content"); +} + +function TestEmptyHeaders() { + is(typeof Headers, "function", "Headers global constructor exists."); + var headers = new Headers(); + ok(headers, "Constructed empty Headers object"); + TestCoreBehavior(headers, "foo"); +} + +function TestFilledHeaders() { + var source = new Headers(); + var filled = new Headers(source); + ok(filled, "Fill header from empty header"); + TestCoreBehavior(filled, "foo"); + + source = new Headers(); + source.append("abc", "123"); + source.append("def", "456"); + source.append("def", "789"); + + filled = new Headers(source); + checkGet( + filled, + "abc", + source.get("abc"), + "Single value header list matches" + ); + checkGet( + filled, + "def", + source.get("def"), + "Multiple value header list matches" + ); + TestCoreBehavior(filled, "def"); + + filled = new Headers({ + zxy: "987", + xwv: "654", + uts: "321", + }); + checkGet(filled, "zxy", "987", "Has first object filled key"); + checkGet(filled, "xwv", "654", "Has second object filled key"); + checkGet(filled, "uts", "321", "Has third object filled key"); + TestCoreBehavior(filled, "xwv"); + + filled = new Headers([ + ["zxy", "987"], + ["xwv", "654"], + ["xwv", "abc"], + ["uts", "321"], + ]); + checkGet(filled, "zxy", "987", "Has first sequence filled key"); + checkGet(filled, "xwv", "654, abc", "Has second sequence filled key"); + checkGet(filled, "uts", "321", "Has third sequence filled key"); + TestCoreBehavior(filled, "xwv"); + + shouldThrow( + function () { + filled = new Headers([ + ["zxy", "987", "654"], + ["uts", "321"], + ]); + }, + TypeError, + "Fill with non-tuple sequence should throw TypeError." + ); + + shouldThrow( + function () { + filled = new Headers([["zxy"], ["uts", "321"]]); + }, + TypeError, + "Fill with non-tuple sequence should throw TypeError." + ); +} + +function iterate(iter) { + var result = []; + for (var val = iter.next(); !val.done; ) { + result.push(val.value); + val = iter.next(); + } + return result; +} + +function iterateForOf(iter) { + var result = []; + for (var value of iter) { + result.push(value); + } + return result; +} + +function byteInflate(str) { + var encoder = new TextEncoder(); + var encoded = encoder.encode(str); + var result = ""; + for (var i = 0; i < encoded.length; ++i) { + result += String.fromCharCode(encoded[i]); + } + return result; +} + +function TestHeadersIterator() { + var ehsanInflated = byteInflate("احسان"); + var headers = new Headers(); + headers.set("foo0", "bar0"); + headers.append("foo", "bar"); + headers.append("foo", ehsanInflated); + headers.append("Foo2", "bar2"); + headers.set("Foo2", "baz2"); + headers.set("foo3", "bar3"); + headers.delete("foo0"); + headers.delete("foo3"); + + var key_iter = headers.keys(); + var value_iter = headers.values(); + var entries_iter = headers.entries(); + + arrayEquals(iterate(key_iter), ["foo", "foo2"], "Correct key iterator"); + arrayEquals( + iterate(value_iter), + ["bar, " + ehsanInflated, "baz2"], + "Correct value iterator" + ); + arrayEquals( + iterate(entries_iter), + [ + ["foo", "bar, " + ehsanInflated], + ["foo2", "baz2"], + ], + "Correct entries iterator" + ); + + arrayEquals( + iterateForOf(headers), + [ + ["foo", "bar, " + ehsanInflated], + ["foo2", "baz2"], + ], + "Correct entries iterator" + ); + arrayEquals( + iterateForOf(new Headers(headers)), + [ + ["foo", "bar, " + ehsanInflated], + ["foo2", "baz2"], + ], + "Correct entries iterator" + ); +} + +function runTest() { + TestEmptyHeaders(); + TestFilledHeaders(); + TestHeadersIterator(); + return Promise.resolve(); +} diff --git a/dom/tests/mochitest/fetch/test_headers_mainthread.html b/dom/tests/mochitest/fetch/test_headers_mainthread.html new file mode 100644 index 0000000000..de78b364e3 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers_mainthread.html @@ -0,0 +1,155 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test Fetch Headers - Basic</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript" src="test_headers_common.js"> </script> +<script type="text/javascript"> +// Main thread specific tests because they need SpecialPowers. Expects +// test_headers_common.js to already be loaded. + +function TestRequestHeaders() { + is(typeof Headers, "function", "Headers global constructor exists."); + var headers = new Headers(); + ok(headers, "Constructed empty Headers object"); + SpecialPowers.wrap(headers).guard = "request"; + TestCoreBehavior(headers, "foo"); + var forbidden = [ + "Accept-Charset", + "Accept-Encoding", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Connection", + "Content-Length", + "Cookie", + "Cookie2", + "Date", + "DNT", + "Expect", + "Host", + "Keep-Alive", + "Origin", + "Referer", + "TE", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Via", + "Proxy-Authorization", + "Proxy-blarg", + "Proxy-", + "Sec-foo", + "Sec-" + ]; + + for (var i = 0, n = forbidden.length; i < n; ++i) { + var name = forbidden[i]; + headers.append(name, "hmm"); + checkNotHas(headers, name, "Should not be able to append " + name + " to request headers"); + headers.set(name, "hmm"); + checkNotHas(headers, name, "Should not be able to set " + name + " on request headers"); + } +} + +function TestRequestNoCorsHeaders() { + is(typeof Headers, "function", "Headers global constructor exists."); + var headers = new Headers(); + ok(headers, "Constructed empty Headers object"); + SpecialPowers.wrap(headers).guard = "request-no-cors"; + + headers.append("foo", "bar"); + checkNotHas(headers, "foo", "Should not be able to append arbitrary headers to request-no-cors headers."); + headers.set("foo", "bar"); + checkNotHas(headers, "foo", "Should not be able to set arbitrary headers on request-no-cors headers."); + + var simpleNames = [ + "Accept", + "Accept-Language", + "Content-Language" + ]; + + var simpleContentTypes = [ + "application/x-www-form-urlencoded", + "multipart/form-data", + "text/plain", + "application/x-www-form-urlencoded; charset=utf-8", + "multipart/form-data; charset=utf-8", + "text/plain; charset=utf-8" + ]; + + for (var i = 0, n = simpleNames.length; i < n; ++i) { + var name = simpleNames[i]; + headers.append(name, "hmm"); + checkHas(headers, name, "Should be able to append " + name + " to request-no-cors headers"); + headers.set(name, "hmm"); + checkHas(headers, name, "Should be able to set " + name + " on request-no-cors headers"); + } + + for (var i = 0, n = simpleContentTypes.length; i < n; ++i) { + var value = simpleContentTypes[i]; + headers.append("Content-Type", value); + checkHas(headers, "Content-Type", "Should be able to append " + value + " Content-Type to request-no-cors headers"); + headers.delete("Content-Type"); + headers.set("Content-Type", value); + checkHas(headers, "Content-Type", "Should be able to set " + value + " Content-Type on request-no-cors headers"); + } +} + +function TestResponseHeaders() { + is(typeof Headers, "function", "Headers global constructor exists."); + var headers = new Headers(); + ok(headers, "Constructed empty Headers object"); + SpecialPowers.wrap(headers).guard = "response"; + TestCoreBehavior(headers, "foo"); + var forbidden = [ + "Set-Cookie", + "Set-Cookie2" + ]; + + for (var i = 0, n = forbidden.length; i < n; ++i) { + var name = forbidden[i]; + headers.append(name, "hmm"); + checkNotHas(headers, name, "Should not be able to append " + name + " to response headers"); + headers.set(name, "hmm"); + checkNotHas(headers, name, "Should not be able to set " + name + " on response headers"); + } +} + +function TestImmutableHeaders() { + is(typeof Headers, "function", "Headers global constructor exists."); + var headers = new Headers(); + ok(headers, "Constructed empty Headers object"); + TestCoreBehavior(headers, "foo"); + headers.append("foo", "atleastone"); + + SpecialPowers.wrap(headers).guard = "immutable"; + + shouldThrow(function() { + headers.append("foo", "wat"); + }, TypeError, "Should not be able to append to immutable headers"); + + shouldThrow(function() { + headers.set("foo", "wat"); + }, TypeError, "Should not be able to set immutable headers"); + + shouldThrow(function() { + headers.delete("foo"); + }, TypeError, "Should not be able to delete immutable headers"); + + checkHas(headers, "foo", "Should be able to check immutable headers"); + ok(headers.get("foo"), "Should be able to get immutable headers"); +} + +TestRequestHeaders(); +TestRequestNoCorsHeaders(); +TestResponseHeaders(); +TestImmutableHeaders(); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_headers_sw_reroute.html b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html new file mode 100644 index 0000000000..eb8d1109bd --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html @@ -0,0 +1,17 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Test Fetch Headers - Basic</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_headers_common.js"); +</script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_readableStreams.html b/dom/tests/mochitest/fetch/test_readableStreams.html new file mode 100644 index 0000000000..5120963445 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_readableStreams.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for ReadableStreams and Fetch</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="common_readableStreams.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +async function tests() { + await SpecialPowers.pushPrefEnv({ + "set": [["dom.caches.testing.enabled", true], + ["dom.quotaManager.testing", true]] + }); + + await test_nativeStream(SAME_COMPARTMENT); + await test_nativeStream(IFRAME_COMPARTMENT); + await workify('test_nativeStream'); + + await test_timeout(SAME_COMPARTMENT); + await test_timeout(IFRAME_COMPARTMENT); + await workify('test_timeout'); + + await test_nonNativeStream(SAME_COMPARTMENT); + await test_nonNativeStream(IFRAME_COMPARTMENT); + await workify('test_nonNativeStream'); + + await test_pendingStream(SAME_COMPARTMENT); + await test_pendingStream(IFRAME_COMPARTMENT); + await workify('test_pendingStream'); + + await test_noUint8Array(SAME_COMPARTMENT); + await test_noUint8Array(IFRAME_COMPARTMENT); + await workify('test_noUint8Array'); + + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the cache works in xorigin + // tests. Otherwise, the iframe containing this page is isolated from + // first-party storage access, which isolates the caches object. + if (isXOrigin) { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await SpecialPowers.addPermission( + "storageAccessAPI", + true, + window.location.href + ); + await SpecialPowers.wrap(document).requestStorageAccess(); + } + + await test_nativeStream_cache(SAME_COMPARTMENT); + await test_nativeStream_cache(IFRAME_COMPARTMENT); + await workify('test_nativeStream_cache'); + + await test_nonNativeStream_cache(SAME_COMPARTMENT); + await test_nonNativeStream_cache(IFRAME_COMPARTMENT); + await workify('test_nonNativeStream_cache'); + + await test_codeExecution(SAME_COMPARTMENT); + await test_codeExecution(IFRAME_COMPARTMENT); + + await test_global(SAME_COMPARTMENT); + await test_global(IFRAME_COMPARTMENT); + await workify('test_global'); +} + +async function runTests() { + try { + await tests(); + } catch (exc) { + ok(false, exc.toString()); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +// The iframe starts the tests by calling parent.next() when it loads. + </script> + <iframe src="iframe_readableStreams.html" id="iframe"></iframe> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_request.html b/dom/tests/mochitest/fetch/test_request.html new file mode 100644 index 0000000000..3a35665eb5 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test Request object in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_request.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_request.js b/dom/tests/mochitest/fetch/test_request.js new file mode 100644 index 0000000000..7abd833869 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request.js @@ -0,0 +1,744 @@ +function testDefaultCtor() { + var req = new Request(""); + is(req.method, "GET", "Default Request method is GET"); + ok( + req.headers instanceof Headers, + "Request should have non-null Headers object" + ); + is( + req.url, + self.location.href, + "URL should be resolved with entry settings object's API base URL" + ); + is(req.destination, "", "Default destination is the empty string."); + is( + req.referrer, + "about:client", + "Default referrer is `client` which serializes to about:client." + ); + is(req.mode, "cors", "Request mode for string input is cors"); + is( + req.credentials, + "same-origin", + "Default Request credentials is same-origin" + ); + is(req.cache, "default", "Default Request cache is default"); + + var req = new Request(req); + is(req.method, "GET", "Default Request method is GET"); + ok( + req.headers instanceof Headers, + "Request should have non-null Headers object" + ); + is( + req.url, + self.location.href, + "URL should be resolved with entry settings object's API base URL" + ); + is(req.destination, "", "Default destination is the empty string."); + is( + req.referrer, + "about:client", + "Default referrer is `client` which serializes to about:client." + ); + is(req.mode, "cors", "Request mode string input is cors"); + is( + req.credentials, + "same-origin", + "Default Request credentials is same-origin" + ); + is(req.cache, "default", "Default Request cache is default"); +} + +function testClone() { + var orig = new Request("./cloned_request.txt", { + method: "POST", + headers: { "Sample-Header": "5" }, + body: "Sample body", + mode: "same-origin", + credentials: "same-origin", + cache: "no-store", + }); + var clone = orig.clone(); + ok(clone.method === "POST", "Request method is POST"); + ok( + clone.headers instanceof Headers, + "Request should have non-null Headers object" + ); + + is( + clone.headers.get("sample-header"), + "5", + "Request sample-header should be 5." + ); + orig.headers.set("sample-header", 6); + is( + clone.headers.get("sample-header"), + "5", + "Cloned Request sample-header should continue to be 5." + ); + + ok( + clone.url === new URL("./cloned_request.txt", self.location.href).href, + "URL should be resolved with entry settings object's API base URL" + ); + ok( + clone.referrer === "about:client", + "Default referrer is `client` which serializes to about:client." + ); + ok(clone.mode === "same-origin", "Request mode is same-origin"); + ok(clone.credentials === "same-origin", "Default credentials is same-origin"); + ok(clone.cache === "no-store", "Default cache is no-store"); + + ok(!orig.bodyUsed, "Original body is not consumed."); + ok(!clone.bodyUsed, "Clone body is not consumed."); + + var origBody = null; + var clone2 = null; + return orig + .text() + .then(function (body) { + origBody = body; + is(origBody, "Sample body", "Original body string matches"); + ok(orig.bodyUsed, "Original body is consumed."); + ok(!clone.bodyUsed, "Clone body is not consumed."); + + try { + orig.clone(); + ok(false, "Cannot clone Request whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + + clone2 = clone.clone(); + return clone.text(); + }) + .then(function (body) { + is(body, origBody, "Clone body matches original body."); + ok(clone.bodyUsed, "Clone body is consumed."); + + try { + clone.clone(); + ok(false, "Cannot clone Request whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + + return clone2.text(); + }) + .then(function (body) { + is(body, origBody, "Clone body matches original body."); + ok(clone2.bodyUsed, "Clone body is consumed."); + + try { + clone2.clone(); + ok(false, "Cannot clone Request whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + }); +} + +function testUsedRequest() { + // Passing a used request should fail. + var req = new Request("", { method: "post", body: "This is foo" }); + var p1 = req.text().then(function (v) { + try { + var req2 = new Request(req); + ok(false, "Used Request cannot be passed to new Request"); + } catch (e) { + ok(true, "Used Request cannot be passed to new Request"); + } + }); + + // Passing a request should set the request as used. + var reqA = new Request("", { method: "post", body: "This is foo" }); + var reqB = new Request(reqA); + is( + reqA.bodyUsed, + true, + "Passing a Request to another Request should set the former as used" + ); + return p1; +} + +function testSimpleUrlParse() { + // Just checks that the URL parser is actually being used. + var req = new Request("/file.html"); + is( + req.url, + new URL("/file.html", self.location.href).href, + "URL parser should be used to resolve Request URL" + ); +} + +// Bug 1109574 - Passing a Request with null body should keep bodyUsed unset. +function testBug1109574() { + var r1 = new Request(""); + is(r1.bodyUsed, false, "Initial value of bodyUsed should be false"); + var r2 = new Request(r1); + is(r1.bodyUsed, false, "Request with null body should not have bodyUsed set"); + // This should succeed. + var r3 = new Request(r1); +} + +// Bug 1184550 - Request constructor should always throw if used flag is set, +// even if body is null +function testBug1184550() { + var req = new Request("", { method: "post", body: "Test" }); + fetch(req); + ok(req.bodyUsed, "Request body should be used immediately after fetch()"); + return fetch(req) + .then(function (resp) { + ok(false, "Second fetch with same request should fail."); + }) + .catch(function (err) { + is(err.name, "TypeError", "Second fetch with same request should fail."); + }); +} + +function testHeaderGuard() { + var headers = { + Cookie: "Custom cookie", + "Non-Simple-Header": "value", + }; + var r1 = new Request("", { headers }); + ok( + !r1.headers.has("Cookie"), + "Default Request header should have guard request and prevent setting forbidden header." + ); + ok( + r1.headers.has("Non-Simple-Header"), + "Default Request header should have guard request and allow setting non-simple header." + ); + + var r2 = new Request("", { mode: "no-cors", headers }); + ok( + !r2.headers.has("Cookie"), + "no-cors Request header should have guard request-no-cors and prevent setting non-simple header." + ); + ok( + !r2.headers.has("Non-Simple-Header"), + "no-cors Request header should have guard request-no-cors and prevent setting non-simple header." + ); +} + +function testMode() { + try { + var req = new Request("http://example.com", { mode: "navigate" }); + ok( + false, + "Creating a Request with navigate RequestMode should throw a TypeError" + ); + } catch (e) { + is( + e.name, + "TypeError", + "Creating a Request with navigate RequestMode should throw a TypeError" + ); + } +} + +function testMethod() { + // These get normalized. + var allowed = ["delete", "get", "head", "options", "post", "put"]; + for (var i = 0; i < allowed.length; ++i) { + try { + var r = new Request("", { method: allowed[i] }); + ok(true, "Method " + allowed[i] + " should be allowed"); + is( + r.method, + allowed[i].toUpperCase(), + "Standard HTTP method " + allowed[i] + " should be normalized" + ); + } catch (e) { + ok(false, "Method " + allowed[i] + " should be allowed"); + } + } + + var allowed = ["pAtCh", "foo"]; + for (var i = 0; i < allowed.length; ++i) { + try { + var r = new Request("", { method: allowed[i] }); + ok(true, "Method " + allowed[i] + " should be allowed"); + is( + r.method, + allowed[i], + "Non-standard but valid HTTP method " + + allowed[i] + + " should not be normalized" + ); + } catch (e) { + ok(false, "Method " + allowed[i] + " should be allowed"); + } + } + + var forbidden = ["connect", "trace", "track", "<invalid token??"]; + for (var i = 0; i < forbidden.length; ++i) { + try { + var r = new Request("", { method: forbidden[i] }); + ok(false, "Method " + forbidden[i] + " should be forbidden"); + } catch (e) { + ok(true, "Method " + forbidden[i] + " should be forbidden"); + } + } + + var allowedNoCors = ["get", "head", "post"]; + for (var i = 0; i < allowedNoCors.length; ++i) { + try { + var r = new Request("", { method: allowedNoCors[i], mode: "no-cors" }); + ok( + true, + "Method " + allowedNoCors[i] + " should be allowed in no-cors mode" + ); + } catch (e) { + ok( + false, + "Method " + allowedNoCors[i] + " should be allowed in no-cors mode" + ); + } + } + + var forbiddenNoCors = ["aardvark", "delete", "options", "put"]; + for (var i = 0; i < forbiddenNoCors.length; ++i) { + try { + var r = new Request("", { method: forbiddenNoCors[i], mode: "no-cors" }); + ok( + false, + "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode" + ); + } catch (e) { + ok( + true, + "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode" + ); + } + } + + // HEAD/GET requests cannot have a body. + try { + var r = new Request("", { method: "get", body: "hello" }); + ok(false, "HEAD/GET request cannot have a body"); + } catch (e) { + is(e.name, "TypeError", "HEAD/GET request cannot have a body"); + } + + try { + var r = new Request("", { method: "head", body: "hello" }); + ok(false, "HEAD/GET request cannot have a body"); + } catch (e) { + is(e.name, "TypeError", "HEAD/GET request cannot have a body"); + } + // Non HEAD/GET should not throw. + var r = new Request("", { method: "patch", body: "hello" }); +} +function testUrlFragment() { + var req = new Request("./request#withfragment"); + is( + req.url, + new URL("./request#withfragment", self.location.href).href, + "request.url should be serialized without exclude fragment flag set" + ); +} +function testUrlMalformed() { + try { + var req = new Request("http:// example.com"); + ok( + false, + "Creating a Request with a malformed URL should throw a TypeError" + ); + } catch (e) { + is( + e.name, + "TypeError", + "Creating a Request with a malformed URL should throw a TypeError" + ); + } +} + +function testUrlCredentials() { + try { + var req = new Request("http://user@example.com"); + ok(false, "URLs with credentials should be rejected"); + } catch (e) { + is(e.name, "TypeError", "URLs with credentials should be rejected"); + } + + try { + var req = new Request("http://user:password@example.com"); + ok(false, "URLs with credentials should be rejected"); + } catch (e) { + is(e.name, "TypeError", "URLs with credentials should be rejected"); + } +} + +function testBodyUsed() { + var req = new Request("./bodyused", { method: "post", body: "Sample body" }); + is(req.bodyUsed, false, "bodyUsed is initially false."); + return req + .text() + .then(v => { + is(v, "Sample body", "Body should match"); + is(req.bodyUsed, true, "After reading body, bodyUsed should be true."); + }) + .then(v => { + return req.blob().then( + v => { + ok(false, "Attempting to read body again should fail."); + }, + e => { + ok(true, "Attempting to read body again should fail."); + } + ); + }); +} + +var text = "κόσμε"; +function testBodyCreation() { + var req1 = new Request("", { method: "post", body: text }); + var p1 = req1.text().then(function (v) { + ok(typeof v === "string", "Should resolve to string"); + is(text, v, "Extracted string should match"); + }); + + var req2 = new Request("", { + method: "post", + body: new Uint8Array([72, 101, 108, 108, 111]), + }); + var p2 = req2.text().then(function (v) { + is("Hello", v, "Extracted string should match"); + }); + + var req2b = new Request("", { + method: "post", + body: new Uint8Array([72, 101, 108, 108, 111]).buffer, + }); + var p2b = req2b.text().then(function (v) { + is("Hello", v, "Extracted string should match"); + }); + + var reqblob = new Request("", { method: "post", body: new Blob([text]) }); + var pblob = reqblob.text().then(function (v) { + is(v, text, "Extracted string should match"); + }); + + // FormData has its own function since it has blobs and files. + + var params = new URLSearchParams(); + params.append("item", "Geckos"); + params.append("feature", "stickyfeet"); + params.append("quantity", "700"); + var req3 = new Request("", { method: "post", body: params }); + var p3 = req3.text().then(function (v) { + var extracted = new URLSearchParams(v); + is(extracted.get("item"), "Geckos", "Param should match"); + is(extracted.get("feature"), "stickyfeet", "Param should match"); + is(extracted.get("quantity"), "700", "Param should match"); + }); + + return Promise.all([p1, p2, p2b, pblob, p3]); +} + +function testFormDataBodyCreation() { + var f1 = new FormData(); + f1.append("key", "value"); + f1.append("foo", "bar"); + + var r1 = new Request("", { method: "post", body: f1 }); + // Since f1 is serialized immediately, later additions should not show up. + f1.append("more", "stuff"); + var p1 = r1.formData().then(function (fd) { + ok(fd instanceof FormData, "Valid FormData extracted."); + ok(fd.has("key"), "key should exist."); + ok(fd.has("foo"), "foo should exist."); + ok(!fd.has("more"), "more should not exist."); + }); + + f1.append("blob", new Blob([text])); + var r2 = new Request("", { method: "post", body: f1 }); + f1.delete("key"); + var p2 = r2.formData().then(function (fd) { + ok(fd instanceof FormData, "Valid FormData extracted."); + ok(fd.has("more"), "more should exist."); + + var b = fd.get("blob"); + is(b.name, "blob", "blob entry should be a Blob."); + ok(b instanceof Blob, "blob entry should be a Blob."); + + return readAsText(b).then(function (output) { + is(output, text, "Blob contents should match."); + }); + }); + + return Promise.all([p1, p2]); +} + +function testBodyExtraction() { + var text = "κόσμε"; + var newReq = function () { + return new Request("", { method: "post", body: text }); + }; + return newReq() + .text() + .then(function (v) { + ok(typeof v === "string", "Should resolve to string"); + is(text, v, "Extracted string should match"); + }) + .then(function () { + return newReq() + .blob() + .then(function (v) { + ok(v instanceof Blob, "Should resolve to Blob"); + return readAsText(v).then(function (result) { + is(result, text, "Decoded Blob should match original"); + }); + }); + }) + .then(function () { + return newReq() + .json() + .then( + function (v) { + ok(false, "Invalid json should reject"); + }, + function (e) { + ok(true, "Invalid json should reject"); + } + ); + }) + .then(function () { + return newReq() + .arrayBuffer() + .then(function (v) { + ok(v instanceof ArrayBuffer, "Should resolve to ArrayBuffer"); + var dec = new TextDecoder(); + is( + dec.decode(new Uint8Array(v)), + text, + "UTF-8 decoded ArrayBuffer should match original" + ); + }); + }) + .then(function () { + return newReq() + .formData() + .then( + function (v) { + ok(false, "invalid FormData read should fail."); + }, + function (e) { + ok(e.name == "TypeError", "invalid FormData read should fail."); + } + ); + }); +} + +function testFormDataBodyExtraction() { + // URLSearchParams translates to application/x-www-form-urlencoded. + var params = new URLSearchParams(); + params.append("item", "Geckos"); + params.append("feature", "stickyfeet"); + params.append("quantity", "700"); + params.append("quantity", "800"); + + var req = new Request("", { method: "POST", body: params }); + var p1 = req.formData().then(function (fd) { + ok(fd.has("item"), "Has entry 'item'."); + ok(fd.has("feature"), "Has entry 'feature'."); + var entries = fd.getAll("quantity"); + is(entries.length, 2, "Entries with same name are correctly handled."); + is(entries[0], "700", "Entries with same name are correctly handled."); + is(entries[1], "800", "Entries with same name are correctly handled."); + }); + + var f1 = new FormData(); + f1.append("key", "value"); + f1.append("foo", "bar"); + f1.append("blob", new Blob([text])); + var r2 = new Request("", { method: "post", body: f1 }); + var p2 = r2.formData().then(function (fd) { + ok(fd.has("key"), "Has entry 'key'."); + ok(fd.has("foo"), "Has entry 'foo'."); + ok(fd.has("blob"), "Has entry 'blob'."); + var entries = fd.getAll("blob"); + is(entries.length, 1, "getAll returns all items."); + is(entries[0].name, "blob", "Filename should be blob."); + ok(entries[0] instanceof Blob, "getAll returns blobs."); + }); + + var ws = "\r\n\r\n\r\n\r\n"; + f1.set( + "key", + new File([ws], "file name has spaces.txt", { type: "new/lines" }) + ); + var r3 = new Request("", { method: "post", body: f1 }); + var p3 = r3.formData().then(function (fd) { + ok(fd.has("foo"), "Has entry 'foo'."); + ok(fd.has("blob"), "Has entry 'blob'."); + var entries = fd.getAll("blob"); + is(entries.length, 1, "getAll returns all items."); + is(entries[0].name, "blob", "Filename should be blob."); + ok(entries[0] instanceof Blob, "getAll returns blobs."); + + ok(fd.has("key"), "Has entry 'key'."); + var f = fd.get("key"); + ok(f instanceof File, "entry should be a File."); + is(f.name, "file name has spaces.txt", "File name should match."); + is(f.type, "new/lines", "File type should match."); + is(f.size, ws.length, "File size should match."); + return readAsText(f).then(function (text) { + is(text, ws, "File contents should match."); + }); + }); + + // Override header and ensure parse fails. + var boundary = "1234567891011121314151617"; + var body = + boundary + + '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' + + boundary + + "-"; + + var r4 = new Request("", { + method: "post", + body, + headers: { + "Content-Type": "multipart/form-datafoobar; boundary=" + boundary, + }, + }); + var p4 = r4.formData().then( + function () { + ok(false, "Invalid mimetype should fail."); + }, + function () { + ok(true, "Invalid mimetype should fail."); + } + ); + + var r5 = new Request("", { + method: "POST", + body: params, + headers: { + "Content-Type": "application/x-www-form-urlencodedfoobar", + }, + }); + var p5 = r5.formData().then( + function () { + ok(false, "Invalid mimetype should fail."); + }, + function () { + ok(true, "Invalid mimetype should fail."); + } + ); + return Promise.all([p1, p2, p3, p4]); +} + +// mode cannot be set to "CORS-with-forced-preflight" from javascript. +function testModeCorsPreflightEnumValue() { + try { + var r = new Request(".", { mode: "cors-with-forced-preflight" }); + ok( + false, + "Creating Request with mode cors-with-forced-preflight should fail." + ); + } catch (e) { + ok( + true, + "Creating Request with mode cors-with-forced-preflight should fail." + ); + // Also ensure that the error message matches error messages for truly + // invalid strings. + var invalidMode = "not-in-requestmode-enum"; + var invalidExc; + try { + var r = new Request(".", { mode: invalidMode }); + } catch (e) { + invalidExc = e; + } + var expectedMessage = invalidExc.message.replace( + invalidMode, + "cors-with-forced-preflight" + ); + is( + e.message, + expectedMessage, + "mode cors-with-forced-preflight should throw same error as invalid RequestMode strings." + ); + } +} + +// HEAD/GET Requests are not allowed to have a body even when copying another +// Request. +function testBug1154268() { + var r1 = new Request("/index.html", { method: "POST", body: "Hi there" }); + ["HEAD", "GET"].forEach(function (method) { + try { + var r2 = new Request(r1, { method }); + ok( + false, + method + " Request copied from POST Request with body should fail." + ); + } catch (e) { + is( + e.name, + "TypeError", + method + " Request copied from POST Request with body should fail." + ); + } + }); +} + +function testRequestConsumedByFailedConstructor() { + var r1 = new Request("http://example.com", { + method: "POST", + body: "hello world", + }); + try { + var r2 = new Request(r1, { method: "GET" }); + ok(false, "GET Request copied from POST Request with body should fail."); + } catch (e) { + ok(true, "GET Request copied from POST Request with body should fail."); + } + ok( + !r1.bodyUsed, + "Initial request should not be consumed by failed Request constructor" + ); +} + +function runTest() { + testDefaultCtor(); + testSimpleUrlParse(); + testUrlFragment(); + testUrlCredentials(); + testUrlMalformed(); + testMode(); + testMethod(); + testBug1109574(); + testBug1184550(); + testHeaderGuard(); + testModeCorsPreflightEnumValue(); + testBug1154268(); + testRequestConsumedByFailedConstructor(); + + return Promise.resolve() + .then(testBodyCreation) + .then(testBodyUsed) + .then(testBodyExtraction) + .then(testFormDataBodyCreation) + .then(testFormDataBodyExtraction) + .then(testUsedRequest) + .then(testClone()); + // Put more promise based tests here. +} diff --git a/dom/tests/mochitest/fetch/test_request_context.html b/dom/tests/mochitest/fetch/test_request_context.html new file mode 100644 index 0000000000..db8b8bdc6e --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request_context.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Make sure that Request.context is not exposed by default</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +var req = new Request(""); +ok(!("context" in req), "Request.context should not be exposed by default"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_request_sw_reroute.html b/dom/tests/mochitest/fetch/test_request_sw_reroute.html new file mode 100644 index 0000000000..0250148f1b --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test Request object in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_request.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_response.html b/dom/tests/mochitest/fetch/test_response.html new file mode 100644 index 0000000000..af75f957fc --- /dev/null +++ b/dom/tests/mochitest/fetch/test_response.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test Response object in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_response.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_response.js b/dom/tests/mochitest/fetch/test_response.js new file mode 100644 index 0000000000..40a124bfc0 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_response.js @@ -0,0 +1,346 @@ +function testDefaultCtor() { + var res = new Response(); + is(res.type, "default", "Default Response type is default"); + ok( + res.headers instanceof Headers, + "Response should have non-null Headers object" + ); + is(res.url, "", "URL should be empty string"); + is(res.status, 200, "Default status is 200"); + is(res.statusText, "", "Default statusText is an empty string"); +} + +function testClone() { + var orig = new Response("This is a body", { + status: 404, + statusText: "Not Found", + headers: { "Content-Length": 5 }, + }); + var clone = orig.clone(); + is(clone.status, 404, "Response status is 404"); + is(clone.statusText, "Not Found", "Response statusText is POST"); + ok( + clone.headers instanceof Headers, + "Response should have non-null Headers object" + ); + + is( + clone.headers.get("content-length"), + "5", + "Response content-length should be 5." + ); + orig.headers.set("content-length", 6); + is( + clone.headers.get("content-length"), + "5", + "Response content-length should be 5." + ); + + ok(!orig.bodyUsed, "Original body is not consumed."); + ok(!clone.bodyUsed, "Clone body is not consumed."); + + var origBody = null; + var clone2 = null; + return orig + .text() + .then(function (body) { + origBody = body; + is(origBody, "This is a body", "Original body string matches"); + ok(orig.bodyUsed, "Original body is consumed."); + ok(!clone.bodyUsed, "Clone body is not consumed."); + + try { + orig.clone(); + ok(false, "Cannot clone Response whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + + clone2 = clone.clone(); + return clone.text(); + }) + .then(function (body) { + is(body, origBody, "Clone body matches original body."); + ok(clone.bodyUsed, "Clone body is consumed."); + + try { + clone.clone(); + ok(false, "Cannot clone Response whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + + return clone2.text(); + }) + .then(function (body) { + is(body, origBody, "Clone body matches original body."); + ok(clone2.bodyUsed, "Clone body is consumed."); + + try { + clone2.clone(); + ok(false, "Cannot clone Response whose body is already consumed"); + } catch (e) { + is( + e.name, + "TypeError", + "clone() of consumed body should throw TypeError" + ); + } + }); +} + +function testCloneUnfiltered() { + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + return fetch(url, { mode: "no-cors" }).then(function (response) { + // By default the chrome-only function should not be available. + is(response.type, "opaque", "response should be opaque"); + is( + response.cloneUnfiltered, + undefined, + "response.cloneUnfiltered should be undefined" + ); + + // When the test is run in a worker context we can't actually try to use + // the chrome-only function. SpecialPowers is not defined. + if (typeof SpecialPowers !== "object") { + return; + } + + // With a chrome code, however, should be able to get an unfiltered response. + var chromeResponse = SpecialPowers.wrap(response); + is( + typeof chromeResponse.cloneUnfiltered, + "function", + "chromeResponse.cloneFiltered should be a function" + ); + var unfiltered = chromeResponse.cloneUnfiltered(); + is(unfiltered.type, "default", "unfiltered response should be default"); + is(unfiltered.status, 200, "unfiltered response should have 200 status"); + }); +} + +function testError() { + var res = Response.error(); + is(res.status, 0, "Error response status should be 0"); + try { + res.headers.set("someheader", "not allowed"); + ok(false, "Error response should have immutable headers"); + } catch (e) { + ok(true, "Error response should have immutable headers"); + } +} + +function testRedirect() { + var res = Response.redirect("./redirect.response"); + is(res.status, 302, "Default redirect has status code 302"); + is(res.statusText, "", "Default redirect has status text empty"); + var h = res.headers.get("location"); + ok( + h === new URL("./redirect.response", self.location.href).href, + "Location header should be correct absolute URL" + ); + try { + res.headers.set("someheader", "not allowed"); + ok(false, "Redirects should have immutable headers"); + } catch (e) { + ok(true, "Redirects should have immutable headers"); + } + + var successStatus = [301, 302, 303, 307, 308]; + for (var i = 0; i < successStatus.length; ++i) { + var res = Response.redirect("./redirect.response", successStatus[i]); + is(res.status, successStatus[i], "Status code should match"); + } + + var failStatus = [300, 0, 304, 305, 306, 309, 500]; + for (var i = 0; i < failStatus.length; ++i) { + try { + var res = Response.redirect(".", failStatus[i]); + ok(false, "Invalid status code should fail " + failStatus[i]); + } catch (e) { + is( + e.name, + "RangeError", + "Invalid status code should fail " + failStatus[i] + ); + } + } +} + +function testOk() { + var r1 = new Response("", { status: 200 }); + ok(r1.ok, "Response with status 200 should have ok true"); + + var r2 = new Response(undefined, { status: 204 }); + ok(r2.ok, "Response with status 204 should have ok true"); + + var r3 = new Response("", { status: 299 }); + ok(r3.ok, "Response with status 299 should have ok true"); + + var r4 = new Response("", { status: 302 }); + ok(!r4.ok, "Response with status 302 should have ok false"); +} + +function testBodyUsed() { + var res = new Response("Sample body"); + ok(!res.bodyUsed, "bodyUsed is initially false."); + return res + .text() + .then(v => { + is(v, "Sample body", "Body should match"); + ok(res.bodyUsed, "After reading body, bodyUsed should be true."); + }) + .then(() => { + return res.blob().then( + v => { + ok(false, "Attempting to read body again should fail."); + }, + e => { + ok(true, "Attempting to read body again should fail."); + } + ); + }); +} + +function testBodyCreation() { + var text = "κόσμε"; + var res1 = new Response(text); + var p1 = res1.text().then(function (v) { + ok(typeof v === "string", "Should resolve to string"); + is(text, v, "Extracted string should match"); + }); + + var res2 = new Response(new Uint8Array([72, 101, 108, 108, 111])); + var p2 = res2.text().then(function (v) { + is("Hello", v, "Extracted string should match"); + }); + + var res2b = new Response(new Uint8Array([72, 101, 108, 108, 111]).buffer); + var p2b = res2b.text().then(function (v) { + is("Hello", v, "Extracted string should match"); + }); + + var resblob = new Response(new Blob([text])); + var pblob = resblob.text().then(function (v) { + is(v, text, "Extracted string should match"); + }); + + var params = new URLSearchParams(); + params.append("item", "Geckos"); + params.append("feature", "stickyfeet"); + params.append("quantity", "700"); + var res3 = new Response(params); + var p3 = res3.text().then(function (v) { + var extracted = new URLSearchParams(v); + is(extracted.get("item"), "Geckos", "Param should match"); + is(extracted.get("feature"), "stickyfeet", "Param should match"); + is(extracted.get("quantity"), "700", "Param should match"); + }); + + return Promise.all([p1, p2, p2b, pblob, p3]); +} + +function testBodyExtraction() { + var text = "κόσμε"; + var newRes = function () { + return new Response(text); + }; + return newRes() + .text() + .then(function (v) { + ok(typeof v === "string", "Should resolve to string"); + is(text, v, "Extracted string should match"); + }) + .then(function () { + return newRes() + .blob() + .then(function (v) { + ok(v instanceof Blob, "Should resolve to Blob"); + return readAsText(v).then(function (result) { + is(result, text, "Decoded Blob should match original"); + }); + }); + }) + .then(function () { + return newRes() + .json() + .then( + function (v) { + ok(false, "Invalid json should reject"); + }, + function (e) { + ok(true, "Invalid json should reject"); + } + ); + }) + .then(function () { + return newRes() + .arrayBuffer() + .then(function (v) { + ok(v instanceof ArrayBuffer, "Should resolve to ArrayBuffer"); + var dec = new TextDecoder(); + is( + dec.decode(new Uint8Array(v)), + text, + "UTF-8 decoded ArrayBuffer should match original" + ); + }); + }); +} + +function testNullBodyStatus() { + [204, 205, 304].forEach(function (status) { + try { + var res = new Response(new Blob(), { status }); + ok( + false, + "Response body provided but status code does not permit a body" + ); + } catch (e) { + ok(true, "Response body provided but status code does not permit a body"); + } + }); + + [204, 205, 304].forEach(function (status) { + try { + var res = new Response(undefined, { status }); + ok(true, "Response body provided but status code does not permit a body"); + } catch (e) { + ok( + false, + "Response body provided but status code does not permit a body" + ); + } + }); +} + +function runTest() { + testDefaultCtor(); + testError(); + testRedirect(); + testOk(); + testNullBodyStatus(); + + return ( + Promise.resolve() + .then(testBodyCreation) + .then(testBodyUsed) + .then(testBodyExtraction) + .then(testClone) + .then(testCloneUnfiltered) + // Put more promise based tests here. + .catch(function (e) { + dump("### ### " + e + "\n"); + ok(false, "got unexpected error!"); + }) + ); +} diff --git a/dom/tests/mochitest/fetch/test_responseReadyForWasm.html b/dom/tests/mochitest/fetch/test_responseReadyForWasm.html new file mode 100644 index 0000000000..7f312a9fb3 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_responseReadyForWasm.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Response ready to be used by wasm</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +const isCachingEnabled = SpecialPowers.getBoolPref("javascript.options.wasm_caching"); + +async function runTests() { + let response = await fetch("/tests/dom/promise/tests/test_webassembly_compile_sample.wasm"); + ok(!!response, "Fetch a wasm module produces a Response object"); + is(response.headers.get("content-type"), "application/wasm", "Correct content-type"); + if (!isCachingEnabled) { + ok(!SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel not available"); + SimpleTest.finish(); + return; + } + + ok(SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel available"); + + let clonedResponse = response.clone(); + ok(!!clonedResponse, "Cloned response"); + is(clonedResponse.headers.get("content-type"), "application/wasm", "Correct content-type"); + ok(SpecialPowers.wrap(clonedResponse).hasCacheInfoChannel, "nsICacheInfoChannel available"); + + response = await fetch(location.href); + ok(!!response, "Fetch another resource"); + ok(response.headers.get("content-type") != "application/wasm", "Correct content-type"); + ok(!SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel available"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +runTests(); + + </script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_response_sw_reroute.html b/dom/tests/mochitest/fetch/test_response_sw_reroute.html new file mode 100644 index 0000000000..b7d52c9849 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_response_sw_reroute.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1039846 - Test Response object in worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="utils.js"> </script> +<script type="text/javascript" src="sw_reroute.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_response.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/test_temporaryFileBlob.html b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html new file mode 100644 index 0000000000..8c645e8416 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1312410</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="common_temporaryFileBlob.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +var tests = [ + // from common_temporaryFileBlob.js: + test_fetch_basic, + test_fetch_worker, + test_xhr_basic, + test_xhr_worker, + test_response_basic, + test_response_worker, + test_request_basic, + test_request_worker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SpecialPowers.pushPrefEnv({ "set" : [[ "dom.blob.memoryToTemporaryFile", 1 ]] }, + next); +SimpleTest.waitForExplicitFinish(); + + </script> +</body> +</html> diff --git a/dom/tests/mochitest/fetch/test_webassembly_streaming.html b/dom/tests/mochitest/fetch/test_webassembly_streaming.html new file mode 100644 index 0000000000..48311ca503 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_webassembly_streaming.html @@ -0,0 +1,22 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test WebAssembly.compileStreaming() and instantiateStreaming()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript" src="fetch_test_framework.js"> </script> +<script class="testbody" type="text/javascript"> +testScript("test_fetch_basic.js"); +</script> +</body> +</html> + diff --git a/dom/tests/mochitest/fetch/utils.js b/dom/tests/mochitest/fetch/utils.js new file mode 100644 index 0000000000..8b93c1e90b --- /dev/null +++ b/dom/tests/mochitest/fetch/utils.js @@ -0,0 +1,51 @@ +// Utilities +// ========= + +// Helper that uses FileReader or FileReaderSync based on context and returns +// a Promise that resolves with the text or rejects with error. +function readAsText(blob) { + if (typeof FileReader !== "undefined") { + return new Promise(function (resolve, reject) { + var fs = new FileReader(); + fs.onload = function () { + resolve(fs.result); + }; + fs.onerror = reject; + fs.readAsText(blob); + }); + } else { + var fs = new FileReaderSync(); + return Promise.resolve(fs.readAsText(blob)); + } +} + +function readAsArrayBuffer(blob) { + if (typeof FileReader !== "undefined") { + return new Promise(function (resolve, reject) { + var fs = new FileReader(); + fs.onload = function () { + resolve(fs.result); + }; + fs.onerror = reject; + fs.readAsArrayBuffer(blob); + }); + } else { + var fs = new FileReaderSync(); + return Promise.resolve(fs.readAsArrayBuffer(blob)); + } +} + +function waitForState(worker, state, context) { + return new Promise(resolve => { + if (worker.state === state) { + resolve(context); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === state) { + worker.removeEventListener("statechange", onStateChange); + resolve(context); + } + }); + }); +} diff --git a/dom/tests/mochitest/fetch/worker_readableStreams.js b/dom/tests/mochitest/fetch/worker_readableStreams.js new file mode 100644 index 0000000000..905c73b7fa --- /dev/null +++ b/dom/tests/mochitest/fetch/worker_readableStreams.js @@ -0,0 +1,26 @@ +importScripts("common_readableStreams.js"); + +function info(message) { + postMessage({ type: "info", message }); +} + +function ok(a, message) { + postMessage({ type: "test", test: !!a, message }); +} + +function is(a, b, message) { + ok(a === b, message); +} + +onmessage = function (e) { + self[e.data](SAME_COMPARTMENT).then( + ok => { + postMessage({ type: "done" }); + }, + exc => { + dump(exc); + dump(exc.stack); + postMessage({ type: "error", message: exc.toString() }); + } + ); +}; diff --git a/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js new file mode 100644 index 0000000000..e36c32a788 --- /dev/null +++ b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js @@ -0,0 +1,31 @@ +importScripts("common_temporaryFileBlob.js"); + +function info(msg) { + postMessage({ type: "info", msg }); +} + +function ok(a, msg) { + postMessage({ type: "check", what: !!a, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function next() { + postMessage({ type: "finish" }); +} + +onmessage = function (e) { + if (e.data == "xhr") { + test_xhr_basic(); + } else if (e.data == "fetch") { + test_fetch_basic(); + } else if (e.data == "response") { + test_response_basic(); + } else if (e.data == "request") { + test_request_basic(); + } else { + ok(false, "Unknown message"); + } +}; diff --git a/dom/tests/mochitest/fetch/worker_wrapper.js b/dom/tests/mochitest/fetch/worker_wrapper.js new file mode 100644 index 0000000000..72d00db0e1 --- /dev/null +++ b/dom/tests/mochitest/fetch/worker_wrapper.js @@ -0,0 +1,85 @@ +importScripts("utils.js"); + +function getScriptUrl() { + return new URL(location.href).searchParams.get("script"); +} + +importScripts(getScriptUrl()); + +var client; +var context; + +function ok(a, msg) { + client.postMessage({ + type: "status", + status: !!a, + msg: a + ": " + msg, + context, + }); +} + +function is(a, b, msg) { + client.postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + context, + }); +} + +addEventListener("message", function workerWrapperOnMessage(e) { + removeEventListener("message", workerWrapperOnMessage); + var data = e.data; + + function runTestAndReportToClient(event) { + var done = function (res) { + client.postMessage({ type: "finish", context }); + return res; + }; + + try { + // runTest() is provided by the test. + var result = runTest().then(done, done); + if ("waitUntil" in event) { + event.waitUntil(result); + } + } catch (e) { + client.postMessage({ + type: "status", + status: false, + msg: "worker failed to run " + data.script + "; error: " + e.message, + context, + }); + done(); + } + } + + if ("ServiceWorker" in self) { + // Fetch requests from a service worker are not intercepted. + self.isSWPresent = false; + + e.waitUntil( + self.clients + .matchAll({ includeUncontrolled: true }) + .then(function (clients) { + for (var i = 0; i < clients.length; ++i) { + if (clients[i].url.indexOf("message_receiver.html") > -1) { + client = clients[i]; + break; + } + } + if (!client) { + dump( + "We couldn't find the message_receiver window, the test will fail\n" + ); + } + context = "ServiceWorker"; + runTestAndReportToClient(e); + }) + ); + } else { + client = self; + context = "Worker"; + runTestAndReportToClient(e); + } +}); |