diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js | 895 |
1 files changed, 895 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js b/toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js new file mode 100644 index 0000000000..7a4170a51e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_cookies_partitionKey.js @@ -0,0 +1,895 @@ +"use strict"; + +/** + * This test verifies that the extension API's access to cookies is consistent + * with the cookies as seen by web pages under the following modes: + * - Every top-level document shares the same cookie jar, every subdocument of + * the top-level document has a distinct cookie jar tied to the site of the + * top-level document (dFPI). + * - All documents have a cookie jar keyed by the domain of the top-level + * document (FPI). + * - All cookies are in one cookie jar (classic behavior = no FPI nor dFPI) + * + * FPI and dFPI are implemented using OriginAttributes, and historically the + * consequence of not recognizing an origin attribute is that cookies cannot be + * deleted. Hence, the functionality of the cookies API is verified as follows, + * by the testCookiesAPI/runTestCase methods. + * + * 1. Load page that creates cookies for the top and a framed document: + * - "delete_me" + * - "edit_me" + * 2. cookies.getAll: get all cookies with extension API. + * 3. cookies.remove: Remove "delete_me" cookies with the extension API. + * 4. cookies.set: Edit "edit_me" cookie with the extension API. + * 5. Verify that the web page can see "edit_me" cookie (via document.cookie). + * 6. cookies.get: "edit_me" is still present. + * 7. cookies.remove: "edit_me" can be removed. + * 8. cookies.getAll: no cookies left. + */ + +const FIRST_DOMAIN = "first.example.com"; +const FIRST_DOMAIN_ETLD_PLUS_1 = "example.com"; +const FIRST_DOMAIN_ETLD_PLUS_MANY = "nested.under.first.example.com"; +const THIRD_PARTY_DOMAIN = "third.example.net"; +const server = createHttpServer({ + hosts: [FIRST_DOMAIN, FIRST_DOMAIN_ETLD_PLUS_MANY, THIRD_PARTY_DOMAIN], +}); +const LOCAL_IP_AND_PORT = `127.0.0.1:${server.identity.primaryPort}`; + +server.registerPathHandler("/top", (request, response) => { + response.setHeader("Set-Cookie", `delete_me=top; SameSite=none`); + response.setHeader("Set-Cookie", `edit_me=top; SameSite=none`, true); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write( + `<!DOCTYPE html><iframe src="//third.example.net/framed"></iframe>` + ); +}); +server.registerPathHandler("/framed", (request, response) => { + response.setHeader("Set-Cookie", `delete_me=frame; SameSite=none`); + response.setHeader("Set-Cookie", `edit_me=frame; SameSite=none`, true); +}); + +// Background script of the extension that drives the test. +// It first waits for the content scripts in /top and /framed to connect, +// in order to verify that cookie operations by the extension API are reflected +// to the web page (verified through document.cookie from the content script). +function backgroundScript() { + let portsByDomain = new Map(); + + async function getDocumentCookies(port) { + return new Promise(resolve => { + port.onMessage.addListener(function listener(cookieString) { + port.onMessage.removeListener(listener); + resolve(cookieString); + }); + port.postMessage("get_cookies"); + }); + } + + // Stringify cookie identifier for comparisons in assertions. + function stringifyCookie(cookie) { + if (!cookie) { + return "COOKIE MISSING"; + } + let domain = cookie.domain; + if (!domain) { + // The return value of `cookies.remove` has a URL instead of a domain. + domain = new URL(cookie.url).hostname; + } + return `${cookie.name} domain=${domain} firstPartyDomain=${ + cookie.firstPartyDomain + } partitionKey=${JSON.stringify(cookie.partitionKey)}`; + } + function stringifyCookies(cookies) { + return cookies.map(stringifyCookie).sort().join(" , "); + } + + // detailsIn may have partitionKey and firstPartyDomain attributes. + // expectedOut has partitionKey and firstPartyDomain attributes. + async function runTestCase({ domain, detailsIn, expectedOut }) { + const port = portsByDomain.get(domain); + browser.test.assertTrue(port, `Got port to document for ${domain}`); + + let allCookies = await browser.cookies.getAll({ + domain, + firstPartyDomain: null, + partitionKey: {}, + }); + + let allCookiesWithFPD = await browser.cookies.getAll({ + domain, + ...detailsIn, + }); + browser.test.assertEq( + stringifyCookies(allCookies), + stringifyCookies(allCookiesWithFPD), + "cookies.getAll returns consistent results" + ); + + for (let [key, expectedValue] of Object.entries(expectedOut)) { + expectedValue = JSON.stringify(expectedValue); + browser.test.assertTrue( + allCookies.every(c => JSON.stringify(c[key]) === expectedValue), + `All ${allCookies.length} cookies have ${key}=${expectedValue}` + ); + } + + // delete_me: get, remove, get. + const cookieToDelete = { + url: `http://${domain}/`, + name: "delete_me", + ...detailsIn, + }; + const deletedCookie = { + ...cookieToDelete, + ...expectedOut, + }; + browser.test.assertEq( + stringifyCookie(deletedCookie), + stringifyCookie(await browser.cookies.get(cookieToDelete)), + "delete_me cookie exists before removal" + ); + browser.test.assertEq( + stringifyCookie(deletedCookie), + stringifyCookie(await browser.cookies.remove(cookieToDelete)), + "delete_me cookie has been removed by cookies.remove" + ); + browser.test.assertEq( + null, + await browser.cookies.get(cookieToDelete), + "delete_me cookie does not exist any more" + ); + + // edit_me: set, retrieve via document.cookie + const cookieToEdit = { + url: `http://${domain}/`, + name: "edit_me", + ...detailsIn, + }; + const editedCookie = await browser.cookies.set({ + ...cookieToEdit, + value: `new_value_${domain}`, + }); + browser.test.assertEq( + stringifyCookie({ ...cookieToEdit, ...expectedOut }), + stringifyCookie(editedCookie), + "edit_me cookie updated" + ); + browser.test.assertEq( + await getDocumentCookies(port), + `edit_me=new_value_${domain}`, + "Expected cookies after removing and editing a cookie" + ); + + // edit_me: get, remove, getAll. + browser.test.assertEq( + stringifyCookie(editedCookie), + stringifyCookie(await browser.cookies.get(cookieToEdit)), + "edit_me cookie still exists" + ); + await browser.cookies.remove(cookieToEdit); + let allCookiesAtEnd = await browser.cookies.getAll({ + domain, + firstPartyDomain: null, + partitionKey: {}, + }); + browser.test.assertEq( + "[]", + JSON.stringify(allCookiesAtEnd), + "No cookies left" + ); + } + + let resolveTestReady; + let testReadyPromise = new Promise(resolve => { + resolveTestReady = resolve; + }); + + browser.test.onMessage.addListener(async (msg, testCase) => { + await testReadyPromise; + browser.test.assertEq("runTest", msg, `Starting: ${testCase.description}`); + try { + await runTestCase(testCase); + } catch (e) { + browser.test.fail(`Unexpected error: ${e} :: ${e.stack}`); + } + browser.test.sendMessage("runTest_done"); + }); + + // cookie-checker-contentscript.js will connect. + browser.runtime.onConnect.addListener(port => { + portsByDomain.set(port.name, port); + browser.test.log(`Got port #${portsByDomain.size} ${port.name}`); + if (portsByDomain.size === 2) { + // The top document and the embedded frame has loaded and the + // content script that we use to read cookies is connected. + // The test can now start. + resolveTestReady(); + } + }); +} + +// The primary purpose of this test is to verify that the cookies API can read +// and write cookies that are actually in use by the web page. +async function testCookiesAPI({ testCases, topDomain = FIRST_DOMAIN }) { + let extension = ExtensionTestUtils.loadExtension({ + background: backgroundScript, + manifest: { + permissions: [ + "cookies", + // Remove port to work around bug 1350523. + `*://${topDomain.replace(/:\d+$/, "")}/*`, + `*://${THIRD_PARTY_DOMAIN}/*`, + ], + content_scripts: [ + { + js: ["cookie-checker-contentscript.js"], + matches: [ + // Remove port to work around bug 1362809. + `*://${topDomain.replace(/:\d+$/, "")}/top`, + `*://${THIRD_PARTY_DOMAIN}/framed`, + ], + all_frames: true, + run_at: "document_end", + }, + ], + }, + files: { + "cookie-checker-contentscript.js": () => { + const port = browser.runtime.connect({ name: location.hostname }); + port.onMessage.addListener(msg => { + browser.test.assertEq(msg, "get_cookies", "Expected port message"); + port.postMessage(document.cookie); + }); + }, + }, + }); + await extension.startup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + `http://${topDomain}/top` + ); + for (let testCase of testCases) { + info(`Running test case: ${testCase.description}`); + extension.sendMessage("runTest", testCase); + await extension.awaitMessage("runTest_done"); + } + await contentPage.close(); + await extension.unload(); +} + +add_task(async function setup() { + // SameSite=none is needed to set cookies in third-party contexts. + // SameSite=none usually requires Secure, but the test server doesn't support + // https, so disable the Secure requirement for SameSite=none. + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + false + ); +}); + +add_task(async function test_no_partitioning() { + const testCases = [ + { + description: "first-party cookies without any partitioning", + domain: FIRST_DOMAIN, + detailsIn: { + firstPartyDomain: "", + partitionKey: null, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies without any partitioning", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + // Without (d)FPI, firstPartyDomain and partitionKey are optional. + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + ]; + await runWithPrefs( + // dFPI is enabled by default on Nightly, disable it. + [["network.cookie.cookieBehavior", 4]], + () => testCookiesAPI({ testCases }) + ); +}); + +add_task(async function test_firstPartyIsolate() { + const testCases = [ + { + description: "first-party cookies with FPI", + domain: FIRST_DOMAIN, + detailsIn: { + firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1, + }, + expectedOut: { + firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1, + partitionKey: null, + }, + }, + { + description: "third-party cookies with FPI", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1, + }, + expectedOut: { + firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1, + partitionKey: null, + }, + }, + ]; + await runWithPrefs( + [ + // FPI is mutually exclusive with dFPI. Disable dFPI. + ["network.cookie.cookieBehavior", 4], + ["privacy.firstparty.isolate", true], + ], + () => testCookiesAPI({ testCases }) + ); +}); + +add_task(async function test_dfpi() { + const testCases = [ + { + description: "first-party cookies with dFPI", + domain: FIRST_DOMAIN, + detailsIn: { + // partitionKey is optional and expected to default to unpartitioned. + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies with dFPI", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` }, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` }, + }, + }, + ]; + await runWithPrefs( + // Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN. + [["network.cookie.cookieBehavior", 5]], + () => testCookiesAPI({ testCases }) + ); +}); + +add_task(async function test_dfpi_with_ip_and_port() { + const testCases = [ + { + description: "first-party cookies for IP with port", + domain: "127.0.0.1", + detailsIn: { + partitionKey: null, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies for IP with port", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` }, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` }, + }, + }, + ]; + await runWithPrefs( + // Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN. + [["network.cookie.cookieBehavior", 5]], + () => testCookiesAPI({ testCases, topDomain: LOCAL_IP_AND_PORT }) + ); +}); + +add_task(async function test_dfpi_with_nested_subdomains() { + const testCases = [ + { + description: "first-party cookies with DFPI at eTLD+many", + domain: FIRST_DOMAIN_ETLD_PLUS_MANY, + detailsIn: { + partitionKey: null, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies for first party with eTLD+many", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + // Partitioned cookies are keyed by eTLD+1, so even if eTLD+many is + // passed, then eTLD+1 is stored (and returned). + partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_MANY}` }, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` }, + }, + }, + ]; + await runWithPrefs( + // Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN. + [["network.cookie.cookieBehavior", 5]], + () => testCookiesAPI({ testCases, topDomain: FIRST_DOMAIN_ETLD_PLUS_MANY }) + ); +}); + +add_task(async function test_dfpi_with_non_default_use_site() { + // privacy.dynamic_firstparty.use_site is a pref that can be used to toggle + // the internal representation of partitionKey. True (default) means keyed + // by site (scheme, host, port); false means keyed by host only. + const testCases = [ + { + description: "first-party cookies with dFPI and use_site=false", + domain: FIRST_DOMAIN, + detailsIn: { + partitionKey: null, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies with dFPI and use_site=false", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` }, + }, + expectedOut: { + firstPartyDomain: "", + // When use_site=false, the scheme is not stored, and the + // implementation just prepends "https" as a dummy scheme. + partitionKey: { topLevelSite: `https://${FIRST_DOMAIN_ETLD_PLUS_1}` }, + }, + }, + ]; + await runWithPrefs( + [ + // Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN. + ["network.cookie.cookieBehavior", 5], + ["privacy.dynamic_firstparty.use_site", false], + ], + () => testCookiesAPI({ testCases }) + ); +}); +add_task(async function test_dfpi_with_ip_and_port_and_non_default_use_site() { + // privacy.dynamic_firstparty.use_site is a pref that can be used to toggle + // the internal representation of partitionKey. True (default) means keyed + // by site (scheme, host, port); false means keyed by host only. + const testCases = [ + { + description: "first-party cookies for IP:port with dFPI+use_site=false", + domain: "127.0.0.1", + detailsIn: { + partitionKey: null, + }, + expectedOut: { + firstPartyDomain: "", + partitionKey: null, + }, + }, + { + description: "third-party cookies for IP:port with dFPI+use_site=false", + domain: THIRD_PARTY_DOMAIN, + detailsIn: { + // When use_site=false, the scheme is not stored in the internal + // representation of the partitionKey. So even though the web page + // creates the cookie at HTTP, the cookies are still detected when + // "https" is used. + partitionKey: { topLevelSite: `https://${LOCAL_IP_AND_PORT}` }, + }, + expectedOut: { + firstPartyDomain: "", + // When use_site=false, the scheme and port are not stored. + // "https" is used as a dummy scheme, and the port is not used. + partitionKey: { topLevelSite: "https://127.0.0.1" }, + }, + }, + ]; + await runWithPrefs( + [ + // Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN. + ["network.cookie.cookieBehavior", 5], + ["privacy.dynamic_firstparty.use_site", false], + ], + () => testCookiesAPI({ testCases, topDomain: LOCAL_IP_AND_PORT }) + ); +}); + +add_task(async function dfpi_invalid_partitionKey() { + AddonTestUtils.init(globalThis); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" + ); + // The test below uses the browser.privacy API, which relies on + // ExtensionSettingsStore, which in turn depends on AddonManager. + await AddonTestUtils.promiseStartupManager(); + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + permissions: ["cookies", "*://example.com/*", "privacy"], + }, + async background() { + const url = "http://example.com/"; + const name = "dfpi_invalid_partitionKey_dummy_name"; + const value = "1"; + + // Shorthands to minimize boilerplate. + const set = d => browser.cookies.set({ url, name, value, ...d }); + const remove = d => browser.cookies.remove({ url, name, ...d }); + const get = d => browser.cookies.get({ url, name, ...d }); + const getAll = d => browser.cookies.getAll(d); + + await browser.test.assertRejects( + set({ partitionKey: { topLevelSite: "example.net" } }), + /Invalid value for 'partitionKey' attribute/, + "partitionKey must be a URL, not a domain" + ); + await browser.test.assertRejects( + set({ partitionKey: { topLevelSite: "chrome://foo" } }), + /Invalid value for 'partitionKey' attribute/, + "partitionKey cannot be the chrome:-scheme (canonicalization fails)" + ); + await browser.test.assertRejects( + set({ partitionKey: { topLevelSite: "chrome://foo/foo/foo" } }), + /Invalid value for 'partitionKey' attribute/, + "partitionKey cannot be the chrome:-scheme (canonicalization passes)" + ); + await browser.test.assertRejects( + set({ partitionKey: { topLevelSite: "http://[]:" } }), + /Invalid value for 'partitionKey' attribute/, + "partitionKey must be a valid URL" + ); + + browser.test.assertThrows( + () => get({ partitionKey: "" }), + /Error processing partitionKey: Expected object instead of ""/, + "cookies.get should reject invalid partitionKey (string)" + ); + browser.test.assertThrows( + () => get({ partitionKey: { topLevelSite: "http://x", badkey: 0 } }), + /Error processing partitionKey: Unexpected property "badkey"/, + "cookies.get should reject unsupported keys in partitionKey" + ); + await browser.test.assertRejects( + remove({ partitionKey: { topLevelSite: "invalid" } }), + /Invalid value for 'partitionKey' attribute/, + "cookies.remove should reject invalid partitionKey.topLevelSite" + ); + await browser.test.assertRejects( + get({ partitionKey: { topLevelSite: "invalid" } }), + /Invalid value for 'partitionKey' attribute/, + "cookies.get should reject invalid partitionKey.topLevelSite" + ); + await browser.test.assertRejects( + getAll({ partitionKey: { topLevelSite: "invalid" } }), + /Invalid value for 'partitionKey' attribute/, + "cookies.getAll should reject invalid partitionKey.topLevelSite" + ); + + // firstPartyDomain and partitionKey are mutually exclusive, because + // FPI and dFPI are mutually exclusive. + await browser.test.assertRejects( + set({ firstPartyDomain: "example.net", partitionKey: {} }), + /Partitioned cookies cannot have a 'firstPartyDomain' attribute./, + "partitionKey and firstPartyDomain cannot both be non-empty" + ); + + // On Nightly, dFPI is enabled by default. We have to disable it first, + // before we can enable FPI. Otherwise we would get error: + // Can't enable firstPartyIsolate when cookieBehavior is 'reject_trackers_and_partition_foreign' + await browser.privacy.websites.cookieConfig.set({ + value: { behavior: "reject_trackers" }, + }); + await browser.privacy.websites.firstPartyIsolate.set({ + value: true, + }); + + // FPI and dFPI are mutually exclusive. FPI is documented to require the + // firstPartyDomain attribute, let's verify that, despite it being + // technically possible to support both attributes. + for (let cookiesMethod of [get, getAll, remove, set]) { + await browser.test.assertRejects( + cookiesMethod({ partitionKey: { topLevelSite: url } }), + /First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set./, + `cookies.${cookiesMethod.name} requires firstPartyDomain when FPI is enabled` + ); + } + + // The pref changes above (to dFPI/FPI) via the browser.privacy API will + // be undone when the extension unloads. + + browser.test.sendMessage("test_done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("test_done"); + await extension.unload(); + + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function dfpi_moz_extension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["cookies", "*://example.com/*"], + }, + async background() { + let cookie = await browser.cookies.set({ + url: "http://example.com/", + name: "moz_ext_party", + value: "1", + // moz-extension: URL is passed here, in an attempt to mark the cookie + // as part of the "moz-extension:"-partition. Below we will expect "" + // because the dFPI implementation treats "moz-extension" as + // unpartitioned, see + // https://searchfox.org/mozilla-central/rev/ac7da6c7306d86e2f86a302ce1e170ad54b3c1fe/caps/OriginAttributes.cpp#79-82 + partitionKey: { topLevelSite: browser.runtime.getURL("/") }, + }); + browser.test.assertEq( + null, + cookie.partitionKey, + "Cookies in moz-extension:-URL are unpartitioned" + ); + let deletedCookie = await browser.cookies.remove({ + url: "http://example.com/", + name: "moz_ext_party", + partitionKey: { topLevelSite: "moz-extension://ignoreme" }, + }); + browser.test.assertEq( + null, + deletedCookie.partitionKey, + "moz-extension:-partition key is treated as unpartitioned" + ); + browser.test.sendMessage("test_done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("test_done"); + await extension.unload(); +}); + +add_task(async function dfpi_about_scheme_as_partitionKey() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["cookies", "*://example.com/*"], + }, + async background() { + let cookie = await browser.cookies.set({ + url: "http://example.com/", + name: "moz_ext_party", + value: "1", + partitionKey: { topLevelSite: "about:blank" }, + }); + // It doesn't really make sense to partition in `about:blank` (since it + // cannot really be a first party), but for completeness of test coverage + // we also check that the use of an about:-scheme results in predictable + // behavior. The weird "about://"-URL below is the serialization of the + // internal value of the partitionKey attribute: + // https://searchfox.org/mozilla-central/rev/ac7da6c7306d86e2f86a302ce1e170ad54b3c1fe/caps/OriginAttributes.cpp#73-77 + browser.test.assertEq( + "about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + cookie.partitionKey.topLevelSite, + "An URL-like representation of the internal about:-format is returned" + ); + let deletedCookie = await browser.cookies.remove({ + url: "http://example.com/", + name: "moz_ext_party", + partitionKey: { + topLevelSite: + "about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + }); + browser.test.assertEq( + "about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + deletedCookie.partitionKey.topLevelSite, + "Cookie can be deleted via the dummy about:-scheme" + ); + browser.test.sendMessage("test_done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("test_done"); + await extension.unload(); +}); + +// Same-site frames are expected to be unpartitioned. +// The cookies API can receive partitionKey and url that are same-site. While +// such cookies won't be sent to websites in practice, we do want to verify that +// the behavior is predictable. +add_task(async function test_url_is_same_site_as_partitionKey() { + // This loads a page with a frame at third.example.net (= THIRD_PARTY_DOMAIN). + let contentPage = await ExtensionTestUtils.loadContentPage( + `http://${THIRD_PARTY_DOMAIN}/top` + ); + await contentPage.close(); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["cookies", "*://third.example.net/"], + }, + async background() { + // Retrieve all cookies, partitioned and unpartitioned. We expect only + // unpartitioned cookies at first because the top frame and the child + // frame have the same origin. + let initialCookies = await browser.cookies.getAll({ partitionKey: {} }); + browser.test.assertEq( + "delete_me=frame,edit_me=frame", + initialCookies.map(c => `${c.name}=${c.value}`).join(), + "Same-site frames are in unpartitioned storage; /frame overwrites /top" + ); + browser.test.assertTrue( + await browser.cookies.remove({ + url: "https://third.example.net/", + name: "delete_me", + }), + "Removed unpartitioned cookie" + ); + browser.test.assertEq( + "[null,null]", + JSON.stringify(initialCookies.map(c => c.partitionKey)), + "Cookies in same-site/same-origin frames are not partitioned" + ); + + // We only have one unpartitioned cookie (edit_cookie) left. + + // Add new cookie whose partitionKey is same-site relative to url. + let newCookie = await browser.cookies.set({ + url: "http://third.example.net/", + name: "edit_me", + value: "url_is_partitionKey_eTLD+2", + partitionKey: { topLevelSite: "http://third.example.net" }, + }); + browser.test.assertEq( + "http://example.net", + newCookie.partitionKey.topLevelSite, + "Created cookie with partitionKey=url; eTLD+2 is normalized as eTLD+1" + ); + + browser.test.assertTrue( + await browser.cookies.remove({ + url: "http://third.example.net/", + name: "edit_me", + partitionKey: {}, + }), + "Removed unpartitioned cookie when partitionKey: {} is used" + ); + + browser.test.assertEq( + null, + await browser.cookies.remove({ + url: "http://third.example.net/", + name: "edit_me", + partitionKey: {}, + }), + "No more unpartitioned cookies to remove" + ); + + browser.test.assertTrue( + await browser.cookies.remove({ + url: "http://third.example.net/", + name: "edit_me", + partitionKey: { topLevelSite: "http://example.net" }, + }), + "Removed partitioned cookie when partitionKey is passed" + ); + + browser.test.sendMessage("test_done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("test_done"); + await extension.unload(); +}); + +add_task(async function test_getAll_partitionKey() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["cookies", "*://third.example.net/"], + }, + async background() { + const url = "http://third.example.net"; + const name = "test_url_is_identical_to_partitionKey"; + const partitionKey = { topLevelSite: "http://example.com" }; + const firstPartyDomain = "example.net"; + + // Create non-partitioned cookie, create partitioned cookie. + await browser.cookies.set({ url, name, value: "no_partition" }); + await browser.cookies.set({ url, name, value: "fpd", firstPartyDomain }); + await browser.cookies.set({ url, name, partitionKey, value: "party" }); + // partitionKey + firstPartyDomain was tested in dfpi_invalid_partitionKey + + async function getAllValues(details) { + let cookies = await browser.cookies.getAll(details); + let values = cookies.map(c => c.value); + return values.sort().join(); // Serialize for use with assertEq. + } + + browser.test.assertEq( + "no_partition", + await getAllValues({}), + "getAll() returns unpartitioned by default" + ); + + browser.test.assertEq( + "no_partition,party", + await getAllValues({ partitionKey: {} }), + "getAll() with partitionKey: {} returns all cookies" + ); + + browser.test.assertEq( + "party", + await getAllValues({ partitionKey }), + "getAll() with specific partitionKey returns partitionKey cookies only" + ); + + browser.test.assertEq( + "", + await getAllValues({ partitionKey: { topLevelSite: url } }), + "getAll() with partitionKey set to cookie URL does not match anything" + ); + + browser.test.assertEq( + "", + await getAllValues({ partitionKey, firstPartyDomain }), + "getAll() with non-empty partitionKey and firstPartyDomain does not match anything" + ); + browser.test.assertEq( + "fpd", + await getAllValues({ partitionKey: {}, firstPartyDomain }), + "getAll() with empty partitionKey and firstPartyDomain matches fpd" + ); + + browser.test.assertEq( + "fpd,no_partition,party", + await getAllValues({ partitionKey: {}, firstPartyDomain: null }), + "getAll() with empty partitionKey and firstPartyDomain:null matches everything" + ); + + await browser.cookies.remove({ url, name }); + await browser.cookies.remove({ url, name, firstPartyDomain }); + await browser.cookies.remove({ url, name, partitionKey }); + + browser.test.sendMessage("test_done"); + }, + }); + await extension.startup(); + await extension.awaitMessage("test_done"); + await extension.unload(); +}); + +add_task(async function no_unexpected_cookies_at_end_of_test() { + let results = []; + for (const cookie of Services.cookies.cookies) { + results.push({ + name: cookie.name, + value: cookie.value, + host: cookie.host, + originAttributes: cookie.originAttributes, + }); + } + Assert.deepEqual(results, [], "Test should not leave any cookies"); +}); |