diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/cookiebanners/test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cookiebanners/test')
32 files changed, 7000 insertions, 0 deletions
diff --git a/toolkit/components/cookiebanners/test/browser/browser.toml b/toolkit/components/cookiebanners/test/browser/browser.toml new file mode 100644 index 0000000000..69728540d9 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser.toml @@ -0,0 +1,62 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_bannerClicking.js"] +support-files = [ + "file_banner.html", + "file_banner_b.html", + "file_delayed_banner.html", +] + +["browser_bannerClicking_domainPref.js"] + +["browser_bannerClicking_events.js"] +support-files = ["file_banner.html"] + +["browser_bannerClicking_globalRules.js"] + +["browser_bannerClicking_globalRules_telemetry.js"] + +["browser_bannerClicking_runContext.js"] +support-files = ["file_banner.html"] + +["browser_bannerClicking_slowLoad.js"] +support-files = [ + "file_delayed_banner_load.html", + "slowSubresource.sjs", +] + +["browser_bannerClicking_stopExecuteAfterSeveralAttempt.js"] + +["browser_bannerClicking_telemetry_querySelector.js"] +support-files = [ + "file_banner.html", +] + +["browser_bannerClicking_visibilityOverride.js"] +support-files = ["file_banner_invisible.html"] + +["browser_cookiebanner_telemetry.js"] +support-files = ["file_iframe_banner.html"] + +["browser_cookiebanner_webconsole.js"] +support-files = [ + "file_banner.html", +] + +["browser_cookiebannerservice.js"] + +["browser_cookiebannerservice_domainPrefs.js"] + +["browser_cookiebannerservice_getRules.js"] + +["browser_cookiebannerservice_hasRuleForBCTree.js"] + +["browser_cookiebannerservice_prefs.js"] + +["browser_cookieinjector.js"] +support-files = ["testCookieHeader.sjs"] + +["browser_cookieinjector_events.js"] + +["browser_cookieinjector_telemetry.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..4353505ab2 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking.js @@ -0,0 +1,485 @@ +/* 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 [serviceMode, detectOnly] of [ + [Ci.nsICookieBannerService.MODE_DISABLED, false], + [Ci.nsICookieBannerService.MODE_DISABLED, true], + [Ci.nsICookieBannerService.MODE_REJECT, true], + [Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, true], + ]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", serviceMode], + ["cookiebanners.service.detectOnly", detectOnly], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ], + }); + + // Clear the executed records before testing. + if (serviceMode != Ci.nsICookieBannerService.MODE_DISABLED) { + Services.cookieBanners.removeAllExecutedRecords(false); + } + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + expectActorEnabled: + serviceMode != Ci.nsICookieBannerService.MODE_DISABLED, + }); + + await SpecialPowers.popPrefEnv(); + } +}); + +/** + * 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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. + await 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(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, + ], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(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], + ["cookiebanners.bannerClicking.timeoutAfterLoad", 10000], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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, + ], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(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], + ], + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + await testClickResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + Services.cookieBanners.removeAllExecutedRecords(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, + ], + ], + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(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, + ], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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..01986eb36b --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_domainPref.js @@ -0,0 +1,368 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +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(); + + // Clear executed records before testing for private and normal browsing. + Services.cookieBanners.removeAllExecutedRecords(true); + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(testPBM); + + 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(); + + // Clear executed records before testing for private and normal browsing. + Services.cookieBanners.removeAllExecutedRecords(true); + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 + ); + + Services.cookieBanners.removeAllExecutedRecords(testPBM); + + 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, + }); + } +}); + +// Verify that the ForgetAboutSite clears the domain preference properly. +add_task(async function test_domain_preference_forgetAboutSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + // Clear executed records before testing for private and normal browsing. + Services.cookieBanners.removeAllExecutedRecords(true); + Services.cookieBanners.removeAllExecutedRecords(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, + false + ); + + info( + "Verify if domain preference takes precedence over then the pref setting for example.org" + ); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + // Call ForgetAboutSite for the domain. + await ForgetAboutSite.removeDataFromDomain(TEST_DOMAIN_B); + + info("Ensure the domain preference is cleared."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); +}); + +add_task(async function test_domain_preference_clearDataService() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + insertTestClickRules(); + + // Clear executed records before testing or private and normal browsing. + Services.cookieBanners.removeAllExecutedRecords(true); + Services.cookieBanners.removeAllExecutedRecords(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, + false + ); + + info( + "Verify if domain preference takes precedence over then the pref setting for example.org" + ); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptIn", + }); + + info("Call ClearDataService.deleteDataFromBaseDomain for the domain."); + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + TEST_DOMAIN_B, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION, + aResolve + ); + }); + + info("Ensure the domain preference is cleared."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + info("Set the domain preference of example.org to MODE_REJECT_OR_ACCEPT"); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + false + ); + + info("Call ClearDataService.deleteDataFromHost for the domain."); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + TEST_DOMAIN_B, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION, + aResolve + ); + }); + + info("Ensure the domain preference is cleared."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + info("Set the domain preference of example.org to MODE_REJECT_OR_ACCEPT"); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + false + ); + + info("Call ClearDataService.deleteDataFromPrincipal for the domain."); + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://" + TEST_DOMAIN_B + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION, + aResolve + ); + }); + + info("Ensure the domain preference is cleared."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + + info("Set the domain preference of example.org to MODE_REJECT_OR_ACCEPT"); + Services.cookieBanners.setDomainPref( + uri, + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + false + ); + + info("Call ClearDataService.deleteData for the domain."); + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION, + aResolve + ); + }); + + info("Ensure the domain preference is cleared."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); +}); 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..de9a464446 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_events.js @@ -0,0 +1,98 @@ +/* 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 {boolean} options.detectOnly - Whether the service should be enabled + * in detection only mode, where it does not handle banners. + * @param {*} options.openPageOptions - Options to overwrite for the openPageAndVerify call. + */ +async function runTest({ mode, detectOnly = false, openPageOptions = {} }) { + if (mode == null) { + throw new Error("Invalid cookie banner service mode."); + } + let initFn = () => { + // Insert rules only if the feature is enabled. + if (Services.cookieBanners.isEnabled) { + insertTestClickRules(); + + // Clear executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + } + }; + + let shouldHandleBanner = + mode == Ci.nsICookieBannerService.MODE_REJECT && !detectOnly; + let expectActorEnabled = mode != Ci.nsICookieBannerService.MODE_DISABLED; + 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, + expectActorEnabled, + ...openPageOptions, // Allow test callers to override any options for this method. + }); + }; + + await runEventTest({ mode, detectOnly, 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 detect-only mode. + */ +add_task(async function test_events_mode_detect_only() { + await runTest({ + mode: Ci.nsICookieBannerService.MODE_REJECT, + detectOnly: true, + }); + await runTest({ + mode: Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + detectOnly: true, + }); +}); + +/** + * Test the banner clicking events with detect-only mode 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_REJECT_OR_ACCEPT, + detectOnly: true, + 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 in disabled mode. + */ +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..a23194be95 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules.js @@ -0,0 +1,234 @@ +/* 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 testCMPResultTelemetry({}); + + 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 testCMPResultTelemetry( + { + success: 1, + success_dom_content_loaded: 1, + }, + false + ); + + Services.cookieBanners.removeAllExecutedRecords(false); + + await openPageAndVerify({ + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: false, + expected: "OptOut", + }); + + await testCMPResultTelemetry( + { + success: 2, + success_dom_content_loaded: 2, + }, + false + ); + + Services.cookieBanners.removeAllExecutedRecords(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 testCMPResultTelemetry( + { + success: 2, + success_dom_content_loaded: 2, + fail: 1, + fail_banner_not_found: 1, + }, + false + ); + + await SpecialPowers.pushPrefEnv({ + set: [["cookiebanners.bannerClicking.timeoutAfterLoad", 10000]], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(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 SpecialPowers.popPrefEnv(); + + await testCMPResultTelemetry({ + 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], + ], + }); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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 testCMPResultTelemetry({}); + + 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", + }); + + // Ensure we don't accidentally collect CMP result telemetry when + // domain-specific rule applies. + await testCMPResultTelemetry({}); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules_telemetry.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules_telemetry.js new file mode 100644 index 0000000000..6d9e96914d --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_globalRules_telemetry.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(clickTestSetup); + +async function verifyDetectedCMPTelemetry(expected) { + // Ensure we have all data from the content process. + await Services.fog.testFlushAllChildren(); + + let LABELS = [ + "trustarcbar", + "quantcast", + "borlabs", + "consentmanagernet", + "cookiebot", + "complianz", + "onetrust", + "didomi", + "sourcepoint", + ]; + + let testMetricState = doAssert => { + for (let label of LABELS) { + let expectedValue = expected[label] ?? null; + if (doAssert) { + is( + Glean.cookieBannersCmp.detectedCmp[label].testGetValue(), + expectedValue, + `Counter for label '${label}' has correct state.` + ); + } else if ( + Glean.cookieBannersCmp.detectedCmp[label].testGetValue() !== + expectedValue + ) { + 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); + } +} + +async function verifyRatioTelemetry(expected) { + // Ensure we have all data from the content process. + await Services.fog.testFlushAllChildren(); + + let cmpMetric = Glean.cookieBannersCmp.ratioHandledByCmpRule; + + let testMetricState = doAssert => { + let cmpValue = cmpMetric.testGetValue(); + + if (!cmpValue) { + return false; + } + + if (!doAssert) { + return ( + cmpValue.numerator == expected.cmp.numerator && + cmpValue.denominator == expected.cmp.denominator + ); + } + + is( + cmpValue.numerator, + expected.cmp.numerator, + "The numerator of rateHandledByCmpRule is expected" + ); + is( + cmpValue.denominator, + expected.cmp.denominator, + "The denominator of rateHandledByCmpRule is expected" + ); + + return true; + }; + + // Wait for matching the expected state. + try { + await TestUtils.waitForCondition( + testMetricState, + "Waiting for metric to match." + ); + } finally { + // Test again but this time with assertions and test all labels. + testMetricState(true); + } +} + +add_task(async function test_cmp_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.service.enableGlobalRules", true], + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 0], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Inserting global test rules."); + + info( + "Add global ruleA with a predefined identifier. This rule will handle the banner." + ); + let ruleA = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleA.id = "trustarcbar"; + ruleA.domains = []; + ruleA.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleA); + + info("Add global ruleC with another predefined identifier."); + let ruleC = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + ruleC.id = "quantcast"; + ruleC.domains = []; + ruleC.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_TOP, + null, + "button#nonExistingOptOut", + "button#nonExistingOptIn" + ); + Services.cookieBanners.insertRule(ruleC); + + info("Open a test page and verify the telemetry"); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await verifyRatioTelemetry({ + cmp: { numerator: 1, denominator: 1 }, + }); + await verifyDetectedCMPTelemetry({ + trustarcbar: 1, + quantcast: 1, + }); + + info("Open the test page again and verify if the telemetry gets updated"); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await verifyRatioTelemetry({ + cmp: { numerator: 2, denominator: 2 }, + }); + await verifyDetectedCMPTelemetry({ + trustarcbar: 2, + quantcast: 2, + }); + + 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, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(ruleDomain); + + info("Open the test page and now the site-specific rule will handle it"); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + info("Verify that the CMP telemetry doesn't get updated."); + await verifyDetectedCMPTelemetry({ + trustarcbar: 2, + quantcast: 2, + }); + + info( + "Verify the handled ratio telemetry for site-specific rule gets updated." + ); + await verifyRatioTelemetry({ + cmp: { numerator: 2, denominator: 3 }, + }); + + // Clear Telemetry + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); +}); 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..e6bfad4765 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_runContext.js @@ -0,0 +1,115 @@ +/* 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 }); + + // Clear executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + insertTestRules({ runContext: Ci.nsIClickRule.RUN_CHILD }); + + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + insertTestRules({ runContext: Ci.nsIClickRule.RUN_ALL }); + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + Services.cookieBanners.removeAllExecutedRecords(false); +}); 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_stopExecuteAfterSeveralAttempt.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_stopExecuteAfterSeveralAttempt.js new file mode 100644 index 0000000000..6e3a6f19fe --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_stopExecuteAfterSeveralAttempt.js @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +add_setup(async function () { + await clickTestSetup(); + + // Set the pref to enable stopping executing cookie banner handling after + // several tries. + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 1], + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + // Clean the records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(true); +}); + +// Ensure the banner clicking doesn't execute at the second load if the +// stopExecuteAfterAttempt feature is enabled. +add_task(async function testStopExecuteAfterOneAttempt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 1], + ], + }); + + insertTestClickRules(); + + // Open the domain and ensure the banner is clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain again and the banner shouldn't be hidden nor the banner is + // clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + // Clear the record for the normal window. + Services.cookieBanners.removeAllExecutedRecords(false); + + // Open the domain again after clearing the record and ensure the banner is + // clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Test in the private browsing mode. + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open the domain in PBM and ensure the banner is clicked. + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain in PBM again and the banner shouldn't be hidden nor the + // banner is clicked. + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + // Clear the record for the private window. + Services.cookieBanners.removeAllExecutedRecords(true); + + // Open the domain again in PBM after clearing the record, and ensure the + // banner is clicked. + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + // Clean the records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(true); +}); + +// Ensure the banner clicking doesn't execute after several attempts if the +// stopExecuteAfterAttempt feature is enabled. +add_task(async function testStopExecuteAfterSeveralAttempts() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 2], + ], + }); + + insertTestClickRules(); + + // Open the domain and ensure the banner is clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain in the second time and the banner should be clicked, + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain in the third time, the the banner shouldn't be hidden nor + // the banner is clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + // Test in the private browsing mode. + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Open the domain in PBM and ensure the banner is clicked. + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain in the second time and the banner should be clicked, + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain in PBM third time and the banner shouldn't be hidden nor + // the banner is clicked. + await openPageAndVerify({ + win: pbmWindow, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + await BrowserTestUtils.closeWindow(pbmWindow); + + // Clean the records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(true); +}); + +// Ensure that ForgetAboutSite clears the handled record properly. +add_task(async function testForgetAboutSiteWithStopExecuteAfterOneAttempt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 1], + ], + }); + + insertTestClickRules(); + + // Open the domain and ensure the banner is clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Open the domain again and the banner shouldn't be hidden nor the banner is + // clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: true, + expected: "NoClick", + }); + + // Call ForgetAboutSite for the domain. + await ForgetAboutSite.removeDataFromDomain(TEST_DOMAIN_A); + + // Open the domain again after ForgetAboutSite and the clicking should work + // again. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Clean the records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(true); +}); + +// Ensure the ClearDataService clears the handled record properly. +add_task(async function testClearDataServiceWithStopExecuteAfterOneAttempt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ["cookiebanners.bannerClicking.maxTriesPerSiteAndSession", 1], + ], + }); + + insertTestClickRules(); + + // Open the domain initially and ensure the banner is clicked. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Invoke deleteDataFromBaseDomain. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + TEST_DOMAIN_A, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD, + aResolve + ); + }); + + // Ensure the record is cleared. The banner clicking should work after clean. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Invoke deleteDataFromHost. + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + TEST_DOMAIN_A, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD, + aResolve + ); + }); + + // Ensure the record is cleared. The banner clicking should work after clean. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Invoke deleteDataFromPrincipal. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://" + TEST_DOMAIN_A + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD, + aResolve + ); + }); + + // Ensure the record is cleared. The banner clicking should work after clean. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Invoke deleteData. + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD, + aResolve + ); + }); + + // Ensure the record is cleared. The banner clicking should work after clean. + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + // Clean the records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(true); +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_telemetry_querySelector.js b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_telemetry_querySelector.js new file mode 100644 index 0000000000..0778c736a4 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_telemetry_querySelector.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Wait for the result of checkFn to equal expectedValue and run an assertion + * once the value matches. + * @param {function} checkFn - Function which returns value to compare to + * expectedValue. + * @param {*} expectedValue - Value to compare against checkFn return value. + * @param {string} message - Assertion / test message. + */ +async function waitForAndAssertEqual(checkFn, expectedValue, message) { + let checkFnResult; + await BrowserTestUtils.waitForCondition(() => { + checkFnResult = checkFn(); + return checkFnResult == expectedValue; + }, message); + Assert.equal(checkFnResult, expectedValue, message); +} + +/** + * Test that query selector histogram data is set / unset. + * @param {*} options + * @param {boolean} options.hasTopLevelData - Whether there should be telemetry + * data for top level windows. + * @param {boolean} options.hasFrameData = Whether there should be telemetry + * data for sub-frames. + */ +async function assertQuerySelectorTelemetry({ hasTopLevelData, hasFrameData }) { + // Ensure we have all data from the content process. + await Services.fog.testFlushAllChildren(); + + let runDurationTopLevel = () => + Glean.cookieBannersClick.querySelectorRunDurationPerWindowTopLevel.testGetValue() != + null; + let runDurationFrame = () => + Glean.cookieBannersClick.querySelectorRunDurationPerWindowFrame.testGetValue() != + null; + + let runCountTopLevel = () => + Glean.cookieBannersClick.querySelectorRunCountPerWindowTopLevel.testGetValue() != + null; + let runCountFrame = () => + Glean.cookieBannersClick.querySelectorRunCountPerWindowFrame.testGetValue() != + null; + + let messagePrefix = hasData => `Should${hasData ? "" : " not"} have`; + + await waitForAndAssertEqual( + runCountTopLevel, + hasTopLevelData, + `${messagePrefix(hasTopLevelData)} top-level run count data.` + ); + await waitForAndAssertEqual( + runDurationTopLevel, + hasTopLevelData, + `${messagePrefix(hasTopLevelData)} top-level run duration data.` + ); + + await waitForAndAssertEqual( + runDurationFrame, + hasFrameData, + `${messagePrefix(hasFrameData)} sub-frame run duration data.` + ); + await waitForAndAssertEqual( + runCountFrame, + hasFrameData, + `${messagePrefix(hasFrameData)} sub-frame run count data.` + ); +} + +add_setup(clickTestSetup); + +/** + * Tests that, when performing cookie banner clicking, query selector + * performance telemetry is collected. + */ +add_task(async function test_click_query_selector_telemetry() { + // Clear telemetry. + Services.fog.testResetFOG(); + + // Enable cookie banner handling. + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + insertTestClickRules(); + + info("No telemetry recorded initially."); + await assertQuerySelectorTelemetry({ + hasFrameData: false, + hasTopLevelData: false, + }); + + // No opt out rule for the example.org, the banner shouldn't be clicked. + info("Top level cookie banner with no matching rule."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_B, + testURL: TEST_PAGE_B, + visible: true, + expected: "NoClick", + }); + await assertQuerySelectorTelemetry({ + hasFrameData: false, + hasTopLevelData: true, + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + info("Top level cookie banner with matching rule."); + await openPageAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + await assertQuerySelectorTelemetry({ + hasFrameData: false, + hasTopLevelData: true, + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + info("Iframe cookie banner with matching rule."); + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + await assertQuerySelectorTelemetry({ + hasFrameData: true, + hasTopLevelData: true, + }); + + Services.cookieBanners.removeAllExecutedRecords(false); + + // Reset test rules and insert only site-specific rules so we can ensure the + // clicking mechanism is only running for the iframe, not the top level. + info("Insert test rules without global rules."); + insertTestClickRules(false); + + // Clear telemetry. + info("Clear telemetry."); + Services.fog.testResetFOG(); + + info("Iframe cookie banner with matching rule."); + await openIframeAndVerify({ + win: window, + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + await assertQuerySelectorTelemetry({ + hasFrameData: true, + hasTopLevelData: false, + }); + + // Clear telemetry. + Services.fog.testResetFOG(); +}); 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..0846f92620 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_bannerClicking_visibilityOverride.js @@ -0,0 +1,84 @@ +/* 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]) { + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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..7366ae2ab6 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_telemetry.js @@ -0,0 +1,781 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +const { MODE_DISABLED, MODE_REJECT, MODE_REJECT_OR_ACCEPT, MODE_UNSET } = + Ci.nsICookieBannerService; + +const TEST_MODES = [ + MODE_DISABLED, + MODE_REJECT, + MODE_REJECT_OR_ACCEPT, + 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"; + } + + 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.startLoadingURIString(browser, page); + } else { + BrowserTestUtils.startLoadingURIString(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", "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}.` + ); + } + + await SpecialPowers.popPrefEnv(); + } + } +}); + +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"; + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + // 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); + + Services.cookieBanners.removeAllExecutedRecords(false); + + 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(); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + // 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"]) { + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + 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); + + Services.cookieBanners.removeAllExecutedRecords(false); + + 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); + + Services.cookieBanners.removeAllExecutedRecords(false); + + 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.startLoadingURIString(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, + ], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + insertTestClickRules(); + + // Disable the cookie banner service in normal browsing. + // Keep it enabled in PBM so the service stays alive and can still collect telemetry. + 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.startLoadingURIString(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(); +}); + +add_task(async function test_service_detectOnly_telemetry() { + let service = Cc["@mozilla.org/cookie-banner-service;1"].getService( + Ci.nsIObserver + ); + + for (let detectOnly of [true, false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["cookiebanners.service.detectOnly", detectOnly]], + }); + + // Trigger the idle-daily on the cookie banner service. + service.observe(null, "idle-daily", null); + + is( + Glean.cookieBanners.serviceDetectOnly.testGetValue(), + detectOnly, + `Has set detect-only metric to ${detectOnly}.` + ); + + await SpecialPowers.popPrefEnv(); + } +}); diff --git a/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_webconsole.js b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_webconsole.js new file mode 100644 index 0000000000..20b1d490a8 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebanner_webconsole.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +/** + * Registers a console listener and waits for the cookie banner handled message + * to be logged. + * @returns {Promise} - Promise which resolves once the message has been logged. + */ +async function waitForCookieBannerHandledConsoleMsg() { + let msg; + let checkFn = msg => + msg && msg.match(/handled a cookie banner on behalf of the user./); + await new Promise(resolve => { + SpecialPowers.registerConsoleListener(consoleMsg => { + msg = consoleMsg.message; + if (checkFn(msg)) { + resolve(); + } + }); + }); + SpecialPowers.postConsoleSentinel(); + + ok(checkFn(msg), "Observed cookie banner handled console message."); +} + +add_setup(clickTestSetup); + +/** + * Tests that when we handle a banner via clicking a message is logged to the + * website console. + */ +add_task(async function test_banner_clicking_log_web_console() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ], + }); + + insertTestClickRules(); + + let consoleMsgPromise = waitForCookieBannerHandledConsoleMsg(); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + info("Handle the banner via click and wait for console message to appear."); + await openPageAndVerify({ + domain: TEST_DOMAIN_A, + testURL: TEST_PAGE_A, + visible: false, + expected: "OptOut", + }); + + await consoleMsgPromise; +}); + +/** + * Tests that when we handle a banner via cookie injection a message is logged + * to the website console. + */ +add_task(async function test_cookie_injection_log_web_console() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode", + Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, + ], + ["cookiebanners.cookieInjector.enabled", true], + ], + }); + + insertTestCookieRules(); + + let consoleMsgPromise = waitForCookieBannerHandledConsoleMsg(); + + // Clear the executed records before testing. + Services.cookieBanners.removeAllExecutedRecords(false); + + info( + "Handle the banner via cookie injection and wait for console message to appear." + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_ORIGIN_A + ); + BrowserTestUtils.removeTab(tab); + + await consoleMsgPromise; + + // Clear injected cookies. + await SiteDataTestUtils.clear(); +}); 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..361d6cb189 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice.js @@ -0,0 +1,635 @@ +/* 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( + "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("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..700155ada3 --- /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..fcc6078e28 --- /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..815fd115e9 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_hasRuleForBCTree.js @@ -0,0 +1,293 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +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(); + + // Visiting sites in this test can set cookies. Clean them up on test exit. + registerCleanupFunction(async () => { + await SiteDataTestUtils.clear(); + }); +}); + +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." + ); + }); +}); + +/** + * Tests that domain prefs are not considered when evaluating whether the + * service has an applicable rule for the given BrowsingContext. + */ +add_task(async function test_hasRuleForBCTree_ignoreDomainPrefs() { + 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" + ); + + // Disable for current site per domain pref. + Services.cookieBanners.setDomainPref( + browser.currentURI, + Ci.nsICookieBannerService.MODE_DISABLED, + false + ); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have rule when called with top BC for A, even if mechanism is disabled for A." + ); + + // Change mode via domain pref. + Services.cookieBanners.setDomainPref( + browser.currentURI, + Ci.nsICookieBannerService.MODE_REJECT, + false + ); + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should still have rule when called with top BC for A, even with custom mode for A" + ); + + // Cleanup. + Services.cookieBanners.removeAllDomainPrefs(false); + }); + + info("Test with top level B"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN_B, async browser => { + let bcTop = browser.browsingContext; + + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should have rule when called with top BC for B" + ); + + // Change mode via domain pref. + Services.cookieBanners.setDomainPref( + browser.currentURI, + Ci.nsICookieBannerService.MODE_REJECT, + false + ); + // Rule for B has no opt-out option. Since the mode is overridden to + // MODE_REJECT for B we don't have any applicable rule for it. This should + // however not be considered for the hasRule getter, it should ignore + // per-domain preferences and evaluate based on the global service mode + // instead. + ok( + Services.cookieBanners.hasRuleForBrowsingContextTree(bcTop), + "Should still have rule when called with top BC for B, even with custom mode for B" + ); + + // Cleanup. + Services.cookieBanners.removeAllDomainPrefs(false); + }); +}); 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..6e9197281c --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookiebannerservice_prefs.js @@ -0,0 +1,213 @@ +/* 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." + ); +}); + +/** + * 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, + ]; + + // 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..d341355f20 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector.js @@ -0,0 +1,625 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +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) { + BrowserTestUtils.startLoadingURIString(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() { + // Enable in PBM so the service is always initialized. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_REJECT, + ], + ], + }); + + for (let [serviceMode, detectOnly] of [ + [Ci.nsICookieBannerService.MODE_DISABLED, false], + [Ci.nsICookieBannerService.MODE_DISABLED, true], + [Ci.nsICookieBannerService.MODE_REJECT, true], + [Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, true], + ]) { + info(`Testing with serviceMode=${serviceMode}; detectOnly=${detectOnly}`); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", serviceMode], + ["cookiebanners.cookieInjector.enabled", true], + ["cookiebanners.service.detectOnly", detectOnly], + ], + }); + + await visitTestSites(); + assertNoCookies(); + + await SiteDataTestUtils.clear(); + await SpecialPowers.popPrefEnv(); + } +}); + +/** + * 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"); + BrowserTestUtils.startLoadingURIString(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, + }); + BrowserTestUtils.startLoadingURIString(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"); + BrowserTestUtils.startLoadingURIString(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 + ); + + BrowserTestUtils.startLoadingURIString(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..0459731a46 --- /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.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +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/browser_cookieinjector_telemetry.js b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector_telemetry.js new file mode 100644 index 0000000000..89a882814f --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/browser_cookieinjector_telemetry.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +add_setup(async function () { + await cookieInjectorTestSetup(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.bannerClicking.logLevel", "Debug"], + ["cookiebanners.bannerClicking.testing", true], + ], + }); +}); + +add_task(async function testCookieInjectionFailTelemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + info("Clearing existing rules"); + Services.cookieBanners.resetRules(false); + + info("Creating a rule contains both cookie and click rules."); + let rule = Cc["@mozilla.org/cookie-banner-rule;1"].createInstance( + Ci.nsICookieBannerRule + ); + rule.id = genUUID(); + rule.domains = [TEST_DOMAIN_A]; + + rule.addCookie( + true, + `cookieConsent_${TEST_DOMAIN_A}_1`, + "optOut1", + null, // empty host to fall back to .<domain> + "/", + 3600, + "", + false, + false, + false, + 0, + 0 + ); + + rule.addClickRule( + "div#banner", + false, + Ci.nsIClickRule.RUN_ALL, + null, + "button#optOut", + "button#optIn" + ); + Services.cookieBanners.insertRule(rule); + + Services.cookieBanners.removeAllExecutedRecords(false); + + // Open the test page with the banner to trigger the cookie injection failure. + let promise = promiseBannerClickingFinish(TEST_DOMAIN_A); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_A); + await promise; + + await Services.fog.testFlushAllChildren(); + + BrowserTestUtils.removeTab(tab); + + await TestUtils.waitForCondition(_ => { + return Glean.cookieBanners.cookieInjectionFail.testGetValue() != null; + }, "Waiting for metric to be ready."); + + is( + Glean.cookieBanners.cookieInjectionFail.testGetValue(), + 1, + "Counter for cookie_injection_fail has correct state" + ); + + // Clear Telemetry + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + + 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..d2af994f39 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner.html @@ -0,0 +1,28 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function hideBanner() { + document.getElementById("banner").style.display = "none"; + } + + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + hideBanner(); + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + hideBanner(); + } + </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..b71faa0ab4 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner_b.html @@ -0,0 +1,28 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function hideBanner() { + document.getElementById("banner").style.display = "none"; + } + + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + hideBanner(); + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + hideBanner(); + } + </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..9bde593b4d --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_banner_invisible.html @@ -0,0 +1,28 @@ +<html> +<head> + <title>A top-level page with cookie banner</title> + <script> + function hideBanner() { + document.getElementById("banner").style.display = "none"; + } + + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + hideBanner(); + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + hideBanner(); + } + </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..e528064434 --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_delayed_banner.html @@ -0,0 +1,54 @@ +<html> +<head> + <title>A top-level page with delayed cookie banner</title> + <script> + function hideBanner() { + document.getElementById("banner").style.display = "none"; + } + + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + hideBanner(); + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + hideBanner(); + } + + 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..0209ad76cd --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/file_delayed_banner_load.html @@ -0,0 +1,49 @@ +<html> +<head> + <title>A top-level page with delayed cookie banner</title> + <script> + function hideBanner() { + document.getElementById("banner").style.display = "none"; + } + + function clickOptOut() { + document.getElementById("result").textContent = "OptOut"; + hideBanner(); + } + + function clickOptIn() { + document.getElementById("result").textContent = "OptIn"; + hideBanner(); + } + + 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..e67ac49b9f --- /dev/null +++ b/toolkit/components/cookiebanners/test/browser/head.js @@ -0,0 +1,658 @@ +/* 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"], + // Avoid importing rules from RemoteSettings. They may interfere with test + // rules / assertions. + ["cookiebanners.listService.testSkipRemoteSettings", true], + ], + }); + + // Reset GLEAN (FOG) telemetry to avoid data bleeding over from other tests. + await Services.fog.testFlushAllChildren(); + 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); + + // Clear executed records. + Services.cookieBanners.removeAllExecutedRecords(false); + Services.cookieBanners.removeAllExecutedRecords(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.timeoutAfterLoad", 500], + ["cookiebanners.bannerClicking.enabled", true], + ["cookiebanners.cookieInjector.enabled", false], + ], + }); +} + +/** + * 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, + expectActorEnabled = true, +}) { + info(`Opening ${testURL}`); + + // If the actor isn't enabled there won't be a "finished" observer message. + let promise = expectActorEnabled + ? promiseBannerClickingFinish(domain) + : new Promise(resolve => setTimeout(resolve, 0)); + + 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(insertGlobalRules = true) { + 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); + + if (insertGlobalRules) { + 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. + */ +function testClickResultTelemetry(expected, resetFOG = true) { + return testClickResultTelemetryInternal( + Glean.cookieBannersClick.result, + expected, + resetFOG + ); +} + +/** + * Test the Glean.cookieBannersCmp.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. + */ +function testCMPResultTelemetry(expected, resetFOG = true) { + return testClickResultTelemetryInternal( + Glean.cookieBannersCmp.result, + expected, + resetFOG + ); +} + +/** + * Test the result metric. + * + * @param {Object} targetTelemetry - The target glean interface to run the + * checks + * @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 testClickResultTelemetryInternal( + targetTelemetry, + expected, + resetFOG +) { + // 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_cookie_injected", + "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) { + let expectedValue = expected[label] ?? null; + if (doAssert) { + is( + targetTelemetry[label].testGetValue(), + expectedValue, + `Counter for label '${label}' has correct state.` + ); + } else if (targetTelemetry[label].testGetValue() !== expectedValue) { + 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) { + await Services.fog.testFlushAllChildren(); + 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 {boolean} options.detectOnly - Whether the service should be enabled + * in detection only mode, where it does not handle banners. + * @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, detectOnly, initFn, triggerFn, testURL }) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["cookiebanners.service.mode", mode], + [ + "cookiebanners.service.mode.privateBrowsing", + Ci.nsICookieBannerService.MODE_DISABLED, + ], + ["cookiebanners.service.detectOnly", detectOnly], + ], + }); + + await initFn(); + + let expectEventDetected = mode != Ci.nsICookieBannerService.MODE_DISABLED; + let expectEventHandled = + !detectOnly && + (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..bc69a49d0f --- /dev/null +++ b/toolkit/components/cookiebanners/test/unit/test_cookiebannerlistservice.js @@ -0,0 +1,611 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +let { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// 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.toml b/toolkit/components/cookiebanners/test/unit/xpcshell.toml new file mode 100644 index 0000000000..cfa2723627 --- /dev/null +++ b/toolkit/components/cookiebanners/test/unit/xpcshell.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_cookiebannerlistservice.js"] |