summaryrefslogtreecommitdiffstats
path: root/dom/tests/mochitest/fetch
diff options
context:
space:
mode:
Diffstat (limited to 'dom/tests/mochitest/fetch')
-rw-r--r--dom/tests/mochitest/fetch/common_temporaryFileBlob.js39
-rw-r--r--dom/tests/mochitest/fetch/empty.js0
-rw-r--r--dom/tests/mochitest/fetch/empty.js^headers^1
-rw-r--r--dom/tests/mochitest/fetch/fetch_test_framework.js165
-rw-r--r--dom/tests/mochitest/fetch/message_receiver.html6
-rw-r--r--dom/tests/mochitest/fetch/mochitest.ini57
-rw-r--r--dom/tests/mochitest/fetch/nested_worker_wrapper.js28
-rw-r--r--dom/tests/mochitest/fetch/reroute.html18
-rw-r--r--dom/tests/mochitest/fetch/reroute.js24
-rw-r--r--dom/tests/mochitest/fetch/reroute.js^headers^1
-rw-r--r--dom/tests/mochitest/fetch/sw_reroute.js31
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic.js104
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http.js201
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors.js1748
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing.html23
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing.js283
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_headers.html17
-rw-r--r--dom/tests/mochitest/fetch/test_headers_common.js229
-rw-r--r--dom/tests/mochitest/fetch/test_headers_mainthread.html155
-rw-r--r--dom/tests/mochitest/fetch/test_headers_sw_reroute.html16
-rw-r--r--dom/tests/mochitest/fetch/test_request.html23
-rw-r--r--dom/tests/mochitest/fetch/test_request.js542
-rw-r--r--dom/tests/mochitest/fetch/test_request_context.html19
-rw-r--r--dom/tests/mochitest/fetch/test_request_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_response.html23
-rw-r--r--dom/tests/mochitest/fetch/test_response.js267
-rw-r--r--dom/tests/mochitest/fetch/test_response_sw_reroute.html22
-rw-r--r--dom/tests/mochitest/fetch/test_temporaryFileBlob.html35
-rw-r--r--dom/tests/mochitest/fetch/utils.js37
-rw-r--r--dom/tests/mochitest/fetch/worker_temporaryFileBlob.js21
-rw-r--r--dom/tests/mochitest/fetch/worker_wrapper.js58
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();
+ }
+});