/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; /* exported testCookies */ /* import-globals-from head.js */ async function testCookies(options) { // Changing the options object is a bit of a hack, but it allows us to easily // pass an expiration date to the background script. options.expiry = Date.now() / 1000 + 3600; async function background(backgroundOptions) { // Ask the parent scope to change some cookies we may or may not have // permission for. let awaitChanges = new Promise(resolve => { browser.test.onMessage.addListener(msg => { browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage"); resolve(); }); }); let changed = []; browser.cookies.onChanged.addListener(event => { changed.push(`${event.cookie.name}:${event.cause}`); }); browser.test.sendMessage("change-cookies"); // Try to access some cookies in various ways. let { url, domain, secure } = backgroundOptions; let failures = 0; let tallyFailure = () => { failures++; }; try { await awaitChanges; let cookie = await browser.cookies.get({ url, name: "foo" }); browser.test.assertEq( backgroundOptions.shouldPass, cookie != null, "should pass == get cookie" ); let cookies = await browser.cookies.getAll({ domain }); if (backgroundOptions.shouldPass) { browser.test.assertEq(2, cookies.length, "expected number of cookies"); } else { browser.test.assertEq(0, cookies.length, "expected number of cookies"); } await Promise.all([ browser.cookies .set({ url, domain, secure, name: "foo", value: "baz", expirationDate: backgroundOptions.expiry, }) .catch(tallyFailure), browser.cookies .set({ url, domain, secure, name: "bar", value: "quux", expirationDate: backgroundOptions.expiry, }) .catch(tallyFailure), browser.cookies.remove({ url, name: "deleted" }), ]); if (backgroundOptions.shouldPass) { // The order of eviction events isn't guaranteed, so just check that // it's there somewhere. let evicted = changed.indexOf("evicted:evicted"); if (evicted < 0) { browser.test.fail("got no eviction event"); } else { browser.test.succeed("got eviction event"); changed.splice(evicted, 1); } browser.test.assertEq( "x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit", changed.join(","), "expected changes" ); } else { browser.test.assertEq("", changed.join(","), "expected no changes"); } if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) { browser.test.assertEq(2, failures, "Expected failures"); } else { browser.test.assertEq(0, failures, "Expected no failures"); } browser.test.notifyPass("cookie-permissions"); } catch (error) { browser.test.fail(`Error: ${error} :: ${error.stack}`); browser.test.notifyFail("cookie-permissions"); } } let extension = ExtensionTestUtils.loadExtension({ manifest: { permissions: options.permissions, }, background: `(${background})(${JSON.stringify(options)})`, }); let stepOne = loadChromeScript(() => { const { addMessageListener, sendAsyncMessage } = this; addMessageListener("options", options => { let domain = options.domain.replace(/^\.?/, "."); // This will be evicted after we add a fourth cookie. Services.cookies.add( domain, "/", "evicted", "bar", options.secure, false, false, options.expiry, {}, Ci.nsICookie.SAMESITE_NONE, options.url.startsWith("https") ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP ); // This will be modified by the background script. Services.cookies.add( domain, "/", "foo", "bar", options.secure, false, false, options.expiry, {}, Ci.nsICookie.SAMESITE_NONE, options.url.startsWith("https") ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP ); // This will be deleted by the background script. Services.cookies.add( domain, "/", "deleted", "bar", options.secure, false, false, options.expiry, {}, Ci.nsICookie.SAMESITE_NONE, options.url.startsWith("https") ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP ); sendAsyncMessage("done"); }); }); stepOne.sendAsyncMessage("options", options); await stepOne.promiseOneMessage("done"); stepOne.destroy(); await extension.startup(); await extension.awaitMessage("change-cookies"); let stepTwo = loadChromeScript(() => { const { addMessageListener, sendAsyncMessage } = this; addMessageListener("options", options => { let domain = options.domain.replace(/^\.?/, "."); Services.cookies.add( domain, "/", "x", "y", options.secure, false, false, options.expiry, {}, Ci.nsICookie.SAMESITE_NONE, options.url.startsWith("https") ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP ); Services.cookies.add( domain, "/", "x", "z", options.secure, false, false, options.expiry, {}, Ci.nsICookie.SAMESITE_NONE, options.url.startsWith("https") ? Ci.nsICookie.SCHEME_HTTPS : Ci.nsICookie.SCHEME_HTTP ); Services.cookies.remove(domain, "x", "/", {}); sendAsyncMessage("done"); }); }); stepTwo.sendAsyncMessage("options", options); await stepTwo.promiseOneMessage("done"); stepTwo.destroy(); extension.sendMessage("cookies-changed"); await extension.awaitFinish("cookie-permissions"); await extension.unload(); let stepThree = loadChromeScript(() => { const { addMessageListener, sendAsyncMessage, assert } = this; let cookieSvc = Services.cookies; function getCookies(host) { let cookies = []; for (let cookie of cookieSvc.getCookiesFromHost(host, {})) { cookies.push(cookie); } return cookies.sort((a, b) => a.name.localeCompare(b.name)); } addMessageListener("options", options => { let cookies = getCookies(options.domain); if (options.shouldPass) { assert.equal(cookies.length, 2, "expected two cookies for host"); assert.equal(cookies[0].name, "bar", "correct cookie name"); assert.equal(cookies[0].value, "quux", "correct cookie value"); assert.equal(cookies[1].name, "foo", "correct cookie name"); assert.equal(cookies[1].value, "baz", "correct cookie value"); } else if (options.shouldWrite) { // Note: |shouldWrite| applies only when |shouldPass| is false. // This is necessary because, unfortunately, websites (and therefore web // extensions) are allowed to write some cookies which they're not allowed // to read. assert.equal(cookies.length, 3, "expected three cookies for host"); assert.equal(cookies[0].name, "bar", "correct cookie name"); assert.equal(cookies[0].value, "quux", "correct cookie value"); assert.equal(cookies[1].name, "deleted", "correct cookie name"); assert.equal(cookies[2].name, "foo", "correct cookie name"); assert.equal(cookies[2].value, "baz", "correct cookie value"); } else { assert.equal(cookies.length, 2, "expected two cookies for host"); assert.equal(cookies[0].name, "deleted", "correct second cookie name"); assert.equal(cookies[1].name, "foo", "correct cookie name"); assert.equal(cookies[1].value, "bar", "correct cookie value"); } for (let cookie of cookies) { cookieSvc.remove(cookie.host, cookie.name, "/", {}); } // Make sure we don't silently poison subsequent tests if something goes wrong. assert.equal(getCookies(options.domain).length, 0, "cookies cleared"); sendAsyncMessage("done"); }); }); stepThree.sendAsyncMessage("options", options); await stepThree.promiseOneMessage("done"); stepThree.destroy(); }