From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test_colorways_builtin_theme_upgrades.js | 582 +++++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js (limited to 'toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js') diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js b/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js new file mode 100644 index 0000000000..6449481f67 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_colorways_builtin_theme_upgrades.js @@ -0,0 +1,582 @@ +"use strict"; + +const { BuiltInThemes } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemes.sys.mjs" +); +const { BuiltInThemeConfig } = ChromeUtils.importESModule( + "resource:///modules/BuiltInThemeConfig.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// Enable SCOPE_APPLICATION for builtin testing. +let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION; +Services.prefs.setIntPref("extensions.enabledScopes", scopes); + +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +const ADDON_ID = "mock-colorway@mozilla.org"; +const ADDON_ID_RETAINED = "mock-disabled-retained-colorway@mozilla.org"; +const ADDON_ID_NOT_RETAINED = "mock-disabled-not-retained-colorway@mozilla.org"; +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; +const NOT_MIGRATED_THEME = "mock-not-migrated-theme@mozilla.org"; + +const RETAINED_THEMES_PREF = "browser.theme.retainedExpiredThemes"; +const COLORWAY_MIGRATION_PREF = "browser.theme.colorway-migration"; + +const ICON_SVG = ` + + + + + + + + + +`; +const createMockThemeManifest = (id, version) => ({ + name: "A mock colorway theme", + author: "Mozilla", + version, + icons: { 32: "icon.svg" }, + theme: { + colors: { + toolbar: "red", + }, + }, + browser_specific_settings: { + gecko: { id }, + }, +}); + +let server = createHttpServer(); + +const SERVER_BASE_URL = `http://localhost:${server.identity.primaryPort}`; + +// The test extension uses an insecure update url. +Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false); +Services.prefs.setCharPref( + "extensions.update.background.url", + `${SERVER_BASE_URL}/upgrade.json` +); + +AddonTestUtils.registerJSON(server, "/upgrade.json", { + addons: { + [ADDON_ID]: { + updates: [ + { + version: "2.0.0", + update_link: `${SERVER_BASE_URL}/${ADDON_ID}.xpi`, + }, + ], + }, + [ADDON_ID_RETAINED]: { + updates: [ + { + version: "3.0.0", + update_link: `${SERVER_BASE_URL}/${ADDON_ID_RETAINED}.xpi`, + }, + ], + }, + // We list the test extension with addon id ADDON_ID_NOT_RETAINED here, + // but the xpi file doesn't exist because we expect that we wouldn't + // be checking this extension for updates, and that expected behavior + // regresses, the test would fail either for the explicit assertion + // (checking that we don't find an update) or because we would be trying + // to download a file from an url that isn't going to be handled. + [ADDON_ID_NOT_RETAINED]: { + updates: [ + { + version: "4.0.0", + update_link: `${SERVER_BASE_URL}/non-existing.xpi`, + }, + ], + }, + }, +}); + +function createWebExtensionFile(id, version) { + return AddonTestUtils.createTempWebExtensionFile({ + files: { "icon.svg": ICON_SVG }, + manifest: createMockThemeManifest(id, version), + }); +} + +let xpiUpdate = createWebExtensionFile(ADDON_ID, "2.0.0"); +let retainedThemeUpdate = createWebExtensionFile(ADDON_ID_RETAINED, "3.0.0"); + +server.registerFile(`/${ADDON_ID}.xpi`, xpiUpdate); +server.registerFile(`/${ADDON_ID_RETAINED}.xpi`, retainedThemeUpdate); + +function assertAddonWrapperProperties( + addonWrapper, + { id, version, isBuiltin, type, isBuiltinColorwayTheme, scope } +) { + Assert.deepEqual( + { + id: addonWrapper.id, + version: addonWrapper.version, + type: addonWrapper.type, + scope: addonWrapper.scope, + isBuiltin: addonWrapper.isBuiltin, + isBuiltinColorwayTheme: addonWrapper.isBuiltinColorwayTheme, + }, + { + id, + version, + type, + scope, + isBuiltin, + isBuiltinColorwayTheme, + }, + `Got expected properties on addon wrapper for "${id}"` + ); +} + +function assertAddonCanUpgrade(addonWrapper, canUpgrade) { + equal( + !!(addonWrapper.permissions & AddonManager.PERM_CAN_UPGRADE), + canUpgrade, + `Expected "${addonWrapper.id}" to ${ + canUpgrade ? "have" : "not have" + } PERM_CAN_UPGRADE AOM permission` + ); +} + +function assertIsActiveThemeID(addonId) { + equal( + Services.prefs.getCharPref("extensions.activeThemeID"), + addonId, + `Expect ${addonId} to be the currently active theme` + ); +} + +function assertIsExpiredTheme(addonId, expectExpired) { + equal( + // themeIsExpired returns undefined for themes without an expiry date, + // normalized here to always be a boolean. + !!BuiltInThemes.themeIsExpired(addonId), + expectExpired, + `Expect ${addonId} to be recognized as an expired colorway theme` + ); +} + +function assertIsRetainedExpiredTheme(addonId, expectRetainedExpired) { + equal( + BuiltInThemes.isRetainedExpiredTheme(addonId), + expectRetainedExpired, + `Expect ${addonId} to be recognized as a retained expired colorway theme` + ); +} + +function waitForBootstrapUpdateMethod(addonId, newVersion) { + return new Promise(resolve => { + function listener(_evt, { method, params }) { + if ( + method === "update" && + params.id === addonId && + params.newVersion === newVersion + ) { + AddonTestUtils.off("bootstrap-method", listener); + info(`Update bootstrap method called for ${addonId} ${newVersion}`); + resolve(); + } + } + AddonTestUtils.on("bootstrap-method", listener); + }); +} + +let waitForTemporaryXPIFilesRemoved; + +add_setup(async () => { + info("Creating BuiltInThemes stubs"); + const sandbox = sinon.createSandbox(); + // Restoring the mocked BuiltInThemeConfig doesn't really matter for xpcshell + // because each test file will run in its own separate xpcshell instance, + // but cleaning it up doesn't harm neither. + registerCleanupFunction(() => { + info("Restoring BuiltInThemes sandbox for cleanup"); + sandbox.restore(); + BuiltInThemes.builtInThemeMap = BuiltInThemeConfig; + }); + + // Mock BuiltInThemes builtInThemeMap. + BuiltInThemes.builtInThemeMap = new Map(); + sandbox.stub(BuiltInThemes.builtInThemeMap, "get").callsFake(id => { + info(`Mock BuiltInthemes.builtInThemeMap.get result for ${id}`); + // No theme info is expected to be returned for the default-theme. + if (id === DEFAULT_THEME_ID) { + return undefined; + } + if (!id.endsWith("colorway@mozilla.org")) { + return BuiltInThemeConfig.get(id); + } + let mockThemeProperties = { + collection: "Mock expired colorway collection", + figureUrl: "about:blank", + expiry: new Date("1970-01-01"), + }; + return mockThemeProperties; + }); + + // Start AOM and make sure updates are enabled. + await AddonTestUtils.promiseStartupManager(); + AddonManager.updateEnabled = true; + + // Enable the default theme explicitly (mainly because on DevEdition builds + // the dark theme would be the one enabled by default). + const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + await defaultTheme.enable(); + assertIsActiveThemeID(defaultTheme.id); + + const tmpFiles = new Set(); + const addonInstallListener = { + onInstallEnded: function collectTmpFiles(install) { + tmpFiles.add(install.file); + }, + }; + AddonManager.addInstallListener(addonInstallListener); + registerCleanupFunction(() => { + AddonManager.removeInstallListener(addonInstallListener); + }); + + // Make sure all the tempfile created for the background updates have + // been removed (otherwise AddonTestUtils cleanup function will trigger + // intermittent test failures due to unexpected xpi files that may still + // be found in the temporary directory). + waitForTemporaryXPIFilesRemoved = async () => { + info( + "Wait for temporary xpi files created by the background updates to have been removed" + ); + const files = Array.from(tmpFiles); + tmpFiles.clear(); + await TestUtils.waitForCondition(async () => { + for (const file of files) { + if (await file.exists()) { + return false; + } + } + return true; + }, "Wait for the temporary files created for the background updates to have been removed"); + }; +}); + +add_task( + { + pref_set: [[COLORWAY_MIGRATION_PREF, false]], + }, + async function test_colorways_migration_disabled() { + info("Install and activate a colorway built-in test theme"); + + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID, "1.0.0"), + }, + false /* waitForStartup */ + ); + const activeTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(activeTheme, { + id: ADDON_ID, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent( + "onEnabled", + addon => addon.id === ADDON_ID + ); + await activeTheme.enable(); + await promiseThemeEnabled; + ok(activeTheme.isActive, "Expect the colorways theme to be active"); + assertIsActiveThemeID(activeTheme.id); + + info("Verify that built-in colorway migration is disabled as expected"); + + assertAddonCanUpgrade(activeTheme, false); + + const promiseBackgroundUpdatesFound = TestUtils.topicObserved( + "addons-background-updates-found" + ); + await AddonManagerPrivate.backgroundUpdateCheck(); + const [, numUpdatesFound] = await promiseBackgroundUpdatesFound; + equal(numUpdatesFound, 0, "Expect no add-on updates to be found"); + + await activeTheme.uninstall(); + } +); + +add_task( + { + pref_set: [ + [COLORWAY_MIGRATION_PREF, true], + [ + RETAINED_THEMES_PREF, + JSON.stringify([ADDON_ID_RETAINED, NOT_MIGRATED_THEME]), + ], + ], + }, + async function test_colorways_builtin_upgrade() { + info("Verify default theme initially enabled"); + const defaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + assertAddonWrapperProperties(defaultTheme, { + id: DEFAULT_THEME_ID, + version: defaultTheme.version, + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: false, + }); + ok( + defaultTheme.isActive, + "Expect the default theme to be initially active" + ); + assertIsActiveThemeID(defaultTheme.id); + + info("Install the non retained expired colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID_NOT_RETAINED, "1.0.0"), + }, + false /* waitForStartup */ + ); + const notRetainedTheme = await AddonManager.getAddonByID( + ADDON_ID_NOT_RETAINED + ); + assertAddonWrapperProperties(notRetainedTheme, { + id: ADDON_ID_NOT_RETAINED, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + + info("Install the retained expired colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID_RETAINED, "1.0.0"), + }, + false /* waitForStartup */ + ); + const retainedTheme = await AddonManager.getAddonByID(ADDON_ID_RETAINED); + assertAddonWrapperProperties(retainedTheme, { + id: ADDON_ID_RETAINED, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + + info("Install the active colorway test theme"); + await installBuiltinExtension( + { + manifest: createMockThemeManifest(ADDON_ID, "1.0.0"), + }, + false /* waitForStartup */ + ); + const activeTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(activeTheme, { + id: ADDON_ID, + version: "1.0.0", + type: "theme", + scope: AddonManager.SCOPE_APPLICATION, + isBuiltin: true, + isBuiltinColorwayTheme: true, + }); + const promiseThemeEnabled = AddonTestUtils.promiseAddonEvent( + "onEnabled", + addon => addon.id === ADDON_ID + ); + await activeTheme.enable(); + await promiseThemeEnabled; + ok(activeTheme.isActive, "Expect the colorways theme to be active"); + assertIsActiveThemeID(activeTheme.id); + + info("Verify only active and retained colorways themes can be upgraded"); + assertIsActiveThemeID(activeTheme.id); + assertIsExpiredTheme(activeTheme.id, true); + assertIsRetainedExpiredTheme(activeTheme.id, false); + + assertIsExpiredTheme(retainedTheme.id, true); + assertIsRetainedExpiredTheme(retainedTheme.id, true); + + assertIsExpiredTheme(notRetainedTheme.id, true); + assertIsRetainedExpiredTheme(notRetainedTheme.id, false); + + assertIsExpiredTheme(defaultTheme.id, false); + assertIsRetainedExpiredTheme(defaultTheme.id, false); + + assertAddonCanUpgrade(retainedTheme, true); + assertAddonCanUpgrade(notRetainedTheme, false); + assertAddonCanUpgrade(activeTheme, true); + // Make sure a non-colorways built-in theme cannot check for updates. + assertAddonCanUpgrade(defaultTheme, false); + + Assert.deepEqual( + Services.prefs.getStringPref(RETAINED_THEMES_PREF), + JSON.stringify([retainedTheme.id, NOT_MIGRATED_THEME]), + `Expect the retained theme id to be listed in the ${RETAINED_THEMES_PREF} pref` + ); + + const promiseUpdatesInstalled = Promise.all([ + waitForBootstrapUpdateMethod(ADDON_ID, "2.0.0"), + waitForBootstrapUpdateMethod(ADDON_ID_RETAINED, "3.0.0"), + ]); + + const promiseInstallsEnded = Promise.all([ + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + addon => addon.id === ADDON_ID + ), + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + addon => addon.id === ADDON_ID_RETAINED + ), + ]); + + const promiseActiveThemeStartupCompleted = + AddonTestUtils.promiseWebExtensionStartup(ADDON_ID); + + const promiseBackgroundUpdatesFound = TestUtils.topicObserved( + "addons-background-updates-found" + ); + await AddonManagerPrivate.backgroundUpdateCheck(); + const [, numUpdatesFound] = await promiseBackgroundUpdatesFound; + equal(numUpdatesFound, 2, "Expect 2 add-on updates to have been found"); + + info("Wait for the 2 expected updates to be completed"); + await promiseUpdatesInstalled; + + const updatedActiveTheme = await AddonManager.getAddonByID(ADDON_ID); + assertAddonWrapperProperties(updatedActiveTheme, { + id: ADDON_ID, + version: "2.0.0", + type: "theme", + scope: AddonManager.SCOPE_PROFILE, + isBuiltin: false, + isBuiltinColorwayTheme: false, + }); + // Expect the updated active theme to stay set as the currently active theme. + assertIsActiveThemeID(updatedActiveTheme.id); + + info("Verify addon update on disabled builtin colorway theme"); + + const updatedRetainedTheme = await AddonManager.getAddonByID( + ADDON_ID_RETAINED + ); + assertAddonWrapperProperties(updatedRetainedTheme, { + id: ADDON_ID_RETAINED, + version: "3.0.0", + type: "theme", + scope: AddonManager.SCOPE_PROFILE, + isBuiltin: false, + isBuiltinColorwayTheme: false, + }); + // Expect the updated active theme to stay set as the currently active theme. + assertIsActiveThemeID(updatedActiveTheme.id); + ok(updatedActiveTheme.isActive, "Expect the colorways theme to be active"); + + // We need to wait for the active theme startup otherwise uninstall the active + // theme will fail to remove the xpi file because it is stil active while the + // test is running on windows builds. + info("Wait for the active theme to have been fully loaded"); + await promiseActiveThemeStartupCompleted; + + await promiseInstallsEnded; + + Assert.deepEqual( + Services.prefs.getStringPref(RETAINED_THEMES_PREF), + JSON.stringify([NOT_MIGRATED_THEME]), + `Expect migrated retained theme to not be listed anymore in the ${RETAINED_THEMES_PREF} pref` + ); + + info( + "uninstall test colorways themes and expect default theme to become active" + ); + + const promiseUninstalled = Promise.all([ + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID + ), + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID_RETAINED + ), + AddonTestUtils.promiseAddonEvent( + "onUninstalled", + addon => addon.id === ADDON_ID_NOT_RETAINED + ), + ]); + + const promiseDefaultThemeEnabled = + AddonTestUtils.promiseAddonEvent("onEnabled"); + + await updatedActiveTheme.uninstall(); + await updatedRetainedTheme.uninstall(); + await notRetainedTheme.uninstall(); + + await promiseUninstalled; + + info("Wait for the default theme to become active"); + // Waiting explicitly for the onEnabled addon event prevents a race between + // the test task exiting (and the AddonManager being shutdown automatically + // as a side effect of that) and the XPIProvider trying to call the addon event + // listeners for the default theme being enabled), which would trigger a test + // failure after the test is existing. + await promiseDefaultThemeEnabled; + + ok(defaultTheme.isActive, "Expect the default theme to be active"); + assertIsActiveThemeID(defaultTheme.id); + + // Wait for the temporary file to be actually removed, otherwise the hack we use + // to mock an AOM restart (which is unloading the related jsm modules) may + // affect the successfull removal of the temporary file because some of the + // global helpers defined and used inside the XPIProvider may have been gone + // already and intermittently trigger unexpected errors. + await waitForTemporaryXPIFilesRemoved(); + + // Restart the addon manager to confirm that the migrated colorways themes + // are still gone after an AOM restart and that the previously installed + // builtin hasn't been made implicitly visible again. + info( + "Verify old builtin colorways are not visible and default-theme still active after AOM restart" + ); + await AddonTestUtils.promiseRestartManager(); + + const defaultThemeAfterRestart = await AddonManager.getAddonByID( + DEFAULT_THEME_ID + ); + ok( + defaultThemeAfterRestart.isActive, + "Expect the default theme to be active" + ); + + equal( + (await AddonManager.getAddonByID(ADDON_ID))?.version, + undefined, + "Expect the active theme addon to not be available anymore after being uninstalled" + ); + equal( + (await AddonManager.getAddonByID(ADDON_ID_RETAINED))?.version, + undefined, + "Expect the retained theme addon to not be available anymore after being uninstalled" + ); + equal( + (await AddonManager.getAddonByID(ADDON_ID_NOT_RETAINED))?.version, + undefined, + "Expect the not retained expired theme addon to not be available anymore after being uninstalled" + ); + } +); -- cgit v1.2.3