diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/cookiebanners/test | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cookiebanners/test')
27 files changed, 5522 insertions, 0 deletions
diff --git a/toolkit/components/cookiebanners/test/browser/browser.ini b/toolkit/components/cookiebanners/test/browser/browser.ini new file mode 100644 index 0000000000..7de42757b2 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser.ini @@ -0,0 +1,36 @@ +[DEFAULT] +support-files = + head.js + +[browser_bannerClicking.js] +support-files = + file_banner.html + file_banner_b.html + file_delayed_banner.html +[browser_bannerClicking_events.js] +support-files = + file_banner.html +[browser_bannerClicking_runContext.js] +support-files = + file_banner.html +[browser_bannerClicking_slowLoad.js] +support-files = + file_delayed_banner_load.html + slowSubresource.sjs +[browser_bannerClicking_domainPref.js] +[browser_bannerClicking_globalRules.js] +[browser_cookiebanner_telemetry.js] +support-files = + file_iframe_banner.html +[browser_cookiebannerservice_domainPrefs.js] +[browser_cookiebannerservice_getRules.js] +[browser_cookiebannerservice_prefs.js] +[browser_cookiebannerservice.js] +[browser_cookiebannerservice_hasRuleForBCTree.js] +[browser_cookieinjector.js] +support-files = + testCookieHeader.sjs +[browser_bannerClicking_visibilityOverride.js] +support-files = + file_banner_invisible.html +[browser_cookieinjector_events.js] diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking.js new file mode 100644 index 0000000000..56ed6581bc --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking.js @@ -0,0 +1,434 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Test that the banner clicking won't click banner if the service is disabled or in detect-only mode. + */ +add_task(async function test_cookie_banner_service_disabled() { + for (let mode of [ + Ci.nsICookieBannerService.MODE_DISABLED, + Ci.nsICookieBannerService.MODE_DETECT_ONLY, + ]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", mode], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ], + }); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + } +}); + +/** + * Test that the banner clicking won't click banner if there is no rule. + */ +add_task(async function test_no_rules() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + // No click telemetry reported. + testClickResultTelemetry({}); +}); + +/** + * Test the banner clicking with MODE_REJECT. + */ +add_task(async function test_clicking_mode_reject() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { success: 1, success_dom_content_loaded: 1 }, + false + ); + + // No opt out rule for the example.org, the banner shouldn't be clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + // No matching rule means we don't record any telemetry for clicks. + await testClickResultTelemetry({ + success: 1, + success_dom_content_loaded: 1, + fail: 1, + fail_no_rule_for_mode: 1, + }); +}); + +/** + * Test the banner clicking with MODE_REJECT_OR_ACCEPT. + */ +add_task(async function test_clicking_mode_reject_or_accept() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + await testClickResultTelemetry({ + success: 2, + success_dom_content_loaded: 2, + }); +}); + +/** + * Test the banner clicking with the case where the banner is added after + * page loads and with a short amount of delay. + */ +add_task(async function test_clicking_with_delayed_banner() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + let TEST_PAGE = + TEST_ORIGIN_A + TEST_PATH + "file_delayed_banner.html?delay=100"; + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry({ + success: 1, + success_mutation_pre_load: 1, + }); +}); + +/** + * Test that the banner clicking in an iframe. + */ +add_task(async function test_embedded_iframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry({ + success: 1, + success_dom_content_loaded: 1, + }); +}); + +/** + * Test banner clicking with the private browsing window. + */ +add_task(async function test_pbm() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + await testClickResultTelemetry({ + success: 1, + success_dom_content_loaded: 1, + }); +}); + +/** + * Tests service mode pref combinations for normal and private browsing. + */ +add_task(async function test_pref_pbm_pref() { + info("Enable in normal browsing but disable in private browsing."); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + info("Disable in normal browsing but enable in private browsing."); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { + success: 2, + success_dom_content_loaded: 2, + }, + false + ); + + info( + "Set normal browsing to REJECT_OR_ACCEPT and private browsing to REJECT." + ); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + info( + "The normal browsing window accepts the banner according to the opt-in rule." + ); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + await testClickResultTelemetry( + { + success: 3, + success_dom_content_loaded: 3, + }, + false + ); + + info( + "The private browsing window should not perform any click, because there is only an opt-in rule." + ); + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + await testClickResultTelemetry({ + success: 3, + success_dom_content_loaded: 3, + fail: 1, + fail_no_rule_for_mode: 1, + }); +}); + +/** + * Test that the banner clicking in an iframe with the private browsing window. + */ +add_task(async function test_embedded_iframe_pbm() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + await openIframeAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + await testClickResultTelemetry({ + success: 1, + success_dom_content_loaded: 1, + }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_domainPref.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_domainPref.js new file mode 100644 index 0000000000..ff9b725710 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_domainPref.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Test that domain preference takes precedence over pref settings. + */ +add_task(async function test_domain_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + for (let testPBM of [false, true]) { + let testWin = window; + if (testPBM) { + testWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + } + + await testClickResultTelemetry({}); + + info( + "Make sure the example.org follows the pref setting when there is no domain preference." + ); + await openPageAndVerify({ + win: testWin, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + await testClickResultTelemetry( + { + fail: 1, + fail_no_rule_for_mode: 1, + }, + false + ); + + info("Set the domain preference of example.org to MODE_REJECT_OR_ACCEPT"); + let uri = Services.io.newURI(TEST_ORIGIN_B); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + testPBM + ); + + info( + "Verify if domain preference takes precedence over then the pref setting for example.org" + ); + await openPageAndVerify({ + win: testWin, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + Services.cookieBanners.removeAllDomainPrefs(testPBM); + + if (testPBM) { + await BrowserTestUtils.closeWindow(testWin); + } + + await testClickResultTelemetry({ + fail: 1, + fail_no_rule_for_mode: 1, + success: 1, + success_dom_content_loaded: 1, + }); + } +}); + +/** + * Test that domain preference works on the top-level domain. + */ +add_task(async function test_domain_preference_iframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + for (let testPBM of [false, true]) { + let testWin = window; + if (testPBM) { + testWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + } + + info( + "Make sure the example.org follows the pref setting when there is no domain preference for the top-level example.net." + ); + await openIframeAndVerify({ + win: testWin, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + await testClickResultTelemetry( + { + fail: 1, + fail_no_rule_for_mode: 1, + }, + false + ); + + info( + "Set the domain preference of the top-level domain to MODE_REJECT_OR_ACCEPT" + ); + let uri = Services.io.newURI(TEST_ORIGIN_C); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + testPBM + ); + + info( + "Verify if domain preference takes precedence over then the pref setting for top-level example.net" + ); + await openIframeAndVerify({ + win: testWin, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + Services.cookieBanners.removeAllDomainPrefs(testPBM); + + if (testPBM) { + await BrowserTestUtils.closeWindow(testWin); + } + + await testClickResultTelemetry({ + fail: 1, + fail_no_rule_for_mode: 1, + success: 1, + success_dom_content_loaded: 1, + }); + } +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_events.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_events.js new file mode 100644 index 0000000000..2a21841a68 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_events.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Triggers cookie banner clicking and tests the events dispatched. + * @param {*} options - Test options. + * @param {nsICookieBannerService::Modes} options.mode - The cookie banner service mode to test with. + * @param {*} options.openPageOptions - Options to overwrite for the openPageAndVerify call. + */ +async function runTest({ mode, openPageOptions = {} }) { + let initFn = () => { + // Insert rules only if the feature is enabled. + if (Services.cookieBanners.isEnabled) { + insertTestClickRules(); + } + }; + + let shouldHandleBanner = mode == Ci.nsICookieBannerService.MODE_REJECT; + let testURL = openPageOptions.testURL || TEST_PAGE_A; + let triggerFn = async () => { + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL, + visible: !shouldHandleBanner, + expected: shouldHandleBanner ? "OptOut" : "NoClick", + keepTabOpen: true, + ...openPageOptions, // Allow test callers to override any options for this method. + }); + }; + + await runEventTest({ mode, initFn, triggerFn, testURL }); + + // Clean up the test tab opened by openPageAndVerify. + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +/** + * Test the banner clicking events with MODE_REJECT. + */ +add_task(async function test_events_mode_reject() { + await runTest({ mode: Ci.nsICookieBannerService.MODE_REJECT }); +}); + +/** + * Test the banner clicking events with MODE_DETECT_ONLY. + */ +add_task(async function test_events_mode_detect_only() { + await runTest({ mode: Ci.nsICookieBannerService.MODE_DETECT_ONLY }); +}); + +/** + * Test the banner clicking events with MODE_DETECT_ONLY with a click rule that + * only supports opt-in.. + */ +add_task(async function test_events_mode_detect_only_opt_in_rule() { + await runTest({ + mode: Ci.nsICookieBannerService.MODE_DETECT_ONLY, + openPageOptions: { + // We only have an opt-in rule for DOMAIN_B. This ensures we still fire + // detection events for that case. + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + shouldHandleBanner: true, + expected: "NoClick", + }, + }); +}); + +/** + * Test the banner clicking events with MODE_DETECT_ONLY. + */ +add_task(async function test_events_mode_disabled() { + await runTest({ mode: Ci.nsICookieBannerService.MODE_DISABLED }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules.js new file mode 100644 index 0000000000..c097d31529 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Test the banner clicking with global rules and MODE_REJECT. + */ +add_task(async function test_clicking_global_rules() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.service.enableGlobalRules", true], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting global test rules."); + + info( + "Add global ruleA which targets an existing banner (presence) with existing buttons. This rule should handle the banner." + ); + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.id = genUUID(); + ruleA.domains = []; + ruleA.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleA); + + info( + "Add global ruleC which targets an existing banner (presence) but non-existing buttons." + ); + let ruleC = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleC.id = genUUID(); + ruleC.domains = []; + ruleC.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + "button#nonExistingOptOut", + "button#nonExistingOptIn" + ); + Services.cookieBanners.insertRule(ruleC); + + info("Add global ruleD which targets a non-existing banner (presence)."); + let ruleD = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleD.id = genUUID(); + ruleD.domains = []; + ruleD.addClickRule( + "div#nonExistingBanner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + null, + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleD); + + await testClickResultTelemetry({}); + + info("The global rule ruleA should handle both test pages with div#banner."); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + await openPageAndVerify({ + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry( + { + success: 2, + success_dom_content_loaded: 2, + }, + false + ); + + info("No global rule should handle TEST_PAGE_C with div#bannerB."); + await openPageAndVerify({ + domain: TEST_DOMAIN_C, + testURL: TEST_PAGE_C, + visible: true, + expected: "NoClick", + bannerId: "bannerB", + }); + + await testClickResultTelemetry( + { + success: 2, + success_dom_content_loaded: 2, + fail: 1, + fail_banner_not_found: 1, + }, + false + ); + + info("Test delayed banner handling with global rules."); + let TEST_PAGE = + TEST_ORIGIN_A + TEST_PATH + "file_delayed_banner.html?delay=100"; + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry({ + success: 3, + success_dom_content_loaded: 2, + fail: 1, + fail_banner_not_found: 1, + success_mutation_pre_load: 1, + }); +}); + +/** + * Test that domain-specific rules take precedence over global rules. + */ +add_task(async function test_clicking_global_rules_precedence() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.service.enableGlobalRules", true], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting global test rules."); + + info( + "Add global ruleA which targets an existing banner (presence) with existing buttons." + ); + let ruleGlobal = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleGlobal.id = genUUID(); + ruleGlobal.domains = []; + ruleGlobal.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + "button#optOut", + null + ); + Services.cookieBanners.insertRule(ruleGlobal); + + info("Add domain specific rule which also targets the existing banner."); + let ruleDomain = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleDomain.id = genUUID(); + ruleDomain.domains = [TEST_DOMAIN_A]; + ruleDomain.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + null, + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleDomain); + + await testClickResultTelemetry({}); + + info("Test that the domain-specific rule applies, not the global one."); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + // Because of the way the rules are setup OptOut would mean the global rule + // applies, opt-in means the domain specific rule applies. + expected: "OptIn", + }); + + await testClickResultTelemetry({ + success: 1, + success_dom_content_loaded: 1, + }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_runContext.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_runContext.js new file mode 100644 index 0000000000..7acf94997f --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_runContext.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Insert a test rule with the specified runContext. + * @param {RunContext} - The runContext to set for the rule. See nsIClickRule + * for documentation. + */ +function insertTestRules({ runContext }) { + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting test rules. " + JSON.stringify({ runContext })); + + info("Add opt-out click rule for DOMAIN_A."); + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.id = genUUID(); + ruleA.domains = [TEST_DOMAIN_A]; + + ruleA.addClickRule( + "div#banner", + false, + runContext, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleA); +} + +/** + * Test that banner clicking only runs if the context matches the runContext + * specified in the click rule. + */ +add_task(async function test_embedded_iframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestRules({ runContext: Ci.nsIClickRule.RUN_TOP }); + + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + insertTestRules({ runContext: Ci.nsIClickRule.RUN_CHILD }); + + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + insertTestRules({ runContext: Ci.nsIClickRule.RUN_ALL }); + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_slowLoad.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_slowLoad.js new file mode 100644 index 0000000000..d0d06f3324 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_slowLoad.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +/** + * Test the banner clicking with the case where the banner is added shortly after + * page load, but the page load itself is delayed. + */ +add_task(async function test_clicking_with_delayed_banner() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + await testClickResultTelemetry({}); + + // A test page that has a delayed load event. + let TEST_PAGE = TEST_ORIGIN_A + TEST_PATH + "file_delayed_banner_load.html"; + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE, + visible: false, + expected: "OptOut", + }); + + await testClickResultTelemetry({ + success: 1, + success_mutation_post_load: 1, + }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_visibilityOverride.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_visibilityOverride.js new file mode 100644 index 0000000000..b2746d7c2e --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_visibilityOverride.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// A test page that has an invisible cookie banner. This simulates sites where +// the banner is invisible whenever we test for it. See Bug 1793803. +const TEST_PAGE = TEST_ORIGIN_A + TEST_PATH + "file_banner_invisible.html"; + +add_setup(clickTestSetup); + +/** + * Insert a test rule with or without the skipPresenceVisibilityCheck flag. + * @param {boolean} skipPresenceVisibilityCheck - Whether to set the flag for + * the test rule. + */ +function insertVisibilityTestRules(skipPresenceVisibilityCheck) { + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info( + "Inserting test rules. " + JSON.stringify({ skipPresenceVisibilityCheck }) + ); + + info("Add opt-out click rule for DOMAIN_A."); + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.id = genUUID(); + ruleA.domains = [TEST_DOMAIN_A]; + + ruleA.addClickRule( + "div#banner", + skipPresenceVisibilityCheck, + Ci.nsIClickRule.RUN_TOP, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleA); +} + +/** + * Test that we click on an invisible banner element if + * skipPresenceVisibilityCheck is set. + */ +add_task(async function test_clicking_with_delayed_banner() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + for (let skipPresenceVisibilityCheck of [false, true]) { + insertVisibilityTestRules(skipPresenceVisibilityCheck); + + await testClickResultTelemetry({}); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE, + visible: false, + expected: skipPresenceVisibilityCheck ? "OptOut" : "NoClick", + }); + + let expectedTelemetry; + if (skipPresenceVisibilityCheck) { + expectedTelemetry = { + success: 1, + success_dom_content_loaded: 1, + }; + } else { + expectedTelemetry = { + fail: 1, + fail_banner_not_visible: 1, + }; + } + await testClickResultTelemetry(expectedTelemetry); + } +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js new file mode 100644 index 0000000000..dd2a85c43a --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js @@ -0,0 +1,750 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); + +const { + MODE_DISABLED, + MODE_REJECT, + MODE_REJECT_OR_ACCEPT, + MODE_DETECT_ONLY, + MODE_UNSET, +} = Ci.nsICookieBannerService; + +const TEST_MODES = [ + MODE_DISABLED, + MODE_REJECT, + MODE_REJECT_OR_ACCEPT, + MODE_DETECT_ONLY, + MODE_UNSET, // Should be recorded as invalid. + 99, // Invalid + -1, // Invalid +]; + +function convertModeToTelemetryString(mode) { + switch (mode) { + case MODE_DISABLED: + return "disabled"; + case MODE_REJECT: + return "reject"; + case MODE_REJECT_OR_ACCEPT: + return "reject_or_accept"; + case MODE_DETECT_ONLY: + return "detect_only"; + } + + return "invalid"; +} + +/** + * A helper function to verify the cookie rule look up telemetry. + * + * @param {String} probe The telemetry probe that we want to verify + * @param {Array} expected An array of objects that describe the expected value. + */ +function verifyLookUpTelemetry(probe, expected) { + for (let telemetry of expected) { + Assert.equal( + telemetry.count, + Glean.cookieBanners[probe][telemetry.label].testGetValue() + ); + } +} + +/** + * A helper function to verify the reload telemetry. + * + * @param {Number} length The expected length of the telemetry array. + * @param {Number} idx The index of the telemetry to be verified. + * @param {Object} expected An object that describe the expected value. + */ +function verifyReloadTelemetry(length, idx, expected) { + let events = Glean.cookieBanners.reload.testGetValue(); + + is(events.length, length, "There is a expected number of reload events."); + + let event = events[idx]; + + let { noRule, hasCookieRule, hasClickRule } = expected; + is(event.name, "reload", "The reload event has the correct name"); + is(event.extra.no_rule, noRule, "The extra field 'no_rule' is expected"); + is( + event.extra.has_cookie_rule, + hasCookieRule, + "The extra field 'has_cookie_rule' is expected" + ); + is( + event.extra.has_click_rule, + hasClickRule, + "The extra field 'has_click_rule' is expected" + ); +} + +/** + * A helper function to reload the browser and wait until it loads. + * + * @param {Browser} browser The browser object. + * @param {String} url The URL to be loaded. + */ +async function reloadBrowser(browser, url) { + let reloaded = BrowserTestUtils.browserLoaded(browser, false, url); + + // Reload as a user. + window.BrowserReload(); + + await reloaded; +} +/** + * A helper function to open the testing page for look up telemetry. + * + * @param {browser} browser The browser element + * @param {boolean} testInTop To indicate the page should be opened in top level + * @param {String} page The url of the testing page + * @param {String} domain The domain of the testing page + */ +async function openLookUpTelemetryTestPage(browser, testInTop, page, domain) { + let clickFinishPromise = promiseBannerClickingFinish(domain); + + if (testInTop) { + BrowserTestUtils.loadURI(browser, page); + } else { + BrowserTestUtils.loadURI(browser, TEST_ORIGIN_C); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [page], async testURL => { + let iframe = content.document.createElement("iframe"); + iframe.src = testURL; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + }); + } + + await clickFinishPromise; +} + +add_setup(async function() { + // Clear telemetry before starting telemetry test. + Services.fog.testResetFOG(); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); + if ( + Services.prefs.getIntPref("cookiebanners.service.mode") != + Ci.nsICookieBannerService.MODE_DISABLED || + Services.prefs.getIntPref("cookiebanners.service.mode.privateBrowsing") != + Ci.nsICookieBannerService.MODE_DISABLED + ) { + // Restore original rules. + Services.cookieBanners.resetRules(true); + } + + // Clear cookies that have been set during testing. + await SiteDataTestUtils.clear(); + }); + + await clickTestSetup(); +}); + +add_task(async function test_service_mode_telemetry() { + let service = Cc["@mozilla.org/cookie-banner-service;1"].getService( + Ci.nsIObserver + ); + + for (let mode of TEST_MODES) { + for (let modePBM of TEST_MODES) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", mode], + ["cookiebanners.service.mode.privateBrowsing", modePBM], + ], + }); + + // Trigger the idle-daily on the cookie banner service. + service.observe(null, "idle-daily", null); + + // Verify the telemetry value. + for (let label of [ + "disabled", + "reject", + "reject_or_accept", + "detect_only", + "invalid", + ]) { + let expected = convertModeToTelemetryString(mode) == label; + let expectedPBM = convertModeToTelemetryString(modePBM) == label; + + is( + Glean.cookieBanners.normalWindowServiceMode[label].testGetValue(), + expected, + `Has set label ${label} to ${expected} for mode ${mode}.` + ); + is( + Glean.cookieBanners.privateWindowServiceMode[label].testGetValue(), + expectedPBM, + `Has set label '${label}' to ${expected} for mode ${modePBM}.` + ); + } + } + } +}); + +add_task(async function test_rule_lookup_telemetry_no_rule() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + // Clear out all rules. + Services.cookieBanners.resetRules(false); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (let context of ["top", "iframe"]) { + let isTop = context === "top"; + + // Open a test domain. We should record a rule miss because there is no rule + // right now + info("Open a test domain."); + await openLookUpTelemetryTestPage( + tab.linkedBrowser, + isTop, + TEST_ORIGIN_A, + TEST_DOMAIN_A + ); + + let expectedTelemetryOnce = [ + { + label: `${context}_miss`, + count: 1, + }, + { + label: `${context}_cookie_miss`, + count: 1, + }, + { + label: `${context}_click_miss`, + count: 1, + }, + ]; + verifyLookUpTelemetry("ruleLookupByLoad", expectedTelemetryOnce); + verifyLookUpTelemetry("ruleLookupByDomain", expectedTelemetryOnce); + + info("Open the same domain again."); + // Load the same domain again, verify that the telemetry counts increases for + // load telemetry not not for domain telemetry. + await openLookUpTelemetryTestPage( + tab.linkedBrowser, + isTop, + TEST_ORIGIN_A, + TEST_DOMAIN_A + ); + + let expectedTelemetryTwice = [ + { + label: `${context}_miss`, + count: 2, + }, + { + label: `${context}_cookie_miss`, + count: 2, + }, + { + label: `${context}_click_miss`, + count: 2, + }, + ]; + verifyLookUpTelemetry("ruleLookupByLoad", expectedTelemetryTwice); + verifyLookUpTelemetry("ruleLookupByDomain", expectedTelemetryOnce); + } + + Services.fog.testResetFOG(); + Services.cookieBanners.resetDomainTelemetryRecord(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_rule_lookup_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + insertTestClickRules(); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (let type of ["click", "cookie"]) { + info(`Running the test for lookup telemetry for ${type} rules.`); + // Clear out all rules. + Services.cookieBanners.resetRules(false); + + info("Insert rules."); + if (type === "click") { + insertTestClickRules(); + } else { + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.domains = [TEST_DOMAIN_A]; + + Services.cookieBanners.insertRule(ruleA); + ruleA.addCookie( + true, + `cookieConsent_${TEST_DOMAIN_A}_1`, + "optOut1", + null, + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + ruleA.addCookie( + false, + `cookieConsent_${TEST_DOMAIN_A}_2`, + "optIn2", + null, + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + + let ruleB = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleB.domains = [TEST_DOMAIN_B]; + + Services.cookieBanners.insertRule(ruleB); + ruleB.addCookie( + false, + `cookieConsent_${TEST_DOMAIN_B}_1`, + "optIn1", + null, + "/", + 3600, + "UNSET", + false, + false, + true, + 0, + 0 + ); + } + + for (let context of ["top", "iframe"]) { + info(`Test in a ${context} context.`); + let isTop = context === "top"; + + info("Load a domain with opt-in and opt-out clicking rules."); + await openLookUpTelemetryTestPage( + tab.linkedBrowser, + isTop, + TEST_ORIGIN_A, + TEST_DOMAIN_A + ); + + let expectedTelemetry = [ + { + label: `${context}_hit`, + count: 1, + }, + { + label: `${context}_hit_opt_in`, + count: 1, + }, + { + label: `${context}_hit_opt_out`, + count: 1, + }, + { + label: `${context}_${type}_hit`, + count: 1, + }, + { + label: `${context}_${type}_hit_opt_in`, + count: 1, + }, + { + label: `${context}_${type}_hit_opt_out`, + count: 1, + }, + ]; + verifyLookUpTelemetry("ruleLookupByLoad", expectedTelemetry); + verifyLookUpTelemetry("ruleLookupByDomain", expectedTelemetry); + + info("Load a domain with only opt-in clicking rules"); + await openLookUpTelemetryTestPage( + tab.linkedBrowser, + isTop, + TEST_ORIGIN_B, + TEST_DOMAIN_B + ); + + expectedTelemetry = [ + { + label: `${context}_hit`, + count: 2, + }, + { + label: `${context}_hit_opt_in`, + count: 2, + }, + { + label: `${context}_hit_opt_out`, + count: 1, + }, + { + label: `${context}_${type}_hit`, + count: 2, + }, + { + label: `${context}_${type}_hit_opt_in`, + count: 2, + }, + { + label: `${context}_${type}_hit_opt_out`, + count: 1, + }, + ]; + + verifyLookUpTelemetry("ruleLookupByLoad", expectedTelemetry); + verifyLookUpTelemetry("ruleLookupByDomain", expectedTelemetry); + + info( + "Load a domain again to verify that we don't collect domain telemetry for this time." + ); + await openLookUpTelemetryTestPage( + tab.linkedBrowser, + isTop, + TEST_ORIGIN_A, + TEST_DOMAIN_A + ); + + // The domain telemetry should't be changed. + verifyLookUpTelemetry("ruleLookupByDomain", expectedTelemetry); + + expectedTelemetry = [ + { + label: `${context}_hit`, + count: 3, + }, + { + label: `${context}_hit_opt_in`, + count: 3, + }, + { + label: `${context}_hit_opt_out`, + count: 2, + }, + { + label: `${context}_${type}_hit`, + count: 3, + }, + { + label: `${context}_${type}_hit_opt_in`, + count: 3, + }, + { + label: `${context}_${type}_hit_opt_out`, + count: 2, + }, + ]; + + // Verify that the load telemetry still increases. + verifyLookUpTelemetry("ruleLookupByLoad", expectedTelemetry); + } + + Services.fog.testResetFOG(); + Services.cookieBanners.resetDomainTelemetryRecord(); + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_reload_telemetry_no_rule() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + // Clear out all rules. + Services.cookieBanners.resetRules(false); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_A + ); + + // Make sure there is no reload event at the beginning. + let events = Glean.cookieBanners.reload.testGetValue(); + ok(!events, "No reload event at the beginning."); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_A + "/"); + + // Check the telemetry + verifyReloadTelemetry(1, 0, { + noRule: "true", + hasCookieRule: "false", + hasClickRule: "false", + }); + + BrowserTestUtils.removeTab(tab); + Services.fog.testResetFOG(); +}); + +add_task(async function test_reload_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + insertTestClickRules(); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_A + ); + + // Make sure there is no reload event at the beginning. + let events = Glean.cookieBanners.reload.testGetValue(); + ok(!events, "No reload event at the beginning."); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_A + "/"); + + // Check the telemetry + verifyReloadTelemetry(1, 0, { + noRule: "false", + hasCookieRule: "false", + hasClickRule: "true", + }); + + // Add a both click rule and cookie rule for another domain. + let cookieRule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + cookieRule.domains = [TEST_DOMAIN_B]; + + Services.cookieBanners.insertRule(cookieRule); + cookieRule.addCookie( + false, + `cookieConsent_${TEST_DOMAIN_B}_1`, + "optIn1", + null, + "/", + 3600, + "UNSET", + false, + false, + true, + 0, + 0 + ); + cookieRule.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_ALL, + null, + null, + "button#optIn" + ); + + // Load the page with another origin. + BrowserTestUtils.loadURI(tab.linkedBrowser, TEST_ORIGIN_B); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_B + "/"); + + // Check the telemetry + verifyReloadTelemetry(2, 1, { + noRule: "false", + hasCookieRule: "true", + hasClickRule: "true", + }); + + BrowserTestUtils.removeTab(tab); + Services.fog.testResetFOG(); +}); + +add_task(async function test_reload_telemetry_mode_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + insertTestClickRules(); + + // Disable the cookie banner service. + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_A + ); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_A + "/"); + + // Check the telemetry. The reload telemetry should report no rule given that + // the service is disabled. + verifyReloadTelemetry(1, 0, { + noRule: "true", + hasCookieRule: "false", + hasClickRule: "false", + }); + + BrowserTestUtils.removeTab(tab); + Services.fog.testResetFOG(); +}); + +add_task(async function test_reload_telemetry_mode_reject() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + insertTestClickRules(); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_A + ); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_A + "/"); + + // Check the telemetry. The reload telemetry should report there is click rule + // for the domain has opt-out rule. + verifyReloadTelemetry(1, 0, { + noRule: "false", + hasCookieRule: "false", + hasClickRule: "true", + }); + + // Load the page with the domain only has opt-in click rule. + BrowserTestUtils.loadURI(tab.linkedBrowser, TEST_ORIGIN_B); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + // Trigger the reload + await reloadBrowser(tab.linkedBrowser, TEST_ORIGIN_B + "/"); + + // Check the telemetry. It should report there is no rule because the domain + // only has an opt-in click rule. + verifyReloadTelemetry(2, 1, { + noRule: "true", + hasCookieRule: "false", + hasClickRule: "false", + }); + + BrowserTestUtils.removeTab(tab); + Services.fog.testResetFOG(); +}); + +add_task(async function test_reload_telemetry_iframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + // Clear out all rules. + Services.cookieBanners.resetRules(false); + + // Insert a click rule for an iframe case. And add a cookie rule for the same + // domain. We shouldn't report there is a cookie rule for iframe because + // cookie rules are top-level only. + let cookieRule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + cookieRule.domains = [TEST_DOMAIN_A]; + Services.cookieBanners.insertRule(cookieRule); + + cookieRule.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_CHILD, + null, + null, + "button#optIn" + ); + cookieRule.addCookie( + false, + `cookieConsent_${TEST_DOMAIN_A}_1`, + "optIn1", + null, + "/", + 3600, + "UNSET", + false, + false, + true, + 0, + 0 + ); + + // Open a tab for testing. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_C + TEST_PATH + "file_iframe_banner.html" + ); + + // Trigger the reload + await reloadBrowser( + tab.linkedBrowser, + TEST_ORIGIN_C + TEST_PATH + "file_iframe_banner.html" + ); + + // Check the telemetry + verifyReloadTelemetry(1, 0, { + noRule: "false", + hasCookieRule: "false", + hasClickRule: "true", + }); + + BrowserTestUtils.removeTab(tab); + Services.fog.testResetFOG(); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice.js new file mode 100644 index 0000000000..15a3c2b5b3 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice.js @@ -0,0 +1,637 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function() { + registerCleanupFunction(() => { + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); + if ( + Services.prefs.getIntPref("cookiebanners.service.mode") != + Ci.nsICookieBannerService.MODE_DISABLED || + Services.prefs.getIntPref("cookiebanners.service.mode.privateBrowsing") != + Ci.nsICookieBannerService.MODE_DISABLED + ) { + // Restore original rules. + Services.cookieBanners.resetRules(true); + } + }); +}); + +add_task(async function test_insertAndGetRule() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Clear any preexisting rules"); + Services.cookieBanners.resetRules(false); + + is( + Services.cookieBanners.rules.length, + 0, + "Cookie banner service has no rules initially." + ); + + info("Test that we can't import rules with empty domain field."); + let ruleInvalid = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + Assert.throws( + () => { + Services.cookieBanners.insertRule(ruleInvalid); + }, + /NS_ERROR_FAILURE/, + "Inserting an invalid rule missing a domain should throw." + ); + + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.domains = ["example.com"]; + + Services.cookieBanners.insertRule(rule); + + is( + rule.cookiesOptOut.length, + 0, + "Should not have any opt-out cookies initially" + ); + is( + rule.cookiesOptIn.length, + 0, + "Should not have any opt-in cookies initially" + ); + + info("Clearing preexisting cookies rules for example.com."); + rule.clearCookies(); + + info("Adding cookies to the rule for example.com."); + rule.addCookie( + true, + "foo", + "bar", + "example.com", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + rule.addCookie( + true, + "foobar", + "barfoo", + "example.com", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + rule.addCookie( + false, + "foo", + "bar", + "foo.example.com", + "/myPath", + 3600, + "", + false, + false, + true, + 0, + 0 + ); + + info("Adding a click rule to the rule for example.com."); + rule.addClickRule( + "div#presence", + false, + Ci.nsIClickRule.RUN_TOP, + "div#hide", + "div#optOut", + "div#optIn" + ); + + is(rule.cookiesOptOut.length, 2, "Should have two opt-out cookies."); + is(rule.cookiesOptIn.length, 1, "Should have one opt-in cookie."); + + is( + Services.cookieBanners.rules.length, + 1, + "Cookie Banner Service has one rule." + ); + + let rule2 = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule2.domains = ["example.org"]; + + Services.cookieBanners.insertRule(rule2); + info("Clearing preexisting cookies rules for example.org."); + rule2.clearCookies(); + + info("Adding a cookie to the rule for example.org."); + rule2.addCookie( + false, + "foo2", + "bar2", + "example.org", + "/", + 0, + "", + false, + false, + false, + 0, + 0 + ); + + info("Adding a click rule to the rule for example.org."); + rule2.addClickRule( + "div#presence", + false, + Ci.nsIClickRule.RUN_TOP, + null, + null, + "div#optIn" + ); + + is( + Services.cookieBanners.rules.length, + 2, + "Cookie Banner Service has two rules." + ); + + info("Getting cookies by URI for example.com."); + let ruleArray = Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://example.com"), + false + ); + ok( + ruleArray && Array.isArray(ruleArray), + "getCookiesForURI should return a rule array." + ); + is(ruleArray.length, 2, "rule array should contain 2 rules."); + ruleArray.every(rule => { + ok(rule instanceof Ci.nsICookieRule, "Rule should have correct type."); + is(rule.cookie.host, "example.com", "Rule should have correct host."); + }); + + info("Clearing cookies of rule."); + rule.clearCookies(); + is(rule.cookiesOptOut.length, 0, "Should have no opt-out cookies."); + is(rule.cookiesOptIn.length, 0, "Should have no opt-in cookies."); + + info("Getting the click rule for example.com."); + let clickRules = Services.cookieBanners.getClickRulesForDomain( + "example.com", + true + ); + is( + clickRules.length, + 1, + "There should be one domain-specific click rule for example.com" + ); + let [clickRule] = clickRules; + + is( + clickRule.presence, + "div#presence", + "Should have the correct presence selector." + ); + is(clickRule.hide, "div#hide", "Should have the correct hide selector."); + is( + clickRule.optOut, + "div#optOut", + "Should have the correct optOut selector." + ); + is(clickRule.optIn, "div#optIn", "Should have the correct optIn selector."); + + info("Getting cookies by URI for example.org."); + let ruleArray2 = Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://example.org"), + false + ); + ok( + ruleArray2 && Array.isArray(ruleArray2), + "getCookiesForURI should return a rule array." + ); + is( + ruleArray2.length, + 0, + "rule array should contain no rules in MODE_REJECT (opt-out only)" + ); + + info("Getting the click rule for example.org."); + let clickRules2 = Services.cookieBanners.getClickRulesForDomain( + "example.org", + true + ); + is( + clickRules2.length, + 1, + "There should be one domain-specific click rule for example.org" + ); + let [clickRule2] = clickRules2; + is( + clickRule2.presence, + "div#presence", + "Should have the correct presence selector." + ); + ok(!clickRule2.hide, "Should have no hide selector."); + ok(!clickRule2.optOut, "Should have no target selector."); + is(clickRule.optIn, "div#optIn", "Should have the correct optIn selector."); + + info("Switching cookiebanners.service.mode to MODE_REJECT_OR_ACCEPT."); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + ruleArray2 = Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://example.org"), + false + ); + ok( + ruleArray2 && Array.isArray(ruleArray2), + "getCookiesForURI should return a rule array." + ); + is( + ruleArray2.length, + 1, + "rule array should contain one rule in mode MODE_REJECT_OR_ACCEPT (opt-out or opt-in)" + ); + + info("Calling getCookiesForURI for unknown domain."); + let ruleArrayUnknown = Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://example.net"), + false + ); + ok( + ruleArrayUnknown && Array.isArray(ruleArrayUnknown), + "getCookiesForURI should return a rule array." + ); + is(ruleArrayUnknown.length, 0, "rule array should contain no rules."); + + // Cleanup. + Services.cookieBanners.resetRules(false); +}); + +add_task(async function test_removeRule() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Clear any preexisting rules"); + Services.cookieBanners.resetRules(false); + + is( + Services.cookieBanners.rules.length, + 0, + "Cookie banner service has no rules initially." + ); + + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.id = genUUID(); + rule.domains = ["example.com"]; + + Services.cookieBanners.insertRule(rule); + + let rule2 = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule2.id = genUUID(); + rule2.domains = ["example.org"]; + + Services.cookieBanners.insertRule(rule2); + + is( + Services.cookieBanners.rules.length, + 2, + "Cookie banner service two rules after insert." + ); + + info("Removing rule for non existent example.net"); + let ruleExampleNet = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleExampleNet.id = genUUID(); + ruleExampleNet.domains = ["example.net"]; + Services.cookieBanners.removeRule(ruleExampleNet); + + is( + Services.cookieBanners.rules.length, + 2, + "Cookie banner service still has two rules." + ); + + info("Removing rule for non existent global rule."); + let ruleGlobal = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleGlobal.id = genUUID(); + ruleGlobal.domains = []; + Services.cookieBanners.removeRule(ruleGlobal); + + is( + Services.cookieBanners.rules.length, + 2, + "Cookie banner service still has two rules." + ); + + info("Removing rule for example.com"); + Services.cookieBanners.removeRule(rule); + + is( + Services.cookieBanners.rules.length, + 1, + "Cookie banner service should have one rule left after remove." + ); + + is( + Services.cookieBanners.rules[0].domains[0], + "example.org", + "It should be the example.org rule." + ); + + // Cleanup. + Services.cookieBanners.resetRules(false); +}); + +add_task(async function test_overwriteRule() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Clear any preexisting rules"); + Services.cookieBanners.resetRules(false); + + is( + Services.cookieBanners.rules.length, + 0, + "Cookie banner service has no rules initially." + ); + + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.domains = ["example.com"]; + + info("Adding a cookie so we can detect if the rule updates."); + rule.addCookie( + true, + "foo", + "original", + "example.com", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + + info("Adding a click rule so we can detect if the rule updates."); + rule.addClickRule("div#original"); + + Services.cookieBanners.insertRule(rule); + + let { cookie } = Services.cookieBanners.rules[0].cookiesOptOut[0]; + + is(cookie.name, "foo", "Should have set the correct cookie name."); + is(cookie.value, "original", "Should have set the correct cookie value."); + + info("Add a new rule with the same domain. It should be overwritten."); + + let ruleNew = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleNew.domains = ["example.com"]; + + ruleNew.addCookie( + true, + "foo", + "new", + "example.com", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + + ruleNew.addClickRule("div#new"); + + Services.cookieBanners.insertRule(ruleNew); + + let { cookie: cookieNew } = Services.cookieBanners.rules[0].cookiesOptOut[0]; + is(cookieNew.name, "foo", "Should have set the original cookie name."); + is(cookieNew.value, "new", "Should have set the updated cookie value."); + + let { presence: presenceNew } = Services.cookieBanners.rules[0].clickRule; + is(presenceNew, "div#new", "Should have set the updated presence value"); + + // Cleanup. + Services.cookieBanners.resetRules(false); +}); + +add_task(async function test_globalRules() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.service.enableGlobalRules", true], + ], + }); + + info("Clear any preexisting rules"); + Services.cookieBanners.resetRules(false); + + is( + Services.cookieBanners.rules.length, + 0, + "Cookie banner service has no rules initially." + ); + + info("Insert a site-specific rule for example.com"); + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.id = genUUID(); + rule.domains = ["example.com"]; + rule.addCookie( + true, + "foo", + "new", + "example.com", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + rule.addClickRule( + "#cookieBannerExample", + false, + Ci.nsIClickRule.RUN_TOP, + "#btnOptOut", + "#btnOptIn" + ); + Services.cookieBanners.insertRule(rule); + + info( + "Insert a global rule with a cookie and a click rule. The cookie rule shouldn't be used." + ); + let ruleGlobalA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleGlobalA.id = genUUID(); + ruleGlobalA.domains = []; + ruleGlobalA.addCookie( + true, + "foo", + "new", + "example.net", + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + ruleGlobalA.addClickRule( + "#globalCookieBanner", + false, + Ci.nsIClickRule.RUN_TOP, + "#btnOptOut", + "#btnOptIn" + ); + Services.cookieBanners.insertRule(ruleGlobalA); + + info("Insert a second global rule"); + let ruleGlobalB = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleGlobalB.id = genUUID(); + ruleGlobalB.domains = []; + ruleGlobalB.addClickRule( + "#globalCookieBannerB", + false, + Ci.nsIClickRule.RUN_TOP, + "#btnOptOutB", + "#btnOptIn" + ); + Services.cookieBanners.insertRule(ruleGlobalB); + + is( + Services.cookieBanners.rules.length, + 3, + "Cookie Banner Service has three rules." + ); + + is( + Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://example.com"), + false + ).length, + 1, + "There should be a cookie rule for example.com" + ); + + is( + Services.cookieBanners.getClickRulesForDomain("example.com", true).length, + 1, + "There should be a a click rule for example.com" + ); + + is( + Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://thishasnorule.com"), + false + ).length, + 0, + "There should be no cookie rule for thishasnorule.com" + ); + + let clickRules = Services.cookieBanners.getClickRulesForDomain( + Services.io.newURI("http://thishasnorule.com"), + true + ); + is( + clickRules.length, + 2, + "There should be two click rules for thishasnorule.com" + ); + ok( + clickRules.every(rule => rule.presence.startsWith("#globalCookieBanner")), + "The returned click rules should be global rules." + ); + + info("Disabling global rules"); + await SpecialPowers.pushPrefEnv({ + set: [["cookiebanners.service.enableGlobalRules", false]], + }); + + is( + Services.cookieBanners.rules.length, + 1, + "Cookie Banner Service has 1 rule." + ); + + is( + Services.cookieBanners.rules[0].id, + rule.id, + "It should be the domain specific rule" + ); + + is( + Services.cookieBanners.getCookiesForURI( + Services.io.newURI("http://thishasnorule.com"), + false + ).length, + 0, + "There should be no cookie rule for thishasnorule.com" + ); + + is( + Services.cookieBanners.getClickRulesForDomain( + Services.io.newURI("http://thishasnorule.com"), + true + ).length, + 0, + "There should be no click rules for thishasnorule.com since global rules are disabled" + ); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_domainPrefs.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_domainPrefs.js new file mode 100644 index 0000000000..58610c6419 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_domainPrefs.js @@ -0,0 +1,403 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function() { + registerCleanupFunction(() => { + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); + if ( + Services.prefs.getIntPref("cookiebanners.service.mode") != + Ci.nsICookieBannerService.MODE_DISABLED || + Services.prefs.getIntPref("cookiebanners.service.mode.privateBrowsing") != + Ci.nsICookieBannerService.MODE_DISABLED + ) { + // Restore original rules. + Services.cookieBanners.resetRules(true); + } + }); +}); + +add_task(async function test_domain_preference() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let uri = Services.io.newURI("http://example.com"); + + // Check no site preference at the beginning + is( + Services.cookieBanners.getDomainPref(uri, false), + Ci.nsICookieBannerService.MODE_UNSET, + "There should be no per site preference at the beginning." + ); + + // Check setting and getting a site preference. + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT, + false + ); + + is( + Services.cookieBanners.getDomainPref(uri, false), + Ci.nsICookieBannerService.MODE_REJECT, + "Can get site preference for example.com with the correct value." + ); + + // Check site preference is shared between http and https. + let uriHttps = Services.io.newURI("https://example.com"); + is( + Services.cookieBanners.getDomainPref(uriHttps, false), + Ci.nsICookieBannerService.MODE_REJECT, + "Can get site preference for example.com in secure context." + ); + + // Check site preference in the other domain, example.org. + let uriOther = Services.io.newURI("https://example.org"); + is( + Services.cookieBanners.getDomainPref(uriOther, false), + Ci.nsICookieBannerService.MODE_UNSET, + "There should be no domain preference for example.org." + ); + + // Check setting site preference won't affect the other domain. + Services.cookieBanners.setDomainPref( + uriOther, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + false + ); + + is( + Services.cookieBanners.getDomainPref(uriOther, false), + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + "Can get domain preference for example.org with the correct value." + ); + is( + Services.cookieBanners.getDomainPref(uri, false), + Ci.nsICookieBannerService.MODE_REJECT, + "Can get site preference for example.com" + ); + + // Check nsICookieBannerService.setDomainPrefAndPersistInPrivateBrowsing(). + Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT + ); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + "Can get site preference for example.com" + ); + + // Check removing the site preference. + Services.cookieBanners.removeDomainPref(uri, false); + is( + Services.cookieBanners.getDomainPref(uri, false), + Ci.nsICookieBannerService.MODE_UNSET, + "There should be no site preference for example.com." + ); + + // Check remove all site preferences. + Services.cookieBanners.removeAllDomainPrefs(false); + is( + Services.cookieBanners.getDomainPref(uri, false), + Ci.nsICookieBannerService.MODE_UNSET, + "There should be no site preference for example.com." + ); + is( + Services.cookieBanners.getDomainPref(uriOther, false), + Ci.nsICookieBannerService.MODE_UNSET, + "There should be no site preference for example.org." + ); +}); + +add_task(async function test_domain_preference_dont_override_disable_pref() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Adding a domain preference for example.com"); + let uri = Services.io.newURI("https://example.com"); + + // Set a domain preference. + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT, + false + ); + + info("Disabling the cookie banner service."); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_DISABLED], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ], + }); + + info("Verifying if the cookie banner service is disabled."); + Assert.throws( + () => { + Services.cookieBanners.getDomainPref(uri, false); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for getDomainPref." + ); + + info("Enable the service again in order to clear the domain prefs."); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + Services.cookieBanners.removeAllDomainPrefs(false); +}); + +/** + * Test that domain preference is properly cleared when private browsing session + * ends. + */ +add_task(async function test_domain_preference_cleared_PBM_ends() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Adding a domain preference for example.com in PBM"); + let uri = Services.io.newURI("https://example.com"); + + info("Open a private browsing window."); + let PBMWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Set a domain preference for PBM. + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_DISABLED, + true + ); + + info("Verifying if the cookie banner domain pref is set for PBM."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_DISABLED, + "The domain pref is properly set for PBM." + ); + + info("Trigger an ending of a private browsing window session"); + let PBMSessionEndsObserved = TestUtils.topicObserved( + "last-pb-context-exited" + ); + + // Close the PBM window and wait until it finishes. + await BrowserTestUtils.closeWindow(PBMWin); + await PBMSessionEndsObserved; + + info("Verify if the private domain pref is cleared."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_UNSET, + "The domain pref is properly set for PBM." + ); +}); + +/** + * Test that the persistent domain preference won't be cleared when private + * browsing session ends. + */ +add_task(async function test_persistent_domain_preference_remain_PBM_ends() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Adding a domain preference for example.com in PBM"); + let uri = Services.io.newURI("https://example.com"); + + info("Open a private browsing window."); + let PBMWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Set a domain preference for PBM. + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_DISABLED, + true + ); + + info("Verifying if the cookie banner domain pref is set for PBM."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_DISABLED, + "The domain pref is properly set for PBM." + ); + + info("Adding a persistent domain preference for example.org in PBM"); + let uriPersistent = Services.io.newURI("https://example.org"); + + // Set a persistent domain preference for PBM. + Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing( + uriPersistent, + Ci.nsICookieBannerService.MODE_DISABLED + ); + + info("Trigger an ending of a private browsing window session"); + let PBMSessionEndsObserved = TestUtils.topicObserved( + "last-pb-context-exited" + ); + + // Close the PBM window and wait until it finishes. + await BrowserTestUtils.closeWindow(PBMWin); + await PBMSessionEndsObserved; + + info("Verify if the private domain pref is cleared."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_UNSET, + "The domain pref is properly set for PBM." + ); + + info("Verify if the persistent private domain pref remains."); + is( + Services.cookieBanners.getDomainPref(uriPersistent, true), + Ci.nsICookieBannerService.MODE_DISABLED, + "The persistent domain pref remains for PBM after private session ends." + ); +}); + +add_task(async function test_remove_persistent_domain_pref_in_PBM() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Adding a domain preference for example.com in PBM"); + let uri = Services.io.newURI("https://example.com"); + + info("Open a private browsing window."); + let PBMWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Set a persistent domain preference for PBM. + Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing( + uri, + Ci.nsICookieBannerService.MODE_DISABLED + ); + + info("Verifying if the cookie banner domain pref is set for PBM."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_DISABLED, + "The domain pref is properly set for PBM." + ); + + info("Remove the persistent domain pref."); + Services.cookieBanners.removeDomainPref(uri, true); + + info("Trigger an ending of a private browsing window session"); + let PBMSessionEndsObserved = TestUtils.topicObserved( + "last-pb-context-exited" + ); + + // Close the PBM window and wait until it finishes. + await BrowserTestUtils.closeWindow(PBMWin); + await PBMSessionEndsObserved; + + info( + "Verify if the private domain pref is no longer persistent and cleared." + ); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_UNSET, + "The domain pref is properly set for PBM." + ); +}); + +/** + * Test that the persistent state of a domain pref in PMB can be override by new + * call without persistent state. + */ +add_task(async function test_override_persistent_state_in_PBM() { + info("Enabling cookie banner service with MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Adding a domain preference for example.com in PBM"); + let uri = Services.io.newURI("https://example.com"); + + info("Open a private browsing window."); + let PBMWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Set a persistent domain preference for PBM. + Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing( + uri, + Ci.nsICookieBannerService.MODE_DISABLED + ); + + info("Trigger an ending of a private browsing window session"); + let PBMSessionEndsObserved = TestUtils.topicObserved( + "last-pb-context-exited" + ); + + // Close the PBM window and wait until it finishes. + await BrowserTestUtils.closeWindow(PBMWin); + await PBMSessionEndsObserved; + + info("Verify if the persistent private domain pref remains."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_DISABLED, + "The persistent domain pref remains for PBM after private session ends." + ); + + info("Open a private browsing window again."); + PBMWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Override the persistent domain pref with non-persistent domain pref."); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_DISABLED, + true + ); + + info("Trigger an ending of a private browsing window session again"); + PBMSessionEndsObserved = TestUtils.topicObserved("last-pb-context-exited"); + + // Close the PBM window and wait until it finishes. + await BrowserTestUtils.closeWindow(PBMWin); + await PBMSessionEndsObserved; + + info("Verify if the private domain pref is cleared."); + is( + Services.cookieBanners.getDomainPref(uri, true), + Ci.nsICookieBannerService.MODE_UNSET, + "The domain pref is properly set for PBM." + ); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_getRules.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_getRules.js new file mode 100644 index 0000000000..03807e9197 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_getRules.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let testRules = [ + // Cookie rule with multiple domains. + { + id: "0e4cdbb8-b688-47e0-9c8b-4db620398dbd", + click: {}, + cookies: { + optIn: [ + { + name: "foo", + value: "bar", + }, + ], + }, + domains: [TEST_DOMAIN_A, TEST_DOMAIN_B], + }, + // Click rule with single domain. + { + id: "0560e02c-a50f-4e7b-86e0-d6b7d258eb5f", + click: { + optOut: "#optOutBtn", + presence: "#cookieBanner", + }, + cookies: {}, + domains: [TEST_DOMAIN_C], + }, +]; + +add_setup(async function() { + // Enable the service and insert the test rules. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.listService.testSkipRemoteSettings", true], + ["cookiebanners.listService.testRules", JSON.stringify(testRules)], + ["cookiebanners.listService.logLevel", "Debug"], + ], + }); + + Services.cookieBanners.resetRules(true); +}); + +function ruleCountForDomain(domain) { + return Services.cookieBanners.rules.filter(rule => + rule.domains.includes(domain) + ).length; +} + +/** + * Tests that the rules getter does not return duplicate rules for rules with + * multiple domains. + */ +add_task(async function test_rules_getter_no_duplicates() { + // The rule import is async because it needs to fetch rules from + // RemoteSettings. Wait for the test rules to be applied. + // See CookieBannerListService#importAllRules. + await BrowserTestUtils.waitForCondition( + () => Services.cookieBanners.rules.length, + "Waiting for test rules to be imported." + ); + is( + Services.cookieBanners.rules.length, + 2, + "Rules getter should only return the two test rules." + ); + is( + ruleCountForDomain(TEST_DOMAIN_A), + 1, + "There should only be one rule with TEST_DOMAIN_A." + ); + is( + ruleCountForDomain(TEST_DOMAIN_B), + 1, + "There should only be one rule with TEST_DOMAIN_B." + ); + is( + ruleCountForDomain(TEST_DOMAIN_C), + 1, + "There should only be one rule with TEST_DOMAIN_C." + ); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_hasRuleForBCTree.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_hasRuleForBCTree.js new file mode 100644 index 0000000000..2b40df4a69 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_hasRuleForBCTree.js @@ -0,0 +1,214 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let testRules = [ + // Top-level cookie rule. + { + id: "87815b2d-a840-4155-8713-f8a26d1f483a", + click: {}, + cookies: { + optIn: [ + { + name: "foo", + value: "bar", + }, + ], + }, + domains: [TEST_DOMAIN_B], + }, + // Child click rule. + { + id: "d42bbaee-f96e-47e7-8e81-efc642518e97", + click: { + optOut: "#optOutBtn", + presence: "#cookieBanner", + runContext: "child", + }, + cookies: {}, + domains: [TEST_DOMAIN_C], + }, + // Top level click rule. + { + id: "19dd1f52-f3e6-4a24-a926-d77f553d1b15", + click: { + optOut: "#optOutBtn", + presence: "#cookieBanner", + }, + cookies: {}, + domains: [TEST_DOMAIN_A], + }, +]; + +/** + * Insert an iframe and wait for it to load. + * @param {BrowsingContext} parentBC - The BC the frame to insert under. + * @param {string} uri - The URI to load in the frame. + * @returns {Promise} - A Promise which resolves once the frame has loaded. + */ +function insertIframe(parentBC, uri) { + return SpecialPowers.spawn(parentBC, [uri], async testURL => { + let iframe = content.document.createElement("iframe"); + iframe.src = testURL; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + return iframe.browsingContext; + }); +} + +add_setup(async function() { + // Enable the service and insert the test rules. We only test + // MODE_REJECT_OR_ACCEPT here as the other modes are covered by other tests + // already and hasRuleForBrowsingContextTree mostly shares logic with other + // service getters. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.listService.testSkipRemoteSettings", true], + ["cookiebanners.listService.testRules", JSON.stringify(testRules)], + ], + }); + + // Ensure the test rules have been applied before the first test starts. + Services.cookieBanners.resetRules(); +}); + +add_task(async function test_unsupported() { + let unsupportedURIs = { + "about:preferences": /NS_ERROR_FAILURE/, + "about:blank": false, + }; + + for (let [key, value] of Object.entries(unsupportedURIs)) { + await BrowserTestUtils.withNewTab(key, async browser => { + if (typeof value == "object") { + // It's an error code. + Assert.throws( + () => { + Services.cookieBanners.hasRuleForBrowsingContextTree( + browser.browsingContext + ); + }, + value, + `Should throw ${value} for hasRuleForBrowsingContextTree call for '${key}'.` + ); + } else { + is( + Services.cookieBanners.hasRuleForBrowsingContextTree( + browser.browsingContext + ), + value, + `Should return expected value for hasRuleForBrowsingContextTree for '${key}'` + ); + } + }); + } +}); + +add_task(async function test_hasRuleForBCTree() { + info("Test with top level A"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN_A, async browser => { + let bcTop = browser.browsingContext; + + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have rule when called with top BC for A" + ); + + info("inserting frame with TEST_ORIGIN_A"); + let bcChildA = await insertIframe(bcTop, TEST_ORIGIN_A); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should still have rule when called with top BC for A." + ); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildA), + "Should not have rule when called with child BC for A, because A has no child click-rule." + ); + }); + + info("Test with top level C"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN_C, async browser => { + let bcTop = browser.browsingContext; + + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have no rule when called with top BC for C, because C only has a child click rule." + ); + + info("inserting frame with TEST_ORIGIN_C"); + let bcChildC = await insertIframe(bcTop, TEST_ORIGIN_C); + + info("inserting unrelated frames"); + await insertIframe(bcTop, "https://itisatracker.org"); + await insertIframe(bcChildC, "https://itisatracker.org"); + + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have rule when called with top BC for C, because frame C has a child click rule." + ); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildC), + "Should have rule when called with child BC for C, because it has a child click rule." + ); + }); + + info("Test with unrelated top level"); + await BrowserTestUtils.withNewTab("http://mochi.test:8888", async browser => { + let bcTop = browser.browsingContext; + + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should not have rule for unrelated site." + ); + + info("inserting frame with TEST_ORIGIN_A"); + let bcChildA = await insertIframe(bcTop, TEST_ORIGIN_A); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should still have no rule when called with top BC for A, because click rule for A only applies top-level." + ); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildA), + "Should have no rule when called with child BC for A." + ); + + info("inserting frame with TEST_ORIGIN_B"); + let bcChildB = await insertIframe(bcTop, TEST_ORIGIN_B); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should still have no rule when called with top BC for A, because cookie rule for B only applies top-level." + ); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildA), + "Should have no rule when called with child BC for A." + ); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildB), + "Should have no rule when called with child BC for B." + ); + + info("inserting nested frame with TEST_ORIGIN_C"); + let bcChildC = await insertIframe(bcChildB, TEST_ORIGIN_C); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have rule when called with top level BC because rule for nested iframe C applies." + ); + ok( + !Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildA), + "Should have no rule when called with child BC for A." + ); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildB), + "Should have rule when called with child BC for B, because C rule for nested iframe C applies." + ); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcChildC), + "Should have rule when called with child BC for C, because C rule for nested iframe C applies." + ); + }); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_prefs.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_prefs.js new file mode 100644 index 0000000000..e3e87b7551 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_prefs.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function() { + registerCleanupFunction(() => { + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); + if ( + Services.prefs.getIntPref("cookiebanners.service.mode") != + Ci.nsICookieBannerService.MODE_DISABLED || + Services.prefs.getIntPref("cookiebanners.service.mode.privateBrowsing") != + Ci.nsICookieBannerService.MODE_DISABLED + ) { + // Restore original rules. + Services.cookieBanners.resetRules(true); + } + }); +}); + +add_task(async function test_enabled_pref() { + info("Disabling cookie banner service."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_DISABLED], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ], + }); + + ok(Services.cookieBanners, "Services.cookieBanners is defined."); + ok( + Services.cookieBanners instanceof Ci.nsICookieBannerService, + "Services.cookieBanners is nsICookieBannerService" + ); + + info( + "Testing that methods throw NS_ERROR_NOT_AVAILABLE if the service is disabled." + ); + + Assert.throws( + () => { + Services.cookieBanners.rules; + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for rules getter." + ); + + // Create a test rule to attempt to insert. + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.id = genUUID(); + rule.domains = ["example.com"]; + + Assert.throws( + () => { + Services.cookieBanners.insertRule(rule); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for insertRule." + ); + + Assert.throws( + () => { + Services.cookieBanners.removeRule(rule); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for removeRule." + ); + + Assert.throws( + () => { + Services.cookieBanners.getCookiesForURI( + Services.io.newURI("https://example.com"), + false + ); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for rules getCookiesForURI." + ); + Assert.throws( + () => { + Services.cookieBanners.getClickRulesForDomain("example.com", true); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for rules getClickRuleForDomain." + ); + let uri = Services.io.newURI("https://example.com"); + Assert.throws( + () => { + Services.cookieBanners.getDomainPref(uri, false); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for getDomainPref." + ); + Assert.throws( + () => { + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT, + false + ); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for setDomainPref." + ); + Assert.throws( + () => { + Services.cookieBanners.setDomainPrefAndPersistInPrivateBrowsing( + uri, + Ci.nsICookieBannerService.MODE_REJECT + ); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for setDomainPrefAndPersistInPrivateBrowsing." + ); + Assert.throws( + () => { + Services.cookieBanners.removeDomainPref(uri, false); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for removeDomainPref." + ); + Assert.throws( + () => { + Services.cookieBanners.removeAllDomainPrefs(false); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should have thrown NS_ERROR_NOT_AVAILABLE for removeAllSitePref." + ); + + info("Enabling cookie banner service. MODE_REJECT"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + let rules = Services.cookieBanners.rules; + ok( + Array.isArray(rules), + "Rules getter should not throw but return an array." + ); + + info("Enabling cookie banner service. MODE_REJECT_OR_ACCEPT"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + rules = Services.cookieBanners.rules; + ok( + Array.isArray(rules), + "Rules getter should not throw but return an array." + ); + + info("Enabling cookie banner service. MODE_DETECT_ONLY"); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_DETECT_ONLY, + ], + ], + }); + + rules = Services.cookieBanners.rules; + ok( + Array.isArray(rules), + "Rules getter should not throw but return an array." + ); +}); + +/** + * Test both service mode pref combinations to ensure the cookie banner service + * is (un-)initialized correctly. + */ +add_task(async function test_enabled_pref_pbm_combinations() { + const MODES = [ + Ci.nsICookieBannerService.MODE_DISABLED, + Ci.nsICookieBannerService.MODE_REJECT, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + Ci.nsICookieBannerService.MODE_DETECT_ONLY, + ]; + + // Test all pref combinations + MODES.forEach(modeNormal => { + MODES.forEach(modePrivate => { + info( + `cookiebanners.service.mode=${modeNormal}; cookiebanners.service.mode.privateBrowsing=${modePrivate}` + ); + Services.prefs.setIntPref("cookiebanners.service.mode", modeNormal); + Services.prefs.setIntPref( + "cookiebanners.service.mode.privateBrowsing", + modePrivate + ); + + if ( + modeNormal == Ci.nsICookieBannerService.MODE_DISABLED && + modePrivate == Ci.nsICookieBannerService.MODE_DISABLED + ) { + Assert.throws( + () => { + Services.cookieBanners.rules; + }, + /NS_ERROR_NOT_AVAILABLE/, + "Cookie banner service should be disabled. Should throw NS_ERROR_NOT_AVAILABLE for rules getter." + ); + } else { + ok( + Services.cookieBanners.rules, + "Cookie banner service should be enabled, rules getter should not throw." + ); + } + }); + }); + + // Cleanup. + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookieinjector.js b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector.js new file mode 100644 index 0000000000..d3e92de1c2 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector.js @@ -0,0 +1,610 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); + +const DOMAIN_A = "example.com"; +const DOMAIN_B = "example.org"; +const DOMAIN_C = "example.net"; + +const ORIGIN_A = "https://" + DOMAIN_A; +const ORIGIN_A_SUB = `https://test1.${DOMAIN_A}`; +const ORIGIN_B = "https://" + DOMAIN_B; +const ORIGIN_C = "https://" + DOMAIN_C; + +const TEST_COOKIE_HEADER_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "testCookieHeader.sjs"; + +/** + * Tests that the test domains have no cookies set. + */ +function assertNoCookies() { + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_A), + "Should not set any cookies for ORIGIN_A" + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_C), + "Should not set any cookies for ORIGIN_C" + ); +} + +/** + * Loads a list of urls consecutively from the same tab. + * @param {string[]} urls - List of urls to load. + */ +async function visitTestSites(urls = [ORIGIN_A, ORIGIN_B, ORIGIN_C]) { + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + + for (let url of urls) { + await BrowserTestUtils.loadURI(tab.linkedBrowser, url); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + } + + BrowserTestUtils.removeTab(tab); +} + +add_setup(cookieInjectorTestSetup); + +/** + * Tests that no cookies are set if the cookie injection component is disabled + * by pref, but the cookie banner service is enabled. + */ +add_task(async function test_cookie_injector_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", false], + ], + }); + + insertTestCookieRules(); + + await visitTestSites(); + assertNoCookies(); + + await SiteDataTestUtils.clear(); +}); + +/** + * Tests that no cookies are set if the cookie injection component is enabled + * by pref, but the cookie banner service is disabled or in detect-only mode. + */ +add_task(async function test_cookie_banner_service_disabled() { + for (let mode of [ + Ci.nsICookieBannerService.MODE_DISABLED, + Ci.nsICookieBannerService.MODE_DETECT_ONLY, + ]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", mode], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + await visitTestSites(); + assertNoCookies(); + + await SiteDataTestUtils.clear(); + } +}); + +/** + * Tests that we don't inject cookies if there are no matching rules. + */ +add_task(async function test_no_rules() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + await visitTestSites(); + assertNoCookies(); + + await SiteDataTestUtils.clear(); +}); + +/** + * Tests that inject the correct cookies for matching rules and MODE_REJECT. + */ +add_task(async function test_mode_reject() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + await visitTestSites(); + + ok( + SiteDataTestUtils.hasCookies(ORIGIN_A, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_A" + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + // RULE_A also includes DOMAIN_C. + ok( + SiteDataTestUtils.hasCookies(ORIGIN_C, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_C" + ); + + await SiteDataTestUtils.clear(); +}); + +/** + * Tests that inject the correct cookies for matching rules and + * MODE_REJECT_OR_ACCEPT. + */ +add_task(async function test_mode_reject_or_accept() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + await visitTestSites(); + + ok( + SiteDataTestUtils.hasCookies(ORIGIN_A, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_A" + ); + ok( + SiteDataTestUtils.hasCookies(ORIGIN_B, [ + { + key: `cookieConsent_${DOMAIN_B}_1`, + value: "optIn1", + }, + ]), + "Should set opt-in cookies for ORIGIN_B" + ); + // Rule a also includes DOMAIN_C + ok( + SiteDataTestUtils.hasCookies(ORIGIN_C, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_C" + ); + + await SiteDataTestUtils.clear(); +}); + +/** + * Test that embedded third-parties do not trigger cookie injection. + */ +add_task(async function test_embedded_third_party() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + info("Loading example.com with an iframe for example.org."); + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await SpecialPowers.spawn(browser, [], async () => { + let iframe = content.document.createElement("iframe"); + iframe.src = "https://example.org"; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + }); + }); + + ok( + SiteDataTestUtils.hasCookies(ORIGIN_A, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for top-level ORIGIN_A" + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for embedded ORIGIN_B" + ); + + await SiteDataTestUtils.clear(); +}); + +/** + * Test that the injected cookies are present in the cookie header for the + * initial top level document request. + */ +add_task(async function test_cookie_header_and_document() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + insertTestCookieRules(); + + await BrowserTestUtils.withNewTab(TEST_COOKIE_HEADER_URL, async browser => { + await SpecialPowers.spawn(browser, [], async () => { + const EXPECTED_COOKIE_STR = + "cookieConsent_example.com_1=optOut1; cookieConsent_example.com_2=optOut2"; + is( + content.document.body.innerText, + EXPECTED_COOKIE_STR, + "Sent the correct cookie header." + ); + is( + content.document.cookie, + EXPECTED_COOKIE_STR, + "document.cookie has the correct cookie string." + ); + }); + }); + + await SiteDataTestUtils.clear(); +}); + +/** + * Test that cookies get properly injected for private browsing mode. + */ +add_task(async function test_pbm() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + insertTestCookieRules(); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let tab = BrowserTestUtils.addTab(pbmWindow.gBrowser, "about:blank"); + await BrowserTestUtils.loadURI(tab.linkedBrowser, ORIGIN_A); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok( + SiteDataTestUtils.hasCookies( + tab.linkedBrowser.contentPrincipal.origin, + [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ], + true + ), + "Should set opt-out cookies for top-level ORIGIN_A in private browsing." + ); + + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_A), + "Should not set any cookies for ORIGIN_A without PBM origin attribute." + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + + await BrowserTestUtils.closeWindow(pbmWindow); + await SiteDataTestUtils.clear(); +}); + +/** + * Test that cookies get properly injected for container tabs. + */ +add_task(async function test_container_tab() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + insertTestCookieRules(); + + info("Loading ORIGIN_B in a container tab."); + let tab = BrowserTestUtils.addTab(gBrowser, ORIGIN_B, { + userContextId: 1, + }); + await BrowserTestUtils.loadURI(tab.linkedBrowser, ORIGIN_B); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_A), + "Should not set any cookies for ORIGIN_A" + ); + ok( + SiteDataTestUtils.hasCookies(tab.linkedBrowser.contentPrincipal.origin, [ + { + key: `cookieConsent_${DOMAIN_B}_1`, + value: "optIn1", + }, + ]), + "Should set opt-out cookies for top-level ORIGIN_B in user context 1." + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B in default user context" + ); + + BrowserTestUtils.removeTab(tab); + await SiteDataTestUtils.clear(); +}); + +/** + * Test that if there is already a cookie with the given key, we don't overwrite + * it. If the rule sets the unsetValue field, this cookie may be overwritten if + * the value matches. + */ +add_task(async function test_no_overwrite() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + info("Pre-setting a cookie that should not be overwritten."); + SiteDataTestUtils.addToCookies({ + origin: ORIGIN_A, + host: `.${DOMAIN_A}`, + path: "/", + name: `cookieConsent_${DOMAIN_A}_1`, + value: "KEEPME", + }); + + info("Pre-setting a cookie that should be overwritten, based on its value"); + SiteDataTestUtils.addToCookies({ + origin: ORIGIN_B, + host: `.${DOMAIN_B}`, + path: "/", + name: `cookieConsent_${DOMAIN_B}_1`, + value: "UNSET", + }); + + await visitTestSites(); + + ok( + SiteDataTestUtils.hasCookies(ORIGIN_A, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "KEEPME", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should retain pre-set opt-in cookies for ORIGIN_A, but write new secondary cookie from rules." + ); + ok( + SiteDataTestUtils.hasCookies(ORIGIN_B, [ + { + key: `cookieConsent_${DOMAIN_B}_1`, + value: "optIn1", + }, + ]), + "Should have overwritten cookie for ORIGIN_B, based on its value and the unsetValue rule property." + ); + + await SiteDataTestUtils.clear(); +}); + +/** + * Tests that cookies are injected for the base domain when visiting a + * subdomain. + */ +add_task(async function test_subdomain() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + await visitTestSites([ORIGIN_A_SUB, ORIGIN_B]); + + ok( + SiteDataTestUtils.hasCookies(ORIGIN_A, [ + { + key: `cookieConsent_${DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_A when visiting subdomain." + ); + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_site_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + await visitTestSites(); + + ok( + !SiteDataTestUtils.hasCookies(ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + + info("Set the site preference of example.org to MODE_REJECT_OR_ACCEPT."); + let uri = Services.io.newURI(ORIGIN_B); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + false + ); + + await visitTestSites(); + ok( + SiteDataTestUtils.hasCookies(ORIGIN_B, [ + { + key: `cookieConsent_${DOMAIN_B}_1`, + value: "optIn1", + }, + ]), + "Should set opt-in cookies for ORIGIN_B" + ); + + Services.cookieBanners.removeAllDomainPrefs(false); + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_site_preference_pbm() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let tab = BrowserTestUtils.addTab(pbmWindow.gBrowser, "about:blank"); + await BrowserTestUtils.loadURI(tab.linkedBrowser, ORIGIN_B); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok( + !SiteDataTestUtils.hasCookies( + tab.linkedBrowser.contentPrincipal.origin, + null, + true + ), + "Should not set any cookies for ORIGIN_B in the private window" + ); + + info( + "Set the site preference of example.org to MODE_REJECT_OR_ACCEPT. in the private window." + ); + let uri = Services.io.newURI(ORIGIN_B); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + true + ); + + await BrowserTestUtils.loadURI(tab.linkedBrowser, ORIGIN_B); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + ok( + SiteDataTestUtils.hasCookies( + tab.linkedBrowser.contentPrincipal.origin, + [ + { + key: `cookieConsent_${DOMAIN_B}_1`, + value: "optIn1", + }, + ], + true + ), + "Should set opt-in cookies for ORIGIN_B" + ); + + Services.cookieBanners.removeAllDomainPrefs(true); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(pbmWindow); + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookieinjector_events.js b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector_events.js new file mode 100644 index 0000000000..50edb7a5d8 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector_events.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.import( + "resource://testing-common/SiteDataTestUtils.jsm" +); + +add_setup(cookieInjectorTestSetup); + +/** + * Tests that we dispatch cookiebannerhandled and cookiebannerdetected events + * for cookie injection. + */ +add_task(async function test_events() { + let tab; + + let triggerFn = async () => { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_ORIGIN_A); + }; + + await runEventTest({ + mode: Ci.nsICookieBannerService.MODE_REJECT, + initFn: insertTestCookieRules, + triggerFn, + testURL: `${TEST_ORIGIN_A}/`, + }); + + // Clean up the test tab opened by triggerFn. + BrowserTestUtils.removeTab(tab); + + ok( + SiteDataTestUtils.hasCookies(TEST_ORIGIN_A, [ + { + key: `cookieConsent_${TEST_DOMAIN_A}_1`, + value: "optOut1", + }, + { + key: `cookieConsent_${TEST_DOMAIN_A}_2`, + value: "optOut2", + }, + ]), + "Should set opt-out cookies for ORIGIN_A" + ); + ok( + !SiteDataTestUtils.hasCookies(TEST_ORIGIN_B), + "Should not set any cookies for ORIGIN_B" + ); + ok( + !SiteDataTestUtils.hasCookies(TEST_ORIGIN_C), + "Should not set any cookies for ORIGIN_C" + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cookiebanners/test/browser/file_banner.html b/toolkit/components/cookiebanners/test/browser/file_banner.html new file mode 100644 index 0000000000..145a21413e --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner.html @@ -0,0 +1,22 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + } + </script> +</head> +<body> + <h1>This is the top-level page</h1> + <div id="banner"> + <button id="optOut" onclick="clickOptOut()">OptOut</button> + <button id="optIn" onclick="clickOptIn()">OptIn</button> + </div> + <p id="result">NoClick</p> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/file_banner_b.html b/toolkit/components/cookiebanners/test/browser/file_banner_b.html new file mode 100644 index 0000000000..bf8df76bd3 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner_b.html @@ -0,0 +1,22 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + } + </script> +</head> +<body> + <h1>This is the top-level page</h1> + <div id="bannerB"> + <button id="optOut" onclick="clickOptOut()">OptOut</button> + <button id="optIn" onclick="clickOptIn()">OptIn</button> + </div> + <p id="result">NoClick</p> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/file_banner_invisible.html b/toolkit/components/cookiebanners/test/browser/file_banner_invisible.html new file mode 100644 index 0000000000..6d6a3a4fe6 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner_invisible.html @@ -0,0 +1,22 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + } + </script> +</head> +<body> + <h1>This is the top-level page</h1> + <div id="banner" style="display: none;"> + <button id="optOut" onclick="clickOptOut()">OptOut</button> + <button id="optIn" onclick="clickOptIn()">OptIn</button> + </div> + <p id="result">NoClick</p> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/file_delayed_banner.html b/toolkit/components/cookiebanners/test/browser/file_delayed_banner.html new file mode 100644 index 0000000000..870a94afc4 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_delayed_banner.html @@ -0,0 +1,48 @@ +<html> +<head> + <title>A top-level page with delayed cookie banner</title> + <script> + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + } + + function generateBanner() { + let banner = document.createElement("div"); + banner.id = "banner"; + + let buttonOptOut = document.createElement("button"); + buttonOptOut.id = "OptOut"; + buttonOptOut.onclick = () => {clickOptOut();}; + + let buttonOptIn = document.createElement("button"); + buttonOptIn.id = "OptIn"; + buttonOptIn.onclick = () => {clickOptIn();}; + + banner.appendChild(buttonOptOut); + banner.appendChild(buttonOptIn); + document.body.appendChild(banner); + } + + window.onload = () => { + const params = (new URL(document.location)).searchParams; + let delay = 0; + + if (params.has("delay")) { + delay = parseInt(params.get("delay")); + } + + window.setTimeout(() => { + generateBanner(); + }, delay); + }; + </script> +</head> +<body> + <h1>This is the top-level page</h1> + <p id="result">NoClick</p> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/file_delayed_banner_load.html b/toolkit/components/cookiebanners/test/browser/file_delayed_banner_load.html new file mode 100644 index 0000000000..011ea3ca97 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_delayed_banner_load.html @@ -0,0 +1,43 @@ +<html> +<head> + <title>A top-level page with delayed cookie banner</title> + <script> + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + } + + function generateBanner() { + let banner = document.createElement("div"); + banner.id = "banner"; + + let buttonOptOut = document.createElement("button"); + buttonOptOut.id = "OptOut"; + buttonOptOut.onclick = () => {clickOptOut();}; + + let buttonOptIn = document.createElement("button"); + buttonOptIn.id = "OptIn"; + buttonOptIn.onclick = () => {clickOptIn();}; + + banner.appendChild(buttonOptOut); + banner.appendChild(buttonOptIn); + document.body.appendChild(banner); + } + + window.onload = () => { + generateBanner(); + }; + </script> + <!-- This will cause DOMContentLoaded and load to be further apart which is + required for certain test cases. slowSubresource.sjs will delay the page load + event.--> + <link rel="stylesheet" href="slowSubresource.sjs"> +</head> +<body> + <h1>This is the top-level page</h1> + <p id="result">NoClick</p> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/file_iframe_banner.html b/toolkit/components/cookiebanners/test/browser/file_iframe_banner.html new file mode 100644 index 0000000000..8bf81a9222 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_iframe_banner.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>A top-level page with iframe cookie banner</title> +</head> +<body> +<iframe src="https://example.com/browser/toolkit/components/cookiebanners/test/browser/file_banner.html"></iframe> +</body> +</html> diff --git a/toolkit/components/cookiebanners/test/browser/head.js b/toolkit/components/cookiebanners/test/browser/head.js new file mode 100644 index 0000000000..b7759296b5 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/head.js @@ -0,0 +1,594 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_DOMAIN_A = "example.com"; +const TEST_DOMAIN_B = "example.org"; +const TEST_DOMAIN_C = "example.net"; + +const TEST_ORIGIN_A = "https://" + TEST_DOMAIN_A; +const TEST_ORIGIN_B = "https://" + TEST_DOMAIN_B; +const TEST_ORIGIN_C = "https://" + TEST_DOMAIN_C; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "" +); + +const TEST_PAGE_A = TEST_ORIGIN_A + TEST_PATH + "file_banner.html"; +const TEST_PAGE_B = TEST_ORIGIN_B + TEST_PATH + "file_banner.html"; +// Page C has a different banner element ID than A and B. +const TEST_PAGE_C = TEST_ORIGIN_C + TEST_PATH + "file_banner_b.html"; + +function genUUID() { + return Services.uuid.generateUUID().number.slice(1, -1); +} + +/** + * Common setup function for cookie banner handling tests. + */ +async function testSetup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable debug logging. + ["cookiebanners.listService.logLevel", "Debug"], + ], + }); + + // Reset GLEAN (FOG) telemetry to avoid data bleeding over from other tests. + Services.fog.testResetFOG(); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("cookiebanners.service.mode"); + Services.prefs.clearUserPref("cookiebanners.service.mode.privateBrowsing"); + if (Services.cookieBanners.isEnabled) { + // Restore original rules. + Services.cookieBanners.resetRules(true); + } + }); +} + +/** + * Setup function for click tests. + */ +async function clickTestSetup() { + await testSetup(); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable debug logging. + ["cookiebanners.bannerClicking.logLevel", "Debug"], + ["cookiebanners.bannerClicking.testing", true], + ["cookiebanners.bannerClicking.timeout", 500], + ["cookiebanners.bannerClicking.enabled", true], + ], + }); +} + +/** + * Setup function for cookie injector tests. + */ +async function cookieInjectorTestSetup() { + await testSetup(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.cookieInjector.enabled", true], + // Required to dispatch cookiebanner events. + ["cookiebanners.bannerClicking.enabled", true], + ], + }); +} + +/** + * A helper function returns a promise which resolves when the banner clicking + * is finished for the given domain. + * + * @param {String} domain the domain that should run the banner clicking. + */ +function promiseBannerClickingFinish(domain) { + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject, topic, data) { + if (data != domain) { + return; + } + + Services.obs.removeObserver( + observer, + "cookie-banner-test-clicking-finish" + ); + resolve(); + }, "cookie-banner-test-clicking-finish"); + }); +} + +/** + * A helper function to verify the banner state of the given browsingContext. + * + * @param {BrowsingContext} bc - the browsing context + * @param {boolean} visible - if the banner should be visible. + * @param {boolean} expected - the expected banner click state. + * @param {string} [bannerId] - id of the cookie banner element. + */ +async function verifyBannerState(bc, visible, expected, bannerId = "banner") { + info("Verify the cookie banner state."); + + await SpecialPowers.spawn( + bc, + [visible, expected, bannerId], + (visible, expected, bannerId) => { + let banner = content.document.getElementById(bannerId); + + is( + banner.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true, + }), + visible, + `The banner element should be ${visible ? "visible" : "hidden"}` + ); + + let result = content.document.getElementById("result"); + + is(result.textContent, expected, "The build click state is correct."); + } + ); +} + +/** + * A helper function to open the test page and verify the banner state. + * + * @param {Window} [win] - the chrome window object. + * @param {String} domain - the domain of the testing page. + * @param {String} testURL - the url of the testing page. + * @param {boolean} visible - if the banner should be visible. + * @param {boolean} expected - the expected banner click state. + * @param {string} [bannerId] - id of the cookie banner element. + * @param {boolean} [keepTabOpen] - whether to leave the tab open after the test + * function completed. + */ +async function openPageAndVerify({ + win = window, + domain, + testURL, + visible, + expected, + bannerId = "banner", + keepTabOpen = false, +}) { + info(`Opening ${testURL}`); + + let promise = promiseBannerClickingFinish(domain); + + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, testURL); + + await promise; + + await verifyBannerState(tab.linkedBrowser, visible, expected, bannerId); + + if (!keepTabOpen) { + BrowserTestUtils.removeTab(tab); + } +} + +/** + * A helper function to open the test page in an iframe and verify the banner + * state in the iframe. + * + * @param {Window} win - the chrome window object. + * @param {String} domain - the domain of the testing iframe page. + * @param {String} testURL - the url of the testing iframe page. + * @param {boolean} visible - if the banner should be visible. + * @param {boolean} expected - the expected banner click state. + */ +async function openIframeAndVerify({ + win, + domain, + testURL, + visible, + expected, +}) { + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_ORIGIN_C + ); + + let promise = promiseBannerClickingFinish(domain); + + let iframeBC = await SpecialPowers.spawn( + tab.linkedBrowser, + [testURL], + async testURL => { + let iframe = content.document.createElement("iframe"); + iframe.src = testURL; + content.document.body.appendChild(iframe); + await ContentTaskUtils.waitForEvent(iframe, "load"); + + return iframe.browsingContext; + } + ); + + await promise; + await verifyBannerState(iframeBC, visible, expected); + + BrowserTestUtils.removeTab(tab); +} + +/** + * A helper function to insert testing rules. + */ +function insertTestClickRules() { + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting test rules."); + + info("Add opt-out click rule for DOMAIN_A."); + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.id = genUUID(); + ruleA.domains = [TEST_DOMAIN_A]; + + ruleA.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_ALL, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleA); + + info("Add opt-in click rule for DOMAIN_B."); + let ruleB = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleB.id = genUUID(); + ruleB.domains = [TEST_DOMAIN_B]; + + ruleB.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_ALL, + null, + null, + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleB); + + info("Add global ruleC which targets a non-existing banner (presence)."); + let ruleC = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleC.id = genUUID(); + ruleC.domains = []; + ruleC.addClickRule( + "div#nonExistingBanner", + false, + Ci.nsIClickRule.RUN_ALL, + null, + null, + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleC); + + info("Add global ruleD which targets a non-existing banner (presence)."); + let ruleD = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleD.id = genUUID(); + ruleD.domains = []; + ruleD.addClickRule( + "div#nonExistingBanner2", + false, + Ci.nsIClickRule.RUN_ALL, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleD); +} + +/** + * Inserts cookie injection test rules for TEST_DOMAIN_A and TEST_DOMAIN_B. + */ +function insertTestCookieRules() { + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting test rules."); + + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.domains = [TEST_DOMAIN_A, TEST_DOMAIN_C]; + + Services.cookieBanners.insertRule(ruleA); + ruleA.addCookie( + true, + `cookieConsent_${TEST_DOMAIN_A}_1`, + "optOut1", + null, // empty host to fall back to .<domain> + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + ruleA.addCookie( + true, + `cookieConsent_${TEST_DOMAIN_A}_2`, + "optOut2", + null, + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + + // An opt-in cookie rule for DOMAIN_B. + let ruleB = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleB.domains = [TEST_DOMAIN_B]; + + Services.cookieBanners.insertRule(ruleB); + ruleB.addCookie( + false, + `cookieConsent_${TEST_DOMAIN_B}_1`, + "optIn1", + TEST_DOMAIN_B, + "/", + 3600, + "UNSET", + false, + false, + true, + 0, + 0 + ); +} + +/** + * Test the Glean.cookieBannersClick.result metric. + * @param {*} expected - Object mapping labels to counters. Omitted labels are + * asserted to be in initial state (undefined =^ 0) + * @param {boolean} [resetFOG] - Whether to reset all FOG telemetry after the + * method has finished. + */ +async function testClickResultTelemetry(expected, resetFOG = true) { + // TODO: Bug 1805653: Enable tests for Linux. + if (AppConstants.platform == "linux") { + ok(true, "Skip click telemetry tests on linux."); + return; + } + + // Ensure we have all data from the content process. + await Services.fog.testFlushAllChildren(); + + let labels = [ + "success", + "success_dom_content_loaded", + "success_mutation_pre_load", + "success_mutation_post_load", + "fail", + "fail_banner_not_found", + "fail_banner_not_visible", + "fail_button_not_found", + "fail_no_rule_for_mode", + "fail_actor_destroyed", + ]; + + let testMetricState = doAssert => { + for (let label of labels) { + if (doAssert) { + is( + Glean.cookieBannersClick.result[label].testGetValue(), + expected[label], + `Counter for label '${label}' has correct state.` + ); + } else if ( + Glean.cookieBannersClick.result[label].testGetValue() !== + expected[label] + ) { + return false; + } + } + + return true; + }; + + // Wait for the labeled counter to match the expected state. Returns greedy on + // mismatch. + try { + await TestUtils.waitForCondition( + testMetricState, + "Waiting for cookieBannersClick.result metric to match." + ); + } finally { + // Test again but this time with assertions and test all labels. + testMetricState(true); + + // Reset telemetry, even if the test condition above throws. This is to + // avoid failing subsequent tests in case of a test failure. + if (resetFOG) { + Services.fog.testResetFOG(); + } + } +} + +/** + * Triggers a cookie banner handling feature and tests the events dispatched. + * @param {*} options - Test options. + * @param {nsICookieBannerService::Modes} options.mode - The cookie banner + * service mode to test with. + * @param {function} options.initFn - Function to call for test initialization. + * @param {function} options.triggerFn - Function to call to trigger the banner + * handling feature. + * @param {string} options.testURL - URL where the test will trigger the banner + * handling feature. + * @returns {Promise} Resolves when the test completes, after cookie banner + * events. + */ +async function runEventTest({ mode, initFn, triggerFn, testURL }) { + await SpecialPowers.pushPrefEnv({ + set: [["cookiebanners.service.mode", mode]], + }); + + await initFn(); + + let expectEventDetected = mode != Ci.nsICookieBannerService.MODE_DISABLED; + let expectEventHandled = + mode == Ci.nsICookieBannerService.MODE_REJECT || + mode == Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT; + + let eventObservedDetected = false; + let eventObservedHandled = false; + + // This is a bit hacky, we use side effects caused by the checkFn we pass into + // waitForEvent to keep track of whether an event fired. + let promiseEventDetected = BrowserTestUtils.waitForEvent( + window, + "cookiebannerdetected", + false, + () => { + eventObservedDetected = true; + return true; + } + ); + let promiseEventHandled = BrowserTestUtils.waitForEvent( + window, + "cookiebannerhandled", + false, + () => { + eventObservedHandled = true; + return true; + } + ); + + // If we expect any events check which one comes first. + let firstEventPromise; + if (expectEventDetected || expectEventHandled) { + firstEventPromise = Promise.race([ + promiseEventHandled, + promiseEventDetected, + ]); + } + + await triggerFn(); + + let eventDetected; + if (expectEventDetected) { + eventDetected = await promiseEventDetected; + + is( + eventDetected.type, + "cookiebannerdetected", + "Should dispatch cookiebannerdetected event." + ); + } + + let eventHandled; + if (expectEventHandled) { + eventHandled = await promiseEventHandled; + is( + eventHandled.type, + "cookiebannerhandled", + "Should dispatch cookiebannerhandled event." + ); + } + + // For MODE_DISABLED this array will be empty, because we don't expect any + // events to be dispatched. + let eventsToTest = [eventDetected, eventHandled].filter(event => !!event); + + for (let event of eventsToTest) { + info(`Testing properties of event ${event.type}`); + + let { windowContext } = event.detail; + ok( + windowContext, + `Event ${event.type} detail should contain a WindowContext` + ); + + let browser = windowContext.browsingContext.top.embedderElement; + ok( + browser, + "WindowContext should have an associated top embedder element." + ); + is( + browser.tagName, + "browser", + "The top embedder element should be a browser" + ); + let chromeWin = browser.ownerGlobal; + is( + chromeWin, + window, + "The chrome window associated with the browser should match the window where the cookie banner was handled." + ); + is( + chromeWin.gBrowser.selectedBrowser, + browser, + "The browser associated with the event should be the selected browser." + ); + is( + browser.currentURI.spec, + testURL, + "The browser's URI spec should match the cookie banner test page." + ); + } + + let firstEvent = await firstEventPromise; + is( + expectEventDetected || expectEventHandled, + !!firstEvent, + "Should have observed the first event if banner clicking is enabled." + ); + + if (expectEventDetected || expectEventHandled) { + is( + firstEvent.type, + "cookiebannerdetected", + "Detected event should be dispatched first" + ); + } + + is( + eventObservedDetected, + expectEventDetected, + `Should ${ + expectEventDetected ? "" : "not " + }have observed 'cookiebannerdetected' event for mode ${mode}` + ); + is( + eventObservedHandled, + expectEventHandled, + `Should ${ + expectEventHandled ? "" : "not " + }have observed 'cookiebannerhandled' event for mode ${mode}` + ); + + // Clean up pending promises by dispatching artificial cookiebanner events. + // Otherwise the test fails because of pending event listeners which + // BrowserTestUtils.waitForEvent registered. + for (let eventType of ["cookiebannerdetected", "cookiebannerhandled"]) { + let event = new CustomEvent(eventType, { + bubbles: true, + cancelable: false, + }); + window.windowUtils.dispatchEventToChromeOnly(window, event); + } + + await promiseEventDetected; + await promiseEventHandled; +} diff --git a/toolkit/components/cookiebanners/test/browser/slowSubresource.sjs b/toolkit/components/cookiebanners/test/browser/slowSubresource.sjs new file mode 100644 index 0000000000..1f14c2e9cf --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/slowSubresource.sjs @@ -0,0 +1,18 @@ +"use strict"; + +let timer; + +const DELAY_MS = 5000; +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/css ", false); + response.write("body { background-color: red; }"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/toolkit/components/cookiebanners/test/browser/testCookieHeader.sjs b/toolkit/components/cookiebanners/test/browser/testCookieHeader.sjs new file mode 100644 index 0000000000..2d7cac5e45 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/testCookieHeader.sjs @@ -0,0 +1,5 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200); + // Write the cookie header value to the body for the test to inspect. + response.write(request.getHeader("Cookie")); +} diff --git a/toolkit/components/cookiebanners/test/unit/test_cookiebannerlistservice.js b/toolkit/components/cookiebanners/test/unit/test_cookiebannerlistservice.js new file mode 100644 index 0000000000..b954fa4128 --- /dev/null +++ b/toolkit/components/cookiebanners/test/unit/test_cookiebannerlistservice.js @@ -0,0 +1,604 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.import( + "resource://services-settings/remote-settings.js" +); + +let { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +// Name of the RemoteSettings collection containing the rules. +const COLLECTION_NAME = "cookie-banner-rules-list"; + +// Name of pref used to import test rules. +const PREF_TEST_RULES = "cookiebanners.listService.testRules"; + +let rulesInserted = []; +let rulesRemoved = []; +let insertCallback = null; + +function genUUID() { + return Services.uuid.generateUUID().number.slice(1, -1); +} + +const RULE_A_ORIGINAL = { + id: genUUID(), + click: { + optOut: "#fooOut", + presence: "#foobar", + }, + domains: ["example.com"], + cookies: { + optOut: [ + { + name: "doOptOut", + value: "true", + isSecure: true, + isSession: false, + }, + ], + }, +}; + +const RULE_B = { + id: genUUID(), + click: { + optOut: "#fooOutB", + presence: "#foobarB", + }, + domains: ["example.org"], + cookies: { + optOut: [ + { + name: "doOptOutB", + value: "true", + isSecure: true, + }, + ], + }, +}; + +const RULE_C = { + id: genUUID(), + click: { + optOut: "#fooOutC", + presence: "#foobarC", + }, + domains: ["example.net"], + cookies: { + optIn: [ + { + name: "gdpr", + value: "1", + path: "/myPath", + host: "foo.example.net", + }, + ], + }, +}; + +const RULE_A_UPDATED = { + id: RULE_A_ORIGINAL.id, + click: { + optOut: "#fooOut", + optIn: "#barIn", + presence: "#foobar", + }, + domains: ["example.com"], + cookies: { + optOut: [ + { + name: "doOptOutUpdated", + value: "true", + isSecure: true, + }, + { + name: "hideBanner", + value: "1", + }, + ], + }, +}; + +const INVALID_RULE_CLICK = { + id: genUUID(), + domains: ["foobar.com"], + click: { + presence: 1, + optIn: "#foo", + }, +}; + +const INVALID_RULE_EMPTY = {}; + +const RULE_D_GLOBAL = { + id: genUUID(), + click: { + optOut: "#globalOptOutD", + presence: "#globalBannerD", + }, + domains: [], + cookies: {}, +}; + +const RULE_E_GLOBAL = { + id: genUUID(), + click: { + optOut: "#globalOptOutE", + presence: "#globalBannerE", + }, + domains: [], + cookies: {}, +}; + +const RULE_F_EMPTY_CLICK = { + id: genUUID(), + click: {}, + domains: ["example.com"], + cookies: { + optOut: [ + { + name: "doOptOut", + value: "true", + isSecure: true, + }, + { + name: "hideBanner", + value: "1", + }, + ], + }, +}; + +// Testing with RemoteSettings requires a profile. +do_get_profile(); + +add_setup(async () => { + // Enable debug logging. + Services.prefs.setStringPref("cookiebanners.listService.logLevel", "Debug"); + + // Stub some nsICookieBannerService methods for easy testing. + let removeRule = sinon.stub().callsFake(rule => { + rulesRemoved.push(rule); + }); + + let insertRule = sinon.stub().callsFake(rule => { + rulesInserted.push(rule); + insertCallback?.(); + }); + + let oldCookieBanners = Services.cookieBanners; + Services.cookieBanners = { + isEnabled: true, + insertRule, + removeRule, + resetRules() {}, + resetDomainTelemetryRecord() {}, + }; + + // Remove stubs on test end. + registerCleanupFunction(() => { + Services.cookieBanners = oldCookieBanners; + Services.prefs.clearUserPref("cookiebanners.listService.logLevel"); + }); +}); + +/** + * Promise wrapper to listen for Services.cookieBanners.insertRule calls from + * the CookieBannerListService. + * @param {function} checkFn - Function which returns true or false to indicate + * if this is the insert the caller is looking for. + * @returns {Promise} - Promise which resolves when checkFn matches after + * insertRule call. + */ +function waitForInsert(checkFn) { + return new Promise(resolve => { + insertCallback = () => { + if (checkFn()) { + insertCallback = null; + resolve(); + } + }; + }); +} + +/** + * Tests that the cookie banner list service imports all rules on init. + */ +add_task(async function test_initial_import() { + info("Initializing RemoteSettings collection " + COLLECTION_NAME); + + let db = RemoteSettings(COLLECTION_NAME).db; + db.clear(); + await db.create(RULE_A_ORIGINAL); + await db.create(RULE_C); + await db.importChanges({}, Date.now()); + + Assert.equal(rulesInserted.length, 0, "No inserted rules initially."); + Assert.equal(rulesRemoved.length, 0, "No removed rules initially."); + + let insertPromise = waitForInsert(() => rulesInserted.length >= 2); + + info( + "Initializing the cookie banner list service which triggers a collection get call" + ); + let cookieBannerListService = Cc[ + "@mozilla.org/cookie-banner-list-service;1" + ].getService(Ci.nsICookieBannerListService); + await cookieBannerListService.initForTest(); + + await insertPromise; + + Assert.equal(rulesInserted.length, 2, "Two inserted rules after init."); + Assert.equal(rulesRemoved.length, 0, "No removed rules after init."); + + let ruleA = rulesInserted.find(rule => rule.id == RULE_A_UPDATED.id); + let cookieRuleA = ruleA.cookiesOptOut[0].cookie; + let ruleC = rulesInserted.find(rule => rule.id == RULE_C.id); + let cookieRuleC = ruleC.cookiesOptIn[0].cookie; + + Assert.ok(ruleA, "Has rule A."); + Assert.deepEqual( + ruleA.domains, + RULE_A_UPDATED.domains, + "Domains for ruleA should match." + ); + // Test the defaults which CookieBannerListService sets when the rule does + // not. + Assert.equal( + cookieRuleA.isSession, + false, + "Cookie for rule A should not be a session cookie." + ); + Assert.equal( + cookieRuleA.host, + null, + "Cookie host for rule A should be default." + ); + Assert.equal( + cookieRuleA.path, + "/", + "Cookie path for rule A should be default." + ); + + Assert.ok(ruleC, "Has rule C."); + Assert.deepEqual( + ruleC.domains, + RULE_C.domains, + "Domains for ruleA should match." + ); + Assert.equal( + ruleC.cookiesOptIn[0].cookie.isSession, + true, + "Cookie for rule C should should be a session cookie." + ); + Assert.equal( + cookieRuleC.host, + "foo.example.net", + "Cookie host for rule C should be custom." + ); + Assert.equal( + cookieRuleC.path, + "/myPath", + "Cookie path for rule C should be custom." + ); + + // Cleanup + cookieBannerListService.shutdown(); + rulesInserted = []; + rulesRemoved = []; + + db.clear(); + await db.importChanges({}, Date.now()); +}); + +/** + * Tests that the cookie banner list service updates rules on sync. + */ +add_task(async function test_remotesettings_sync() { + // Initialize the cookie banner list service so it subscribes to + // RemoteSettings updates. + let cookieBannerListService = Cc[ + "@mozilla.org/cookie-banner-list-service;1" + ].getService(Ci.nsICookieBannerListService); + await cookieBannerListService.initForTest(); + + const payload = { + current: [RULE_A_ORIGINAL, RULE_C, RULE_D_GLOBAL], + created: [RULE_B, RULE_E_GLOBAL], + updated: [{ old: RULE_A_ORIGINAL, new: RULE_A_UPDATED }], + deleted: [RULE_C, RULE_D_GLOBAL], + }; + + Assert.equal(rulesInserted.length, 0, "No inserted rules initially."); + Assert.equal(rulesRemoved.length, 0, "No removed rules initially."); + + info("Dispatching artificial RemoteSettings sync event"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { data: payload }); + + Assert.equal(rulesInserted.length, 3, "Three inserted rules after sync."); + Assert.equal(rulesRemoved.length, 2, "Two removed rules after sync."); + + let ruleA = rulesInserted.find(rule => rule.id == RULE_A_UPDATED.id); + let ruleB = rulesInserted.find(rule => rule.id == RULE_B.id); + let ruleE = rulesInserted.find(rule => rule.id == RULE_E_GLOBAL.id); + let ruleC = rulesRemoved[0]; + + info("Testing that service inserted updated version of RULE_A."); + Assert.deepEqual( + ruleA.domains, + RULE_A_UPDATED.domains, + "Domains should match RULE_A." + ); + Assert.equal( + ruleA.cookiesOptOut.length, + RULE_A_UPDATED.cookies.optOut.length, + "cookiesOptOut length should match RULE_A." + ); + + info("Testing opt-out cookies of RULE_A"); + for (let i = 0; i < RULE_A_UPDATED.cookies.optOut.length; i += 1) { + Assert.equal( + ruleA.cookiesOptOut[i].cookie.name, + RULE_A_UPDATED.cookies.optOut[i].name, + "cookiesOptOut cookie name should match RULE_A." + ); + Assert.equal( + ruleA.cookiesOptOut[i].cookie.value, + RULE_A_UPDATED.cookies.optOut[i].value, + "cookiesOptOut cookie value should match RULE_A." + ); + } + + Assert.equal(ruleB.id, RULE_B.id, "Should have inserted RULE_B"); + Assert.equal(ruleC.id, RULE_C.id, "Should have removed RULE_C"); + Assert.equal( + ruleE.id, + RULE_E_GLOBAL.id, + "Should have inserted RULE_E_GLOBAL" + ); + + // Cleanup + cookieBannerListService.shutdown(); + rulesInserted = []; + rulesRemoved = []; + + let { db } = RemoteSettings(COLLECTION_NAME); + db.clear(); + await db.importChanges({}, Date.now()); +}); + +/** + * Tests the cookie banner rule test pref. + */ +add_task(async function test_rule_test_pref() { + Services.prefs.setStringPref( + PREF_TEST_RULES, + JSON.stringify([RULE_A_ORIGINAL, RULE_B]) + ); + + Assert.equal(rulesInserted.length, 0, "No inserted rules initially."); + Assert.equal(rulesRemoved.length, 0, "No removed rules initially."); + + let insertPromise = waitForInsert(() => rulesInserted.length >= 2); + + // Initialize the cookie banner list service so it imports test rules and listens for pref changes. + let cookieBannerListService = Cc[ + "@mozilla.org/cookie-banner-list-service;1" + ].getService(Ci.nsICookieBannerListService); + await cookieBannerListService.initForTest(); + + info("Wait for rules to be inserted"); + await insertPromise; + + Assert.equal(rulesInserted.length, 2, "Should have inserted two rules."); + Assert.equal(rulesRemoved.length, 0, "Should not have removed any rules."); + + Assert.ok( + rulesInserted.some(rule => rule.id == RULE_A_ORIGINAL.id), + "Should have inserted RULE_A" + ); + Assert.ok( + rulesInserted.some(rule => rule.id == RULE_B.id), + "Should have inserted RULE_B" + ); + + rulesInserted = []; + rulesRemoved = []; + + let insertPromise2 = waitForInsert(() => rulesInserted.length >= 3); + + info( + "Updating test rules via pref. The list service should detect the pref change." + ); + // This includes some invalid rules, they should be skipped. + Services.prefs.setStringPref( + PREF_TEST_RULES, + JSON.stringify([ + RULE_A_ORIGINAL, + RULE_B, + INVALID_RULE_EMPTY, + RULE_C, + INVALID_RULE_CLICK, + ]) + ); + + info("Wait for rules to be inserted"); + await insertPromise2; + + Assert.equal(rulesInserted.length, 3, "Should have inserted three rules."); + Assert.equal(rulesRemoved.length, 0, "Should not have removed any rules."); + + Assert.ok( + rulesInserted.some(rule => rule.id == RULE_A_ORIGINAL.id), + "Should have inserted RULE_A" + ); + Assert.ok( + rulesInserted.some(rule => rule.id == RULE_B.id), + "Should have inserted RULE_B" + ); + Assert.ok( + rulesInserted.some(rule => rule.id == RULE_C.id), + "Should have inserted RULE_C" + ); + + // Cleanup + cookieBannerListService.shutdown(); + rulesInserted = []; + rulesRemoved = []; + + Services.prefs.clearUserPref(PREF_TEST_RULES); +}); + +/** + * Tests that runContext string values get properly translated into nsIClickRule::RunContext. + */ +add_task(async function test_runContext_conversion() { + info("Initializing RemoteSettings collection " + COLLECTION_NAME); + + let ruleA = { + id: genUUID(), + click: { + presence: "#foobar", + runContext: "child", + }, + domains: ["a.com"], + }; + let ruleB = { + id: genUUID(), + click: { + presence: "#foobar", + }, + domains: ["b.com"], + }; + let ruleC = { + id: genUUID(), + click: { + presence: "#foobar", + runContext: "all", + }, + domains: ["c.com"], + }; + let ruleD = { + id: genUUID(), + click: { + presence: "#foobar", + runContext: "top", + }, + domains: ["d.com"], + }; + let ruleE = { + id: genUUID(), + click: { + presence: "#foobar", + runContext: "thisIsNotValid", + }, + domains: ["e.com"], + }; + + let db = RemoteSettings(COLLECTION_NAME).db; + db.clear(); + await db.create(ruleA); + await db.create(ruleB); + await db.create(ruleC); + await db.create(ruleD); + await db.create(ruleE); + await db.importChanges({}, Date.now()); + + let insertPromise = waitForInsert(() => rulesInserted.length >= 4); + + info( + "Initializing the cookie banner list service which triggers a collection get call" + ); + let cookieBannerListService = Cc[ + "@mozilla.org/cookie-banner-list-service;1" + ].getService(Ci.nsICookieBannerListService); + await cookieBannerListService.initForTest(); + + await insertPromise; + + let resultA = rulesInserted.find(rule => rule.domains.includes("a.com")) + .clickRule; + let resultB = rulesInserted.find(rule => rule.domains.includes("b.com")) + .clickRule; + let resultC = rulesInserted.find(rule => rule.domains.includes("c.com")) + .clickRule; + let resultD = rulesInserted.find(rule => rule.domains.includes("d.com")) + .clickRule; + let resultE = rulesInserted.find(rule => rule.domains.includes("e.com")) + .clickRule; + + Assert.equal( + resultA.runContext, + Ci.nsIClickRule.RUN_CHILD, + "Rule A should have been imported with the correct runContext" + ); + Assert.equal( + resultB.runContext, + Ci.nsIClickRule.RUN_TOP, + "Rule B should have fallen back to default runContext" + ); + Assert.equal( + resultC.runContext, + Ci.nsIClickRule.RUN_ALL, + "Rule C should have been imported with the correct runContext" + ); + Assert.equal( + resultD.runContext, + Ci.nsIClickRule.RUN_TOP, + "Rule D should have been imported with the correct runContext" + ); + Assert.equal( + resultE.runContext, + Ci.nsIClickRule.RUN_TOP, + "Rule E should have fallen back to default runContext" + ); + + // Cleanup + cookieBannerListService.shutdown(); + rulesInserted = []; + rulesRemoved = []; + + db.clear(); + await db.importChanges({}, Date.now()); +}); + +/** + * Tests empty click rules don't get imported. + */ +add_task(async function test_empty_click_rule() { + info("Initializing RemoteSettings collection " + COLLECTION_NAME); + + let db = RemoteSettings(COLLECTION_NAME).db; + db.clear(); + await db.create(RULE_F_EMPTY_CLICK); + await db.importChanges({}, Date.now()); + + let insertPromise = waitForInsert(() => rulesInserted.length >= 1); + + info( + "Initializing the cookie banner list service which triggers a collection get call" + ); + let cookieBannerListService = Cc[ + "@mozilla.org/cookie-banner-list-service;1" + ].getService(Ci.nsICookieBannerListService); + await cookieBannerListService.initForTest(); + + await insertPromise; + + let ruleF = rulesInserted.find(rule => rule.id == RULE_F_EMPTY_CLICK.id); + + Assert.ok(ruleF, "Has rule F."); + Assert.ok(ruleF.cookiesOptOut?.length, "Should have imported a cookie rule."); + Assert.equal(ruleF.clickRule, null, "Should not have imported a click rule."); + + // Cleanup + cookieBannerListService.shutdown(); + rulesInserted = []; + rulesRemoved = []; + + db.clear(); + await db.importChanges({}, Date.now()); +}); diff --git a/toolkit/components/cookiebanners/test/unit/xpcshell.ini b/toolkit/components/cookiebanners/test/unit/xpcshell.ini new file mode 100644 index 0000000000..b343ee6c12 --- /dev/null +++ b/toolkit/components/cookiebanners/test/unit/xpcshell.ini @@ -0,0 +1,2 @@ +[test_cookiebannerlistservice.js] + |