diff options
Diffstat (limited to 'dom/tests/mochitest/fetch')
41 files changed, 4438 insertions, 0 deletions
diff --git a/dom/tests/mochitest/fetch/common_temporaryFileBlob.js b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js new file mode 100644 index 000000000..47f5695b1 --- /dev/null +++ b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js @@ -0,0 +1,39 @@ +var data = new Array(256).join("1234567890ABCDEF"); + +function test_basic() { + info("Simple 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"); + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function() { + is(fr.result, data, "Data content matches"); + next(); + } + }); +} + +function test_worker() { + info("XHR in workers"); + 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(42); +} diff --git a/dom/tests/mochitest/fetch/empty.js b/dom/tests/mochitest/fetch/empty.js new file mode 100644 index 000000000..e69de29bb --- /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 000000000..d0b9633bb --- /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 000000000..8ded0911f --- /dev/null +++ b/dom/tests/mochitest/fetch/fetch_test_framework.js @@ -0,0 +1,165 @@ +function testScript(script) { + + // 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(2); + + // 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.requestcontext.enabled", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true]] + }, resolve); + }); + } + + function workerTest() { + return new Promise(function(resolve, reject) { + var worker = new Worker("worker_wrapper.js"); + 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": script }); + }); + } + + function nestedWorkerTest() { + return new Promise(function(resolve, reject) { + var worker = new Worker("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": 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.waiting || + registration.active; + + window.addEventListener("message",function onMessage(event) { + if (event.data.context != "ServiceWorker") { + return; + } + if (event.data.type == 'finish') { + window.removeEventListener("message", onMessage); + registration.unregister() + .then(resolve) + .catch(reject); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }, false); + + worker.onerror = reject; + + var iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ script: script }); + }; + document.body.appendChild(iframe); + } + + navigator.serviceWorker.register("worker_wrapper.js", {scope: "."}) + .then(function(registration) { + if (registration.installing) { + var done = false; + registration.installing.onstatechange = function() { + if (!done) { + done = true; + setupSW(registration); + } + }; + } else { + setupSW(registration); + } + }); + }); + } + + 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() { + // XXX Bug 1281212 - This makes other, unrelated test suites fail, primarily on WinXP. + let isWin = navigator.platform.indexOf("Win") == 0; + return isWin ? undefined : 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() { + if (parent && parent.finishTest) { + parent.finishTest(); + } else { + SimpleTest.finish(); + } + }); +} + diff --git a/dom/tests/mochitest/fetch/message_receiver.html b/dom/tests/mochitest/fetch/message_receiver.html new file mode 100644 index 000000000..82cb587c7 --- /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.ini b/dom/tests/mochitest/fetch/mochitest.ini new file mode 100644 index 000000000..cf4477463 --- /dev/null +++ b/dom/tests/mochitest/fetch/mochitest.ini @@ -0,0 +1,57 @@ +[DEFAULT] +support-files = + fetch_test_framework.js + test_fetch_basic.js + test_fetch_basic_http.js + test_fetch_cors.js + 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^ + sw_reroute.js + empty.js + empty.js^headers^ + worker_temporaryFileBlob.js + common_temporaryFileBlob.js + !/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 + +[test_headers.html] +[test_headers_sw_reroute.html] +[test_headers_mainthread.html] +[test_fetch_basic.html] +[test_fetch_basic_sw_reroute.html] +[test_fetch_basic_sw_empty_reroute.html] +[test_fetch_basic_http.html] +[test_fetch_basic_http_sw_reroute.html] +[test_fetch_basic_http_sw_empty_reroute.html] +[test_fetch_cors.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_fetch_cors_sw_reroute.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_fetch_cors_sw_empty_reroute.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_formdataparsing.html] +[test_formdataparsing_sw_reroute.html] +[test_request.html] +[test_request_context.html] +[test_request_sw_reroute.html] +[test_response.html] +[test_response_sw_reroute.html] +[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 000000000..7e463e7df --- /dev/null +++ b/dom/tests/mochitest/fetch/nested_worker_wrapper.js @@ -0,0 +1,28 @@ +// Hold the nested worker alive until this parent worker closes. +var worker; + +addEventListener('message', function nestedWorkerWrapperOnMessage(evt) { + removeEventListener('message', nestedWorkerWrapperOnMessage); + + worker = new Worker('worker_wrapper.js'); + + 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 000000000..bb12212ea --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.html @@ -0,0 +1,18 @@ +<!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; +} +testScript(location.search.substring(1) + ".js"); +</script> diff --git a/dom/tests/mochitest/fetch/reroute.js b/dom/tests/mochitest/fetch/reroute.js new file mode 100644 index 000000000..2c9bcd35f --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js @@ -0,0 +1,24 @@ +onfetch = function(e) { + if (e.request.url.indexOf("Referer") >= 0) { + // 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=' + ({ 'Referer': self.location.href }).toSource(); + + 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: body, + mode: 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 000000000..d0b9633bb --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/sw_reroute.js b/dom/tests/mochitest/fetch/sw_reroute.js new file mode 100644 index 000000000..11ad7b76e --- /dev/null +++ b/dom/tests/mochitest/fetch/sw_reroute.js @@ -0,0 +1,31 @@ +var gRegistration; + +function testScript(script) { + function setupSW(registration) { + gRegistration = registration; + + var iframe = document.createElement("iframe"); + iframe.src = "reroute.html?" + script.replace(".js", ""); + document.body.appendChild(iframe); + } + + SpecialPowers.pushPrefEnv({ + "set": [["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true]] + }, function() { + navigator.serviceWorker.ready.then(setupSW); + var scriptURL = location.href.includes("sw_empty_reroute.html") + ? "empty.js" : "reroute.js"; + navigator.serviceWorker.register(scriptURL, {scope: "/"}); + }); +} + +function finishTest() { + 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 000000000..ce7b63aba --- /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 type="text/javascript" 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 000000000..16ce9d8a3 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic.js @@ -0,0 +1,104 @@ +function testAboutURL() { + var p1 = fetch('about:blank').then(function(res) { + is(res.status, 200, "about:blank should load a valid Response"); + is(res.headers.get('content-type'), 'text/html;charset=utf-8', + "about:blank content-type should be text/html;charset=utf-8"); + is(res.headers.has('content-length'), false, + "about:blank should not have a content-length header"); + return res.text().then(function(v) { + is(v, "", "about:blank body should be empty"); + }); + }); + + 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: 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 runTest() { + return Promise.resolve() + .then(testAboutURL) + .then(testDataURL) + .then(testSameOriginBlobURL) + .then(testNonGetBlobURL) + // 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 000000000..24175bb19 --- /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 type="text/javascript" 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 000000000..4cd326ea8 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.js @@ -0,0 +1,201 @@ +var path = "/tests/dom/xhr/tests/"; + +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, entry[3], "Status text should match expected for " + entry[0]); + // This file redirects to pass2, but that is invisible if a SW is present. + if (entry[0] != "file_XHR_pass3.txt" || isSWPresent) + 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, entry[3], "Status text should match expected for " + entry[0]); + // This file redirects to pass2, but that is invisible if a SW is present. + if (entry[0] != "file_XHR_pass3.txt" || isSWPresent) + 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) => { + ok(r.status, 200, "status should match"); + return r.blob().then((b) => { + ok(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) => { + ok(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_http_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html new file mode 100644 index 000000000..0f5052eda --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.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 1039846 - Test fetch() http fetching in worker</title> + <script type="text/javascript" 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="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 000000000..0f5052eda --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.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 1039846 - Test fetch() http fetching in worker</title> + <script type="text/javascript" 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="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 000000000..bcf959add --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.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 1039846 - Test fetch() function in worker</title> + <script type="text/javascript" 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="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 000000000..bcf959add --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.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 1039846 - Test fetch() function in worker</title> + <script type="text/javascript" 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="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_cors.html b/dom/tests/mochitest/fetch/test_fetch_cors.html new file mode 100644 index 000000000..b9a6992a5 --- /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 type="text/javascript" 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 000000000..ac83d050d --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors.js @@ -0,0 +1,1748 @@ +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 "omit". + pass: 1, + noCookie: 1, + }, + { + 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 " + test.toSource()); + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + return res.text().then(function(v) { + is(v, "<res>hello pass</res>\n", + "wrong text in test for " + test.toSource()); + }); + } + + 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 " + test.toSource()); + ok(e instanceof TypeError, "Test should fail " + test.toSource()); + 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 + // XXXbz this passes for now, because we ignore passwords + // without usernames in most cases. + { pass: 1, + 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"] + .indexOf(test.headers[name].toLowerCase()) == -1); + } + req.url += "&headers=" + escape(test.headers.toSource()); + reqHeaders = + escape(Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(String.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(test.responseHeaders.toSource()); + 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 " + test.toSource()); + if (test.status) { + is(res.status, test.status, "wrong status in test for " + test.toSource()); + is(res.statusText, test.statusMessage, "wrong status text for " + test.toSource()); + } + else { + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + } + if (test.responseHeaders) { + for (header in test.responseHeaders) { + if (test.expectedResponseHeaders.indexOf(header) == -1) { + is(res.headers.has(header), false, + "|Headers.has()|wrong response header (" + header + ") in test for " + + test.toSource()); + } + else { + is(res.headers.get(header), test.responseHeaders[header], + "|Headers.get()|wrong response header (" + header + ") in test for " + + test.toSource()); + } + } + } + + return res.text(); + }).then(function(v) { + if (test.method !== "HEAD") { + is(v, "<res>hello pass</res>\n", + "wrong responseText in test for " + test.toSource()); + } + else { + is(v, "", + "wrong responseText in HEAD test for " + test.toSource()); + } + }).catch(function(e) { + ok(!test.pass, "Expected test failure for " + test.toSource()); + ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource()); + }); + })(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(test.hops.toSource()); + } else { + url = baseURL + "allowOrigin=" + escape(test.origin || origin); + } + req = { + url: 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 " + test.toSource()); + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + return res.text().then(function(v) { + is(v, "<res>hello pass</res>\n", + "wrong text in test for " + test.toSource()); + }); + } + + 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 " + test.toSource()); + ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource()); + 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 ' + test.toSource()); + + // 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 " + test.toSource()); + return unfiltered.text().then(function(v) { + if (status === 200) { + is(v, "<res>hello pass</res>\n", + "wrong text in test for " + test.toSource()); + } + }); + } + + function runATest(tests, i) { + if (typeof SpecialPowers !== 'object') { + finalPromiseResolve(); + return; + } + + 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 to fail " + test.toSource()); + ok(e instanceof TypeError, "Test should fail " + test.toSource()); + 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(test.hops.toSource()), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(test.headers.toSource()); + } + + 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 " + test.toSource()); + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + is(res.type, 'cors', 'wrong response type for ' + test.toSource()); + 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 " + test.toSource()); + }); + }, function(e) { + ok(!test.pass, "Expected test failure for " + test.toSource()); + ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource()); + }); + })(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(test.hops.toSource()), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(test.headers.toSource()); + } + + 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 " + test.toSource()); + // 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 ' + test.toSource()); + is(res.status, 0, "wrong status in test for " + test.toSource()); + is(res.statusText, "", "wrong status text for " + test.toSource()); + is(res.url, '', 'wrong response url for ' + test.toSource()); + return res.text().then(function(v) { + is(v, "", "wrong responseText in test for " + test.toSource()); + }); + }, function(e) { + ok(!test.pass, "Expected test failure for " + test.toSource()); + ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource()); + }); + })(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=" + dict.toSource()).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(); + + return Promise.resolve() + .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 000000000..7ad368cfd --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.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 1039846 - Test fetch() CORS mode</title> + <script type="text/javascript" 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="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 000000000..7ad368cfd --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.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 1039846 - Test fetch() CORS mode</title> + <script type="text/javascript" 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="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_formdataparsing.html b/dom/tests/mochitest/fetch/test_formdataparsing.html new file mode 100644 index 000000000..ca81311fd --- /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 type="text/javascript" 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 000000000..bc7eb21a6 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_formdataparsing.js @@ -0,0 +1,283 @@ +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); + } + }, 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: 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 000000000..b3fe0db44 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.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 1109751 - Test FormData parsing</title> + <script type="text/javascript" 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="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 000000000..f13f53425 --- /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 type="text/javascript" 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 000000000..fe792b25b --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers_common.js @@ -0,0 +1,229 @@ +// +// 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"); + + 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, "bam\n"); + }, TypeError, "Append invalid header value should throw TypeError."); + + shouldThrow(function() { + headers.append(name, "bam\n\r"); + }, 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("utf-8"); + 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", "foo", "foo2"], "Correct key iterator"); + arrayEquals(iterate(value_iter), ["bar", ehsanInflated, "baz2"], "Correct value iterator"); + arrayEquals(iterate(entries_iter), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); + + arrayEquals(iterateForOf(headers), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator"); + arrayEquals(iterateForOf(new Headers(headers)), [["foo", "bar"], ["foo", 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 000000000..9bdc89b71 --- /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 type="text/javascript" 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 000000000..dd6ec5fb3 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html @@ -0,0 +1,16 @@ +<!-- 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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<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_request.html b/dom/tests/mochitest/fetch/test_request.html new file mode 100644 index 000000000..83869fd11 --- /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 type="text/javascript" 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 000000000..badad61b9 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request.js @@ -0,0 +1,542 @@ +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.context, "fetch", "Default context is fetch."); + 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, "omit", "Default Request credentials is omit"); + 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.context, "fetch", "Default context is fetch."); + 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, "omit", "Default Request credentials is omit"); + 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: 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: 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"); + ok(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: 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: 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 000000000..07c99e2f0 --- /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 type="text/javascript" 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 000000000..9fbfe767c --- /dev/null +++ b/dom/tests/mochitest/fetch/test_request_sw_reroute.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 Request object in worker</title> + <script type="text/javascript" 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="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 000000000..648851e8b --- /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 type="text/javascript" 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 000000000..e396184ee --- /dev/null +++ b/dom/tests/mochitest/fetch/test_response.js @@ -0,0 +1,267 @@ +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, "OK", "Default statusText is OK"); +} + +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"); + 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": 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": 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_response_sw_reroute.html b/dom/tests/mochitest/fetch/test_response_sw_reroute.html new file mode 100644 index 000000000..9f9751b8a --- /dev/null +++ b/dom/tests/mochitest/fetch/test_response_sw_reroute.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 1039846 - Test Response object in worker</title> + <script type="text/javascript" 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="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 000000000..f7390f9d2 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1312410</title> + <script type="application/javascript" 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_basic, + test_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/utils.js b/dom/tests/mochitest/fetch/utils.js new file mode 100644 index 000000000..5d294041e --- /dev/null +++ b/dom/tests/mochitest/fetch/utils.js @@ -0,0 +1,37 @@ +// 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)); + } +} + diff --git a/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js new file mode 100644 index 000000000..182dc7c18 --- /dev/null +++ b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js @@ -0,0 +1,21 @@ +importScripts('common_temporaryFileBlob.js'); + +function info(msg) { + postMessage({type: 'info', msg: msg}); +} + +function ok(a, msg) { + postMessage({type: 'check', what: !!a, msg: msg}); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function next() { + postMessage({type: 'finish'}); +} + +onmessage = function(e) { + test_basic(); +} diff --git a/dom/tests/mochitest/fetch/worker_wrapper.js b/dom/tests/mochitest/fetch/worker_wrapper.js new file mode 100644 index 000000000..c2f0ec00e --- /dev/null +++ b/dom/tests/mochitest/fetch/worker_wrapper.js @@ -0,0 +1,58 @@ +importScripts("utils.js"); +var client; +var context; + +function ok(a, msg) { + client.postMessage({type: 'status', status: !!a, + msg: a + ": " + msg, context: context}); +} + +function is(a, b, msg) { + client.postMessage({type: 'status', status: a === b, + msg: a + " === " + b + ": " + msg, context: context}); +} + +addEventListener('message', function workerWrapperOnMessage(e) { + removeEventListener('message', workerWrapperOnMessage); + var data = e.data; + + function loadTest() { + var done = function() { + client.postMessage({ type: 'finish', context: context }); + } + + try { + importScripts(data.script); + // runTest() is provided by the test. + runTest().then(done, done); + } catch(e) { + client.postMessage({ + type: 'status', + status: false, + msg: 'worker failed to import ' + data.script + "; error: " + e.message, + context: context + }); + done(); + } + } + + if ("ServiceWorker" in self) { + self.clients.matchAll().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"; + loadTest(); + }); + } else { + client = self; + context = "Worker"; + loadTest(); + } +}); |