summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/cookies/resources
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/cookies/resources
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/cookies/resources')
-rw-r--r--testing/web-platform/tests/cookies/resources/__init__.py0
-rw-r--r--testing/web-platform/tests/cookies/resources/cookie-helper.sub.js284
-rw-r--r--testing/web-platform/tests/cookies/resources/cookie-test.js186
-rw-r--r--testing/web-platform/tests/cookies/resources/cookie.py42
-rw-r--r--testing/web-platform/tests/cookies/resources/drop.py14
-rw-r--r--testing/web-platform/tests/cookies/resources/dropSameSite.py13
-rw-r--r--testing/web-platform/tests/cookies/resources/dropSameSiteMultiAttribute.py17
-rw-r--r--testing/web-platform/tests/cookies/resources/dropSameSiteNone.py11
-rw-r--r--testing/web-platform/tests/cookies/resources/dropSecure.py11
-rw-r--r--testing/web-platform/tests/cookies/resources/echo-cookie.html31
-rw-r--r--testing/web-platform/tests/cookies/resources/echo-json.py15
-rw-r--r--testing/web-platform/tests/cookies/resources/helpers.py59
-rw-r--r--testing/web-platform/tests/cookies/resources/imgIfMatch.py16
-rw-r--r--testing/web-platform/tests/cookies/resources/list-cookies-for-script.py12
-rw-r--r--testing/web-platform/tests/cookies/resources/list.py10
-rw-r--r--testing/web-platform/tests/cookies/resources/navigate.html8
-rw-r--r--testing/web-platform/tests/cookies/resources/postToParent.py39
-rw-r--r--testing/web-platform/tests/cookies/resources/redirectWithCORSHeaders.py22
-rw-r--r--testing/web-platform/tests/cookies/resources/set-cookie.py45
-rw-r--r--testing/web-platform/tests/cookies/resources/set.py15
-rw-r--r--testing/web-platform/tests/cookies/resources/setSameSite.py32
-rw-r--r--testing/web-platform/tests/cookies/resources/setSameSiteDomain.py36
-rw-r--r--testing/web-platform/tests/cookies/resources/setSameSiteMultiAttribute.py60
-rw-r--r--testing/web-platform/tests/cookies/resources/setSameSiteNone.py16
-rw-r--r--testing/web-platform/tests/cookies/resources/setSecure.py14
-rw-r--r--testing/web-platform/tests/cookies/resources/testharness-helpers.js49
26 files changed, 1057 insertions, 0 deletions
diff --git a/testing/web-platform/tests/cookies/resources/__init__.py b/testing/web-platform/tests/cookies/resources/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/__init__.py
diff --git a/testing/web-platform/tests/cookies/resources/cookie-helper.sub.js b/testing/web-platform/tests/cookies/resources/cookie-helper.sub.js
new file mode 100644
index 0000000000..3338cf0e80
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/cookie-helper.sub.js
@@ -0,0 +1,284 @@
+// Set up exciting global variables for cookie tests.
+(_ => {
+ var HOST = "{{host}}";
+ var INSECURE_PORT = ":{{ports[http][0]}}";
+ var SECURE_PORT = ":{{ports[https][0]}}";
+ var CROSS_ORIGIN_HOST = "{{hosts[alt][]}}";
+
+ window.INSECURE_ORIGIN = "http://" + HOST + INSECURE_PORT;
+
+ //For secure cookie verification
+ window.SECURE_ORIGIN = "https://" + HOST + SECURE_PORT;
+
+ //standard references
+ window.SECURE_SUBDOMAIN_ORIGIN = "https://{{domains[www1]}}" + SECURE_PORT;
+ window.SECURE_CROSS_SITE_ORIGIN = "https://" + CROSS_ORIGIN_HOST + SECURE_PORT;
+ window.CROSS_SITE_HOST = CROSS_ORIGIN_HOST;
+
+ // Set the global cookie name.
+ window.HTTP_COOKIE = "cookie_via_http";
+})();
+
+// A tiny helper which returns the result of fetching |url| with credentials.
+function credFetch(url) {
+ return fetch(url, {"credentials": "include"})
+ .then(response => {
+ if (response.status !== 200) {
+ throw new Error(response.statusText);
+ }
+ return response;
+ });
+}
+
+// Returns a URL on |origin| which redirects to a given absolute URL.
+function redirectTo(origin, url) {
+ return origin + "/cookies/resources/redirectWithCORSHeaders.py?status=307&location=" + encodeURIComponent(url);
+}
+
+// Returns a URL on |origin| which navigates the window to the given URL (by
+// setting window.location).
+function navigateTo(origin, url) {
+ return origin + "/cookies/resources/navigate.html?location=" + encodeURIComponent(url);
+}
+
+// Returns whether a cookie with name `name` with value `value` is in the cookie
+// string (presumably obtained via document.cookie).
+function cookieStringHasCookie(name, value, cookieString) {
+ return new RegExp(`(?:^|; )${name}=${value}(?:$|;)`).test(cookieString);
+}
+
+// Asserts that `document.cookie` contains or does not contain (according to
+// the value of |present|) a cookie named |name| with a value of |value|.
+function assert_dom_cookie(name, value, present) {
+ assert_equals(cookieStringHasCookie(name, value, document.cookie), present, "`" + name + "=" + value + "` in `document.cookie`");
+}
+
+function assert_cookie(origin, obj, name, value, present) {
+ assert_equals(obj[name], present ? value : undefined, "`" + name + "=" + value + "` in request to `" + origin + "`.");
+}
+
+// Remove the cookie named |name| from |origin|, then set it on |origin| anew.
+// If |origin| matches `self.origin`, also assert (via `document.cookie`) that
+// the cookie was correctly removed and reset.
+async function create_cookie(origin, name, value, extras) {
+ alert("Create_cookie: " + origin + "/cookies/resources/drop.py?name=" + name);
+ await credFetch(origin + "/cookies/resources/drop.py?name=" + name);
+ if (origin == self.origin)
+ assert_dom_cookie(name, value, false);
+ await credFetch(origin + "/cookies/resources/set.py?" + name + "=" + value + ";path=/;" + extras);
+ if (origin == self.origin)
+ assert_dom_cookie(name, value, true);
+}
+
+//
+// Prefix-specific test helpers
+//
+function set_prefixed_cookie_via_dom_test(options) {
+ promise_test(t => {
+ var name = options.prefix + "prefixtestcookie";
+ erase_cookie_from_js(name, options.params);
+ t.add_cleanup(() => erase_cookie_from_js(name, options.params));
+ var value = "" + Math.random();
+ document.cookie = name + "=" + value + ";" + options.params;
+
+ assert_dom_cookie(name, value, options.shouldExistInDOM);
+
+ return credFetch("/cookies/resources/list.py")
+ .then(r => r.json())
+ .then(cookies => assert_equals(cookies[name], options.shouldExistViaHTTP ? value : undefined));
+ }, options.title);
+}
+
+function set_prefixed_cookie_via_http_test(options) {
+ promise_test(t => {
+ var name = options.prefix + "prefixtestcookie";
+ var value = "" + Math.random();
+
+ t.add_cleanup(() => {
+ var cookie = name + "=0;expires=" + new Date(0).toUTCString() + ";" +
+ options.params;
+
+ return credFetch(options.origin + "/cookies/resources/set.py?" + cookie);
+ });
+
+ return credFetch(options.origin + "/cookies/resources/set.py?" + name + "=" + value + ";" + options.params)
+ .then(_ => credFetch(options.origin + "/cookies/resources/list.py"))
+ .then(r => r.json())
+ .then(cookies => assert_equals(cookies[name], options.shouldExistViaHTTP ? value : undefined));
+ }, options.title);
+}
+
+//
+// SameSite-specific test helpers:
+//
+
+// status for "network" cookies.
+window.SameSiteStatus = {
+ CROSS_SITE: "cross-site",
+ LAX: "lax",
+ STRICT: "strict"
+};
+// status for "document.cookie".
+window.DomSameSiteStatus = {
+ CROSS_SITE: "cross-site",
+ SAME_SITE: "same-site",
+};
+
+const wait_for_message = (type, origin) => {
+ return new Promise((resolve, reject) => {
+ window.addEventListener('message', e => {
+ if (origin && e.origin != origin) {
+ reject("Message from unexpected origin in wait_for_message:" + e.origin);
+ return;
+ }
+
+ if (e.data.type && e.data.type === type)
+ resolve(e);
+ }, { once: true });
+ });
+};
+
+// Reset SameSite test cookies on |origin|. If |origin| matches `self.origin`, assert
+// (via `document.cookie`) that they were properly removed and reset.
+async function resetSameSiteCookies(origin, value) {
+ let w = window.open(origin + "/cookies/samesite/resources/puppet.html");
+ try {
+ await wait_for_message("READY", origin);
+ w.postMessage({type: "drop", useOwnOrigin: true}, "*");
+ await wait_for_message("drop-complete", origin);
+ if (origin == self.origin) {
+ assert_dom_cookie("samesite_strict", value, false);
+ assert_dom_cookie("samesite_lax", value, false);
+ assert_dom_cookie("samesite_none", value, false);
+ assert_dom_cookie("samesite_unspecified", value, false);
+ }
+
+ w.postMessage({type: "set", value: value, useOwnOrigin: true}, "*");
+ await wait_for_message("set-complete", origin);
+ if (origin == self.origin) {
+ assert_dom_cookie("samesite_strict", value, true);
+ assert_dom_cookie("samesite_lax", value, true);
+ assert_dom_cookie("samesite_none", value, true);
+ assert_dom_cookie("samesite_unspecified", value, true);
+ }
+ } finally {
+ w.close();
+ }
+}
+
+// Given an |expectedStatus| and |expectedValue|, assert the |cookies| contains
+// the proper set of cookie names and values. Expects SameSite-Lax-by-default.
+function verifySameSiteCookieState(expectedStatus, expectedValue, cookies, domCookieStatus) {
+ assert_equals(cookies["samesite_none"], expectedValue, "SameSite=None cookies are always sent.");
+ if (expectedStatus == SameSiteStatus.CROSS_SITE) {
+ assert_not_equals(cookies["samesite_strict"], expectedValue, "SameSite=Strict cookies are not sent with cross-site requests.");
+ assert_not_equals(cookies["samesite_lax"], expectedValue, "SameSite=Lax cookies are not sent with cross-site requests.");
+ assert_not_equals(cookies["samesite_unspecified"], expectedValue, "Unspecified-SameSite cookies are not sent with cross-site requests.");
+ } else if (expectedStatus == SameSiteStatus.LAX) {
+ assert_not_equals(cookies["samesite_strict"], expectedValue, "SameSite=Strict cookies are not sent with lax requests.");
+ assert_equals(cookies["samesite_lax"], expectedValue, "SameSite=Lax cookies are sent with lax requests.");
+ assert_equals(cookies["samesite_unspecified"], expectedValue, "Unspecified-SameSite cookies are are sent with lax requests.")
+ } else if (expectedStatus == SameSiteStatus.STRICT) {
+ assert_equals(cookies["samesite_strict"], expectedValue, "SameSite=Strict cookies are sent with strict requests.");
+ assert_equals(cookies["samesite_lax"], expectedValue, "SameSite=Lax cookies are sent with strict requests.");
+ assert_equals(cookies["samesite_unspecified"], expectedValue, "Unspecified-SameSite cookies are are sent with strict requests.")
+ }
+
+ if (cookies["domcookies"]) {
+ verifyDocumentCookieSameSite(domCookieStatus, expectedValue, cookies['domcookies']);
+ }
+}
+
+function verifyDocumentCookieSameSite(expectedStatus, expectedValue, domcookies) {
+ const cookies = domcookies.split(";")
+ .map(cookie => cookie.trim().split("="))
+ .reduce((obj, cookie) => {
+ obj[cookie[0]] = cookie[1];
+ return obj;
+ }, {});
+
+ if (expectedStatus == DomSameSiteStatus.SAME_SITE) {
+ assert_equals(cookies["samesite_none"], expectedValue, "SameSite=None cookies are always included in document.cookie.");
+ assert_equals(cookies["samesite_unspecified"], expectedValue, "Unspecified-SameSite cookies are always included in document.cookie.");
+ assert_equals(cookies["samesite_strict"], expectedValue, "SameSite=Strict cookies are always included in document.cookie.");
+ assert_equals(cookies["samesite_lax"], expectedValue, "SameSite=Lax cookies are always included in document.cookie.");
+ } else if (expectedStatus == DomSameSiteStatus.CROSS_SITE) {
+ assert_equals(cookies["samesite_none"], expectedValue, "SameSite=None cookies are always included in document.cookie.");
+ assert_not_equals(cookies["samesite_unspecified"], expectedValue, "Unspecified-SameSite cookies are not included in document.cookie when cross-site.");
+ assert_not_equals(cookies["samesite_strict"], expectedValue, "SameSite=Strict cookies are not included in document.cookie when cross-site.");
+ assert_not_equals(cookies["samesite_lax"], expectedValue, "SameSite=Lax cookies are not included in document.cookie when cross-site.");
+ }
+}
+
+//
+// LeaveSecureCookiesAlone-specific test helpers:
+//
+
+window.SecureStatus = {
+ INSECURE_COOKIE_ONLY: "1",
+ BOTH_COOKIES: "2",
+};
+
+//Reset SameSite test cookies on |origin|. If |origin| matches `self.origin`, assert
+//(via `document.cookie`) that they were properly removed and reset.
+function resetSecureCookies(origin, value) {
+return credFetch(origin + "/cookies/resources/dropSecure.py")
+ .then(_ => {
+ if (origin == self.origin) {
+ assert_dom_cookie("alone_secure", value, false);
+ assert_dom_cookie("alone_insecure", value, false);
+ }
+ })
+ .then(_ => {
+ return credFetch(origin + "/cookie/resources/setSecure.py?" + value)
+ })
+}
+
+// Reset SameSite=None test cookies on |origin|. If |origin| matches
+// `self.origin`, assert (via `document.cookie`) that they were properly
+// removed.
+function resetSameSiteNoneCookies(origin, value) {
+ return credFetch(origin + "/cookies/resources/dropSameSiteNone.py")
+ .then(_ => {
+ if (origin == self.origin) {
+ assert_dom_cookie("samesite_none_insecure", value, false);
+ assert_dom_cookie("samesite_none_secure", value, false);
+ }
+ })
+ .then(_ => {
+ return credFetch(origin + "/cookies/resources/setSameSiteNone.py?" + value);
+ })
+}
+
+// Reset test cookies with multiple SameSite attributes on |origin|.
+// If |origin| matches `self.origin`, assert (via `document.cookie`)
+// that they were properly removed.
+function resetSameSiteMultiAttributeCookies(origin, value) {
+ return credFetch(origin + "/cookies/resources/dropSameSiteMultiAttribute.py")
+ .then(_ => {
+ if (origin == self.origin) {
+ assert_dom_cookie("samesite_unsupported", value, false);
+ assert_dom_cookie("samesite_unsupported_none", value, false);
+ assert_dom_cookie("samesite_unsupported_lax", value, false);
+ assert_dom_cookie("samesite_unsupported_strict", value, false);
+ assert_dom_cookie("samesite_none_unsupported", value, false);
+ assert_dom_cookie("samesite_lax_unsupported", value, false);
+ assert_dom_cookie("samesite_strict_unsupported", value, false);
+ assert_dom_cookie("samesite_lax_none", value, false);
+ }
+ })
+ .then(_ => {
+ return credFetch(origin + "/cookies/resources/setSameSiteMultiAttribute.py?" + value);
+ })
+}
+
+//
+// DOM based cookie manipulation APIs
+//
+
+// erase cookie value and set for expiration
+function erase_cookie_from_js(name, params) {
+ document.cookie = `${name}=0; expires=${new Date(0).toUTCString()}; ${params};`;
+ var re = new RegExp("(?:^|; )" + name);
+ assert_equals(re.test(document.cookie), false, "Sanity check: " + name + " has been deleted.");
+}
diff --git a/testing/web-platform/tests/cookies/resources/cookie-test.js b/testing/web-platform/tests/cookies/resources/cookie-test.js
new file mode 100644
index 0000000000..a909e4d72f
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/cookie-test.js
@@ -0,0 +1,186 @@
+// getAndExpireCookiesForDefaultPathTest is a helper method to get and delete
+// cookies using echo-cookie.html.
+async function getAndExpireCookiesForDefaultPathTest() {
+ return new Promise((resolve, reject) => {
+ try {
+ const iframe = document.createElement('iframe');
+ iframe.style = 'display: none';
+ iframe.src = '/cookies/resources/echo-cookie.html';
+ iframe.addEventListener('load', (e) => {
+ const win = e.target.contentWindow;
+ const iframeCookies = win.getCookies();
+ win.expireCookies().then(() => {
+ document.documentElement.removeChild(iframe);
+ resolve(iframeCookies);
+ });
+ }, {once: true});
+ document.documentElement.appendChild(iframe);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+// getAndExpireCookiesForRedirectTest is a helper method to get and delete
+// cookies that were set from a Location header redirect.
+async function getAndExpireCookiesForRedirectTest(location) {
+ return new Promise((resolve, reject) => {
+ try {
+ const iframe = document.createElement('iframe');
+ iframe.style = 'display: none';
+ iframe.src = location;
+ const listener = (e) => {
+ if (typeof e.data == 'object' && 'cookies' in e.data) {
+ window.removeEventListener('message', listener);
+ document.documentElement.removeChild(iframe);
+ resolve(e.data.cookies);
+ }
+ };
+ window.addEventListener('message', listener);
+ iframe.addEventListener('load', (e) => {
+ e.target.contentWindow.postMessage('getAndExpireCookiesForRedirectTest', '*');
+ }, {once: true});
+ document.documentElement.appendChild(iframe);
+ } catch (e) {
+ reject(e);
+ }
+ });
+}
+
+// httpCookieTest sets a `cookie` (via HTTP), then asserts it was or was not set
+// via `expectedValue` (via the DOM). Then cleans it up (via test driver). Most
+// tests do not set a Path attribute, so `defaultPath` defaults to true. If the
+// cookie values are expected to cause the HTTP request or response to fail, the
+// test can be made to pass when this happens via `allowFetchFailure`, which
+// defaults to false.
+//
+// `cookie` may be a single cookie string, or an array of cookie strings, where
+// the order of the array items represents the order of the Set-Cookie headers
+// sent by the server.
+//
+// Note: this function has a dependency on testdriver.js. Any test files calling
+// it should include testdriver.js and testdriver-vendor.js
+function httpCookieTest(cookie, expectedValue, name, defaultPath = true,
+ allowFetchFailure = false) {
+ return promise_test((t) => {
+ var skipAssertions = false;
+ return new Promise(async (resolve, reject) => {
+ // The result is ignored as we're expiring cookies for cleaning here.
+ await getAndExpireCookiesForDefaultPathTest();
+ await test_driver.delete_all_cookies();
+ t.add_cleanup(test_driver.delete_all_cookies);
+
+ let encodedCookie = encodeURIComponent(JSON.stringify(cookie));
+ try {
+ await fetch(`/cookies/resources/cookie.py?set=${encodedCookie}`);
+ } catch {
+ if (allowFetchFailure) {
+ skipAssertions = true;
+ resolve();
+ } else {
+ reject('Failed to fetch /cookies/resources/cookie.py');
+ }
+ }
+ let cookies = document.cookie;
+ if (defaultPath) {
+ // for the tests where a Path is set from the request-uri
+ // path, we need to go look for cookies in an iframe at that
+ // default path.
+ cookies = await getAndExpireCookiesForDefaultPathTest();
+ }
+ resolve(cookies);
+ }).then((cookies) => {
+ if (skipAssertions) {
+ return;
+ }
+ if (Boolean(expectedValue)) {
+ assert_equals(cookies, expectedValue, 'The cookie was set as expected.');
+ } else {
+ assert_equals(cookies, expectedValue, 'The cookie was rejected.');
+ }
+ });
+ }, name);
+}
+
+// This is a variation on httpCookieTest, where a redirect happens via
+// the Location header and we check to see if cookies are sent via
+// getRedirectedCookies
+//
+// Note: the locations targeted by this function have a dependency on
+// path-redirect-shared.js and should be sure to include it.
+function httpRedirectCookieTest(cookie, expectedValue, name, location) {
+ return promise_test(async (t) => {
+ // The result is ignored as we're expiring cookies for cleaning here.
+ await getAndExpireCookiesForRedirectTest(location);
+
+ const encodedCookie = encodeURIComponent(JSON.stringify(cookie));
+ const encodedLocation = encodeURIComponent(location);
+ const setParams = `?set=${encodedCookie}&location=${encodedLocation}`;
+ await fetch(`/cookies/resources/cookie.py${setParams}`);
+ // for the tests where a redirect happens, we need to head
+ // to that URI to get the cookies (and then delete them there)
+ const cookies = await getAndExpireCookiesForRedirectTest(location);
+ if (Boolean(expectedValue)) {
+ assert_equals(cookies, expectedValue, 'The cookie was set as expected.');
+ } else {
+ assert_equals(cookies, expectedValue, 'The cookie was rejected.');
+ }
+ }, name);
+}
+
+// Sets a `cookie` via the DOM, checks it against `expectedValue` via the DOM,
+// then cleans it up via the DOM. This is needed in cases where going through
+// HTTP headers may modify the cookie line (e.g. by stripping control
+// characters).
+//
+// Note: this function has a dependency on testdriver.js. Any test files calling
+// it should include testdriver.js and testdriver-vendor.js
+function domCookieTest(cookie, expectedValue, name) {
+ return promise_test(async (t) => {
+ await test_driver.delete_all_cookies();
+ t.add_cleanup(test_driver.delete_all_cookies);
+
+ if (typeof cookie === "string") {
+ document.cookie = cookie;
+ } else if (Array.isArray(cookie)) {
+ for (const singlecookie of cookie) {
+ document.cookie = singlecookie;
+ }
+ } else {
+ throw new Error('Unexpected type passed into domCookieTest as cookie: ' + typeof cookie);
+ }
+ let cookies = document.cookie;
+ assert_equals(cookies, expectedValue, Boolean(expectedValue) ?
+ 'The cookie was set as expected.' :
+ 'The cookie was rejected.');
+ }, name);
+}
+
+// Returns an array of control characters along with their ASCII codes. Control
+// characters are defined by RFC 5234 to be %x00-1F / %x7F.
+function getCtlCharacters() {
+ const ctlCodes = [...Array(0x20).keys()]
+ .concat([0x7F]);
+ return ctlCodes.map(i => ({ code: i, chr: String.fromCharCode(i) }))
+}
+
+// Returns a cookie string with name set to "t" * nameLength and value
+// set to "1" * valueLength. Passing in 0 for either allows for creating
+// a name- or value-less cookie.
+//
+// Note: Cookie length checking should ignore the "=".
+function cookieStringWithNameAndValueLengths(nameLength, valueLength) {
+ return `${"t".repeat(nameLength)}=${"1".repeat(valueLength)}`;
+}
+
+// Finds the root window.top.opener and directs test_driver commands to it.
+//
+// If you see a message like: "Error: Tried to run in a non-testharness window
+// without a call to set_test_context." then you probably need to call this.
+function setTestContextUsingRootWindow() {
+ let test_window = window.top;
+ while (test_window.opener && !test_window.opener.closed) {
+ test_window = test_window.opener.top;
+ }
+ test_driver.set_test_context(test_window);
+}
diff --git a/testing/web-platform/tests/cookies/resources/cookie.py b/testing/web-platform/tests/cookies/resources/cookie.py
new file mode 100644
index 0000000000..936679bc9f
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/cookie.py
@@ -0,0 +1,42 @@
+import json
+
+from cookies.resources.helpers import setNoCacheAndCORSHeaders
+from wptserve.utils import isomorphic_decode
+from wptserve.utils import isomorphic_encode
+
+def set_cookie(headers, cookie_string):
+ """Helper method to add a Set-Cookie header"""
+ headers.append((b'Set-Cookie', isomorphic_encode(cookie_string)))
+
+def main(request, response):
+ """Set a cookie via GET params.
+
+ Usage: `/cookie.py?set={cookie}`
+
+ The passed-in cookie string should be stringified via JSON.stringify() (in
+ the case of multiple cookie headers sent in an array) and encoded via
+ encodeURIComponent, otherwise `parse_qsl` will split on any semicolons
+ (used by the Request.GET property getter). Note that values returned by
+ Request.GET will decode any percent-encoded sequences sent in a GET param
+ (which may or may not be surprising depending on what you're doing).
+
+ Note: here we don't use Response.delete_cookie() or similar other methods
+ in this resources directory because there are edge cases that are impossible
+ to express via those APIs, namely a bare (`Path`) or empty Path (`Path=`)
+ attribute. Instead, we pipe through the entire cookie and append `max-age=0`
+ to it.
+ """
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ if b'set' in request.GET:
+ cookie = isomorphic_decode(request.GET[b'set'])
+ cookie = json.loads(cookie)
+ cookies = cookie if isinstance(cookie, list) else [cookie]
+ for c in cookies:
+ set_cookie(headers, c)
+
+ if b'location' in request.GET:
+ headers.append((b'Location', request.GET[b'location']))
+ return 302, headers, b'{"redirect": true}'
+
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/drop.py b/testing/web-platform/tests/cookies/resources/drop.py
new file mode 100644
index 0000000000..612add2169
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/drop.py
@@ -0,0 +1,14 @@
+from cookies.resources.helpers import makeDropCookie, readParameter, setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Respond to `/cookie/drop?name={name}` by expiring the cookie named `{name}`."""
+ headers = setNoCacheAndCORSHeaders(request, response)
+ try:
+ # Expire the named cookie, and return a JSON-encoded success code.
+ name = readParameter(request, paramName=u"name", requireValue=True)
+ scheme = request.url_parts.scheme
+ headers.append(makeDropCookie(name, u"https" == scheme))
+ return headers, b'{"success": true}'
+ except:
+ return 500, headers, b'{"error" : "Empty or missing name parameter."}'
+
diff --git a/testing/web-platform/tests/cookies/resources/dropSameSite.py b/testing/web-platform/tests/cookies/resources/dropSameSite.py
new file mode 100644
index 0000000000..a0aa83558d
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/dropSameSite.py
@@ -0,0 +1,13 @@
+from cookies.resources.helpers import makeDropCookie, setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Respond to `/cookie/same-site/resources/dropSameSite.py by dropping the
+ four cookies set by setSameSiteCookies.py"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ # Expire the cookies, and return a JSON-encoded success code.
+ headers.append(makeDropCookie(b"samesite_strict", False))
+ headers.append(makeDropCookie(b"samesite_lax", False))
+ headers.append(makeDropCookie(b"samesite_none", False))
+ headers.append(makeDropCookie(b"samesite_unspecified", False))
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/dropSameSiteMultiAttribute.py b/testing/web-platform/tests/cookies/resources/dropSameSiteMultiAttribute.py
new file mode 100644
index 0000000000..af4fbeeca4
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/dropSameSiteMultiAttribute.py
@@ -0,0 +1,17 @@
+from cookies.resources.helpers import makeDropCookie, setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Respond to `/cookies/resources/dropSameSiteMultiAttribute.py by dropping
+ the cookies set by setSameSiteMultiAttribute.py"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ # Expire the cookies, and return a JSON-encoded success code.
+ headers.append(makeDropCookie(b"samesite_unsupported", True))
+ headers.append(makeDropCookie(b"samesite_unsupported_none", True))
+ headers.append(makeDropCookie(b"samesite_unsupported_lax", False))
+ headers.append(makeDropCookie(b"samesite_unsupported_strict", False))
+ headers.append(makeDropCookie(b"samesite_none_unsupported", True))
+ headers.append(makeDropCookie(b"samesite_lax_unsupported", True))
+ headers.append(makeDropCookie(b"samesite_strict_unsupported", True))
+ headers.append(makeDropCookie(b"samesite_lax_none", True))
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/dropSameSiteNone.py b/testing/web-platform/tests/cookies/resources/dropSameSiteNone.py
new file mode 100644
index 0000000000..2d0a837b5d
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/dropSameSiteNone.py
@@ -0,0 +1,11 @@
+from cookies.resources.helpers import makeDropCookie, setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Respond to `/cookies/resources/dropSameSiteNone.py by dropping the
+ two cookies set by setSameSiteNone.py"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ # Expire the cookies, and return a JSON-encoded success code.
+ headers.append(makeDropCookie(b"samesite_none_insecure", False))
+ headers.append(makeDropCookie(b"samesite_none_secure", True))
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/dropSecure.py b/testing/web-platform/tests/cookies/resources/dropSecure.py
new file mode 100644
index 0000000000..af71148cd6
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/dropSecure.py
@@ -0,0 +1,11 @@
+from cookies.resources.helpers import makeDropCookie, setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Respond to `/cookie/drop/secure` by dropping the two cookie set by
+ `setSecureTestCookies()`"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ # Expire the cookies, and return a JSON-encoded success code.
+ headers.append(makeDropCookie(b"alone_secure", False))
+ headers.append(makeDropCookie(b"alone_insecure", False))
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/echo-cookie.html b/testing/web-platform/tests/cookies/resources/echo-cookie.html
new file mode 100644
index 0000000000..ab78af8325
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/echo-cookie.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset=utf-8>
+ <title>helper iframe for matching cookie path tests</title>
+ <meta name=help href="http://tools.ietf.org/html/rfc6265#section-5.1.4">
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/cookies/resources/cookie-test.js"></script>
+</head>
+<body>
+<script>
+window.setCookie = function (name, path) {
+ document.cookie = name + '=1; Path=' + path + ';';
+};
+window.fetchCookieThen = function (name, path) {
+ return fetch("/cookies/resources/set-cookie.py?name=" + encodeURIComponent(name) + "&path=" + encodeURIComponent(path), {'credentials': 'include'});
+};
+window.isCookieSet = function (name, path) {
+ return document.cookie.match(name + '=1');
+};
+// Note: this function has a dependency on testdriver.js. Any test files calling
+// it should include testdriver.js and testdriver-vendor.js
+window.expireCookies = async () => {
+ setTestContextUsingRootWindow();
+ await test_driver.delete_all_cookies();
+};
+window.getCookies = () => document.cookie;
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/cookies/resources/echo-json.py b/testing/web-platform/tests/cookies/resources/echo-json.py
new file mode 100644
index 0000000000..9f1568e816
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/echo-json.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = [(b"Content-Type", b"application/json"),
+ (b"Access-Control-Allow-Credentials", b"true")]
+
+ if b"origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers[b"origin"]))
+
+ values = []
+ for key in request.cookies:
+ for value in request.cookies.get_list(key):
+ values.append(u"\"%s\": \"%s\"" % (isomorphic_decode(key), value))
+ body = u"{ %s }" % u",".join(values)
+ return headers, body
diff --git a/testing/web-platform/tests/cookies/resources/helpers.py b/testing/web-platform/tests/cookies/resources/helpers.py
new file mode 100644
index 0000000000..5fee5a9a91
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/helpers.py
@@ -0,0 +1,59 @@
+from urllib.parse import parse_qs
+
+from wptserve.utils import isomorphic_encode
+
+def setNoCacheAndCORSHeaders(request, response):
+ """Set Cache-Control, CORS and Content-Type headers appropriate for the cookie tests."""
+ headers = [(b"Content-Type", b"application/json"),
+ (b"Access-Control-Allow-Credentials", b"true")]
+
+ origin = b"*"
+ if b"origin" in request.headers:
+ origin = request.headers[b"origin"]
+
+ headers.append((b"Access-Control-Allow-Origin", origin))
+ #headers.append(("Access-Control-Allow-Credentials", "true"))
+ headers.append((b"Cache-Control", b"no-cache"))
+ headers.append((b"Expires", b"Fri, 01 Jan 1990 00:00:00 GMT"))
+
+ return headers
+
+def makeCookieHeader(name, value, otherAttrs):
+ """Make a Set-Cookie header for a cookie with the name, value and attributes provided."""
+ def makeAV(a, v):
+ if None == v or b"" == v:
+ return a
+ if isinstance(v, int):
+ return b"%s=%i" % (a, v)
+ else:
+ return b"%s=%s" % (a, v)
+
+ # ensure cookie name is always first
+ attrs = [b"%s=%s" % (name, value)]
+ attrs.extend(makeAV(a, v) for (a, v) in otherAttrs.items())
+ return (b"Set-Cookie", b"; ".join((attrs)))
+
+def makeDropCookie(name, secure):
+ attrs = {b"max-age": 0, b"path": b"/"}
+ if secure:
+ attrs[b"secure"] = b""
+ return makeCookieHeader(name, b"", attrs)
+
+def readParameter(request, paramName, requireValue):
+ """Read a parameter from the request. Raise if requireValue is set and the
+ parameter has an empty value or is not present."""
+ params = parse_qs(request.url_parts.query)
+ param = params[paramName][0].strip()
+ if len(param) == 0:
+ raise Exception(u"Empty or missing name parameter.")
+ return isomorphic_encode(param)
+
+def readCookies(request):
+ """Read the cookies from the client present in the request."""
+ cookies = {}
+ for key in request.cookies:
+ for cookie in request.cookies.get_list(key):
+ # do we care we'll clobber cookies here? If so, do we
+ # need to modify the test to take cookie names and value lists?
+ cookies[key] = cookie.value
+ return cookies
diff --git a/testing/web-platform/tests/cookies/resources/imgIfMatch.py b/testing/web-platform/tests/cookies/resources/imgIfMatch.py
new file mode 100644
index 0000000000..72fa50e66e
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/imgIfMatch.py
@@ -0,0 +1,16 @@
+from cookies.resources import helpers
+
+def main(request, response):
+ """Respond to `/cookie/imgIfMatch?name={name}&value={value}` with a 404 if
+ the cookie isn't present, and a transparent GIF otherwise."""
+ headers = helpers.setNoCacheAndCORSHeaders(request, response)
+ name = helpers.readParameter(request, paramName=u"name", requireValue=True)
+ value = helpers.readParameter(request, paramName=u"value", requireValue=True)
+ cookiesWithMatchingNames = request.cookies.get_list(name)
+ for cookie in cookiesWithMatchingNames:
+ if cookie.value == value:
+ # From https://github.com/mathiasbynens/small/blob/master/gif-transparent.gif
+ headers.append((b"Content-Type", b"image/gif"))
+ gif = b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xFF\xFF\xFF\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B"
+ return headers, gif
+ return 500, headers, b'{"error": {"message": "The cookie\'s value did not match the given value."}}'
diff --git a/testing/web-platform/tests/cookies/resources/list-cookies-for-script.py b/testing/web-platform/tests/cookies/resources/list-cookies-for-script.py
new file mode 100644
index 0000000000..b325d1f745
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/list-cookies-for-script.py
@@ -0,0 +1,12 @@
+import json
+from cookies.resources import helpers
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = helpers.setNoCacheAndCORSHeaders(request, response)
+ headers[0] = (b"Content-Type", b"text/javascript")
+ cookies = helpers.readCookies(request)
+ decoded_cookies = {isomorphic_decode(key): isomorphic_decode(val) for key, val in cookies.items()}
+ return headers, 'self._cookies = [{}];\n'.format(
+ ', '.join(['"{}"'.format(name) for name in decoded_cookies.keys()]))
diff --git a/testing/web-platform/tests/cookies/resources/list.py b/testing/web-platform/tests/cookies/resources/list.py
new file mode 100644
index 0000000000..4cb6639659
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/list.py
@@ -0,0 +1,10 @@
+import json
+from cookies.resources import helpers
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = helpers.setNoCacheAndCORSHeaders(request, response)
+ cookies = helpers.readCookies(request)
+ decoded_cookies = {isomorphic_decode(key): isomorphic_decode(val) for key, val in cookies.items()}
+ return headers, json.dumps(decoded_cookies)
diff --git a/testing/web-platform/tests/cookies/resources/navigate.html b/testing/web-platform/tests/cookies/resources/navigate.html
new file mode 100644
index 0000000000..077efba569
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/navigate.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ // Navigates the window to a location specified via URL query param.
+ const params = new URLSearchParams(window.location.search);
+ const loc = params.get('location');
+ window.location = loc;
+</script>
diff --git a/testing/web-platform/tests/cookies/resources/postToParent.py b/testing/web-platform/tests/cookies/resources/postToParent.py
new file mode 100644
index 0000000000..43f7d679fb
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/postToParent.py
@@ -0,0 +1,39 @@
+import json
+from cookies.resources import helpers
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ headers = helpers.setNoCacheAndCORSHeaders(request, response)
+ cookies = helpers.readCookies(request)
+ headers.append((b"Content-Type", b"text/html; charset=utf-8"))
+
+ tmpl = u"""
+<!DOCTYPE html>
+<script>
+ var data = %s;
+ data.type = "COOKIES";
+
+ try {
+ data.domcookies = document.cookie;
+ } catch (e) {}
+
+ if (window.parent != window) {
+ window.parent.postMessage(data, "*");
+ if (window.top != window.parent)
+ window.top.postMessage(data, "*");
+ }
+
+
+ if (window.opener)
+ window.opener.postMessage(data, "*");
+
+ window.addEventListener("message", e => {
+ console.log(e);
+ if (e.data == "reload")
+ window.location.reload();
+ });
+</script>
+"""
+ decoded_cookies = {isomorphic_decode(key): isomorphic_decode(val) for key, val in cookies.items()}
+ return headers, tmpl % json.dumps(decoded_cookies)
diff --git a/testing/web-platform/tests/cookies/resources/redirectWithCORSHeaders.py b/testing/web-platform/tests/cookies/resources/redirectWithCORSHeaders.py
new file mode 100644
index 0000000000..0af14da3e9
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/redirectWithCORSHeaders.py
@@ -0,0 +1,22 @@
+from cookies.resources.helpers import setNoCacheAndCORSHeaders
+
+def main(request, response):
+ """Simple handler that causes redirection.
+
+ The request should typically have two query parameters:
+ status - The status to use for the redirection. Defaults to 302.
+ location - The resource to redirect to.
+ """
+ status = 302
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+ headers = setNoCacheAndCORSHeaders(request, response)
+
+ location = request.GET.first(b"location")
+
+ headers.append((b"Location", location))
+
+ return status, headers, b""
diff --git a/testing/web-platform/tests/cookies/resources/set-cookie.py b/testing/web-platform/tests/cookies/resources/set-cookie.py
new file mode 100644
index 0000000000..59b5b8006a
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/set-cookie.py
@@ -0,0 +1,45 @@
+from datetime import date
+
+def main(request, response):
+ """
+ Returns cookie name and path from query params in a Set-Cookie header.
+
+ e.g.
+
+ > GET /cookies/resources/set-cookie.py?name=match-slash&path=%2F HTTP/1.1
+ > Host: localhost:8000
+ > User-Agent: curl/7.43.0
+ > Accept: */*
+ >
+ < HTTP/1.1 200 OK
+ < Content-Type: application/json
+ < Set-Cookie: match-slash=1; Path=/; Expires=09 Jun 2021 10:18:14 GMT
+ < Server: BaseHTTP/0.3 Python/2.7.12
+ < Date: Tue, 04 Oct 2016 18:16:06 GMT
+ < Content-Length: 80
+ """
+
+ name = request.GET[b'name']
+ path = request.GET[b'path']
+ samesite = request.GET.get(b'samesite')
+ secure = b'secure' in request.GET
+ expiry_year = date.today().year + 1
+ cookie = b"%s=1; Path=%s; Expires=09 Jun %d 10:18:14 GMT" % (name, path, expiry_year)
+ if samesite:
+ cookie += b";SameSite=%s" % samesite
+ if secure:
+ cookie += b";Secure"
+
+ headers = [
+ (b"Content-Type", b"application/json"),
+ (b"Set-Cookie", cookie)
+ ]
+
+ # Set the cors enabled headers.
+ origin = request.headers.get(b"Origin")
+ if origin is not None and origin != b"null":
+ headers.append((b"Access-Control-Allow-Origin", origin))
+ headers.append((b"Access-Control-Allow-Credentials", 'true'))
+
+ body = b"var dummy='value';"
+ return headers, body
diff --git a/testing/web-platform/tests/cookies/resources/set.py b/testing/web-platform/tests/cookies/resources/set.py
new file mode 100644
index 0000000000..eda9338c92
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/set.py
@@ -0,0 +1,15 @@
+from cookies.resources import helpers
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookie/set?{cookie}` by echoing `{cookie}` as a `Set-Cookie` header."""
+ headers = helpers.setNoCacheAndCORSHeaders(request, response)
+
+ # Cookies may require whitespace (e.g. in the `Expires` attribute), so the
+ # query string should be decoded.
+ cookie = unquote(request.url_parts.query)
+ headers.append((b"Set-Cookie", isomorphic_encode(cookie)))
+
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/setSameSite.py b/testing/web-platform/tests/cookies/resources/setSameSite.py
new file mode 100644
index 0000000000..05f0967088
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/setSameSite.py
@@ -0,0 +1,32 @@
+from cookies.resources.helpers import makeCookieHeader, setNoCacheAndCORSHeaders
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookie/set/samesite?{value}` by setting four cookies:
+ 1. `samesite_strict={value};SameSite=Strict;path=/`
+ 2. `samesite_lax={value};SameSite=Lax;path=/`
+ 3. `samesite_none={value};SameSite=None;path=/`
+ 4. `samesite_unspecified={value};path=/`
+ Then navigate to a page that will post a message back to the opener with the set cookies"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+ value = isomorphic_encode(request.url_parts.query)
+
+ headers.append((b"Content-Type", b"text/html; charset=utf-8"))
+ headers.append(makeCookieHeader(b"samesite_strict", value, {b"SameSite":b"Strict", b"path":b"/"}))
+ headers.append(makeCookieHeader(b"samesite_lax", value, {b"SameSite":b"Lax", b"path":b"/"}))
+ # SameSite=None cookies must be Secure.
+ headers.append(makeCookieHeader(b"samesite_none", value, {b"SameSite":b"None", b"path":b"/", b"Secure": b""}))
+ headers.append(makeCookieHeader(b"samesite_unspecified", value, {b"path":b"/"}))
+
+ document = b"""
+<!DOCTYPE html>
+<script>
+ // A same-site navigation, which should attach all cookies including SameSite ones.
+ // This is necessary because this page may have been reached via a cross-site navigation, so
+ // we might not have access to some SameSite cookies from here.
+ window.location = "../samesite/resources/echo-cookies.html";
+</script>
+"""
+
+ return headers, document
diff --git a/testing/web-platform/tests/cookies/resources/setSameSiteDomain.py b/testing/web-platform/tests/cookies/resources/setSameSiteDomain.py
new file mode 100644
index 0000000000..c8b7a71981
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/setSameSiteDomain.py
@@ -0,0 +1,36 @@
+from cookies.resources.helpers import makeCookieHeader, setNoCacheAndCORSHeaders
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookie/set/samesite?{value}` by setting four cookies:
+ 1. `samesite_strict={value};SameSite=Strict;path=/;domain={host}`
+ 2. `samesite_lax={value};SameSite=Lax;path=/;domain={host}`
+ 3. `samesite_none={value};SameSite=None;path=/;Secure;domain={host}`
+ 4. `samesite_unspecified={value};path=/;domain={host}`
+ Where {host} is the hostname from which this page is served. (Requesting this resource
+ without a Host header will result in a 500 server error.)
+ Then navigate to a page that will post a message back to the opener with the set cookies"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+ value = isomorphic_encode(request.url_parts.query)
+ host_header = request.headers['host']
+ hostname = host_header.split(b":")[0]
+ host = isomorphic_encode(hostname)
+ headers.append((b"Content-Type", b"text/html; charset=utf-8"))
+ headers.append(makeCookieHeader(b"samesite_strict", value, {b"SameSite":b"Strict", b"path":b"/", b"domain":host}))
+ headers.append(makeCookieHeader(b"samesite_lax", value, {b"SameSite":b"Lax", b"path":b"/", b"domain":host}))
+ # SameSite=None cookies must be Secure.
+ headers.append(makeCookieHeader(b"samesite_none", value, {b"SameSite":b"None", b"path":b"/", b"Secure": b"", b"domain":host}))
+ headers.append(makeCookieHeader(b"samesite_unspecified", value, {b"path":b"/", b"domain":host}))
+
+ document = b"""
+<!DOCTYPE html>
+<script>
+ // A same-site navigation, which should attach all cookies including SameSite ones.
+ // This is necessary because this page may have been reached via a cross-site navigation, so
+ // we might not have access to some SameSite cookies from here.
+ window.location = "../samesite/resources/echo-cookies.html";
+</script>
+"""
+
+ return headers, document
diff --git a/testing/web-platform/tests/cookies/resources/setSameSiteMultiAttribute.py b/testing/web-platform/tests/cookies/resources/setSameSiteMultiAttribute.py
new file mode 100644
index 0000000000..988f67f0b0
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/setSameSiteMultiAttribute.py
@@ -0,0 +1,60 @@
+from cookies.resources.helpers import makeCookieHeader, setNoCacheAndCORSHeaders
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookie/set/samesite?{value}` by setting the following combination of cookies:
+ 1. `samesite_unsupported={value};SameSite=Unsupported;path=/;Secure`
+ 2. `samesite_unsupported_none={value};SameSite=Unsupported;SameSite=None;path=/;Secure`
+ 3. `samesite_unsupported_lax={value};SameSite=Unsupported;SameSite=Lax;path=/`
+ 4. `samesite_unsupported_strict={value};SameSite=Unsupported;SameSite=Strict;path=/`
+ 5. `samesite_none_unsupported={value};SameSite=None;SameSite=Unsupported;path=/;Secure`
+ 6. `samesite_lax_unsupported={value};SameSite=Lax;SameSite=Unsupported;path=/;Secure`
+ 7. `samesite_strict_unsupported={value};SameSite=Strict;SameSite=Unsupported;path=/;Secure`
+ 8. `samesite_lax_none={value};SameSite=Lax;SameSite=None;path=/;Secure`
+ 9. `samesite_lax_strict={value};SameSite=Lax;SameSite=Strict;path=/`
+ 10. `samesite_strict_lax={value};SameSite=Strict;SameSite=Lax;path=/`
+ Then navigate to a page that will post a message back to the opener with the set cookies"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+ value = isomorphic_encode(request.url_parts.query)
+
+ headers.append((b"Content-Type", b"text/html; charset=utf-8"))
+ # Unknown value; single attribute
+ headers.append(makeCookieHeader(
+ b"samesite_unsupported", value, {b"SameSite":b"Unsupported", b"path":b"/", b"Secure":b""}))
+
+ # Multiple attributes; first attribute unknown
+ headers.append(makeCookieHeader(
+ b"samesite_unsupported_none", value, {b"SameSite":b"Unsupported", b"SameSite":b"None", b"path":b"/", b"Secure":b""}))
+ headers.append(makeCookieHeader(
+ b"samesite_unsupported_lax", value, {b"SameSite":b"Unsupported", b"SameSite":b"Lax", b"path":b"/"}))
+ headers.append(makeCookieHeader(
+ b"samesite_unsupported_strict", value, {b"SameSite":b"Unsupported", b"SameSite":b"Strict", b"path":b"/"}))
+
+ # Multiple attributes; second attribute unknown
+ headers.append(makeCookieHeader(
+ b"samesite_none_unsupported", value, {b"SameSite":b"None", b"SameSite":b"Unsupported", b"path":b"/", b"Secure":b""}))
+ headers.append(makeCookieHeader(
+ b"samesite_lax_unsupported", value, {b"SameSite":b"Lax", b"SameSite":b"Unsupported", b"path":b"/", b"Secure":b""}))
+ headers.append(makeCookieHeader(
+ b"samesite_strict_unsupported", value, {b"SameSite":b"Strict", b"SameSite":b"Unsupported", b"path":b"/", b"Secure":b""}))
+
+ # Multiple attributes; both known
+ headers.append(makeCookieHeader(
+ b"samesite_lax_none", value, {b"SameSite":b"Lax", b"SameSite":b"None", b"path":b"/", b"Secure":b""}))
+ headers.append(makeCookieHeader(
+ b"samesite_lax_strict", value, {b"SameSite":b"Lax", b"SameSite":b"Strict", b"path":b"/"}))
+ headers.append(makeCookieHeader(
+ b"samesite_strict_lax", value, {b"SameSite":b"Strict", b"SameSite":b"Lax", b"path":b"/"}))
+
+ document = b"""
+<!DOCTYPE html>
+<script>
+ // A same-site navigation, which should attach all cookies including SameSite ones.
+ // This is necessary because this page may have been reached via a cross-site navigation, so
+ // we might not have access to some SameSite cookies from here.
+ window.location = "../samesite/resources/echo-cookies.html";
+</script>
+"""
+
+ return headers, document
diff --git a/testing/web-platform/tests/cookies/resources/setSameSiteNone.py b/testing/web-platform/tests/cookies/resources/setSameSiteNone.py
new file mode 100644
index 0000000000..446c75eb44
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/setSameSiteNone.py
@@ -0,0 +1,16 @@
+from cookies.resources.helpers import makeCookieHeader, setNoCacheAndCORSHeaders
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookies/resources/setSameSiteNone.py?{value}` by setting two cookies:
+ 1. `samesite_none_insecure={value};SameSite=None;path=/`
+ 2. `samesite_none_secure={value};SameSite=None;Secure;path=/`
+ """
+ headers = setNoCacheAndCORSHeaders(request, response)
+ value = isomorphic_encode(request.url_parts.query)
+
+ headers.append(makeCookieHeader(b"samesite_none_insecure", value, {b"SameSite":b"None", b"path":b"/"}))
+ headers.append(makeCookieHeader(b"samesite_none_secure", value, {b"SameSite":b"None", b"Secure":b"", b"path":b"/"}))
+
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/setSecure.py b/testing/web-platform/tests/cookies/resources/setSecure.py
new file mode 100644
index 0000000000..dd0dd1622b
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/setSecure.py
@@ -0,0 +1,14 @@
+from cookies.resources.helpers import makeCookieHeader, readParameter, setNoCacheAndCORSHeaders
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ """Respond to `/cookie/set/secure?{value}` by setting two cookies:
+ alone_secure={value};secure;path=/`
+ alone_insecure={value};path=/"""
+ headers = setNoCacheAndCORSHeaders(request, response)
+ value = isomorphic_encode(request.url_parts.query)
+
+ headers.append(makeCookieHeader(b"alone_secure", value, {b"secure": b"", b"path": b"/"}))
+ headers.append(makeCookieHeader(b"alone_insecure", value, {b"path": b"/"}))
+ return headers, b'{"success": true}'
diff --git a/testing/web-platform/tests/cookies/resources/testharness-helpers.js b/testing/web-platform/tests/cookies/resources/testharness-helpers.js
new file mode 100644
index 0000000000..84368d6d99
--- /dev/null
+++ b/testing/web-platform/tests/cookies/resources/testharness-helpers.js
@@ -0,0 +1,49 @@
+// Given an array of potentially asynchronous tests, this function will execute
+// each in serial, ensuring that one and only one test is executing at a time.
+//
+// The test array should look like this:
+//
+//
+// var tests = [
+// [
+// "Test description goes here.",
+// function () {
+// // Test code goes here. `this` is bound to the test object.
+// }
+// ],
+// ...
+// ];
+//
+// The |setup| and |teardown| arguments are functions which are executed before
+// and after each test, respectively.
+function executeTestsSerially(testList, setup, teardown) {
+ var tests = testList.map(function (t) {
+ return {
+ test: async_test(t[0]),
+ code: t[1]
+ };
+ });
+
+ var executeNextTest = function () {
+ var current = tests.shift();
+ if (current === undefined) {
+ return;
+ }
+
+ // Setup the test fixtures.
+ if (setup) {
+ setup();
+ }
+
+ // Bind a callback to tear down the test fixtures.
+ if (teardown) {
+ current.test.add_cleanup(teardown);
+ }
+
+ // Execute the test.
+ current.test.step(current.code);
+ };
+
+ add_result_callback(function () { setTimeout(executeNextTest, 0) });
+ executeNextTest();
+}