Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland'
- This test runs only with pattern: buildapp == 'browser'
- Manifest: browser/components/backup/tests/marionette/manifest.toml
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import sys
import tempfile
from pathlib import Path
import mozfile
sys.path.append(os.fspath(Path(__file__).parents[0]))
from backup_test_base import BackupTestBase
class BackupReplaceCurrentProfileTest(BackupTestBase):
"""
Tests for recovering backups with replaceCurrentProfile=true.
This includes:
1. Verifying the old profile is deleted from the database
2. Verifying metadata (name, avatar, theme) is copied when recovering
a legacy backup into a selectable profile environment
"""
def test_replace_current_profile(self):
"""
Tests that recovering with replaceCurrentProfile=true deletes the
current profile from the database.
"""
self.logger.info("=== Test: Replace Current Profile ===")
self.logger.info("Step 1: Setting up source selectable profile")
profile_name = self.register_profile_and_restart()
self._cleanups.append({"profile_name": profile_name})
self.logger.info(f"Registered toolkit profile: {profile_name}")
selectable_info = self.setup_selectable_profile()
original_profile_id = selectable_info["id"]
original_profile_path = selectable_info["path"]
self.logger.info(
f"Source profile: id={original_profile_id}, path={original_profile_path}"
)
self.marionette.restart(clean=False, in_app=True)
self.logger.info("Step 2: Creating backup")
self._archive_path = self.create_backup()
self._cleanups.append({"path": self._archive_path})
self.assertTrue(
os.path.exists(self._archive_path), "Backup archive should exist"
)
self.logger.info(f"Backup created at: {self._archive_path}")
self.logger.info("Step 3: Getting profiles before recovery")
profiles_before = self.get_all_profiles()
self.logger.info(f"Profiles before recovery: {profiles_before}")
self.logger.info("Step 4: Recovering backup with replaceCurrentProfile=true")
self._recovery_path = os.path.join(
tempfile.gettempdir(), "replace-profile-recovery"
)
mozfile.remove(self._recovery_path)
self._cleanups.append({"path": self._recovery_path})
new_profile_root = self.run_async(
"""
let newProfileRoot = await IOUtils.createUniqueDirectory(
PathUtils.tempDir, "replaceProfileTest"
);
return newProfileRoot;
"""
)
self._cleanups.append({"path": new_profile_root})
self.logger.info(f"Created profile root for recovery: {new_profile_root}")
self.run_async(
"""
const { BackupService } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupService.sys.mjs"
);
let [archivePath, recoveryPath, profileRoot] = arguments;
let bs = BackupService.get();
bs.deleteAndQuitCurrentSelectableProfile = async () => null;
await bs.recoverFromBackupArchive(
archivePath, null, false, recoveryPath, profileRoot, true
);
""",
script_args=[self._archive_path, self._recovery_path, new_profile_root],
)
self.logger.info("Step 5: Launching recovered profile to verify it works")
recovered_profile_dirs = os.listdir(new_profile_root)
self.assertEqual(
len(recovered_profile_dirs), 1, "Should have exactly one recovered profile"
)
recovered_profile_path = os.path.join(
new_profile_root, recovered_profile_dirs[0]
)
self.logger.info(f"Recovered profile path: {recovered_profile_path}")
self._new_profile_path = recovered_profile_path
self.marionette.quit()
self.marionette.instance.profile = recovered_profile_path
self.marionette.start_session()
self.marionette.set_context("chrome")
self.wait_for_post_recovery()
self.logger.info("Post-recovery complete")
self.init_selectable_profile_service()
self.logger.info("Step 6: Cleaning up")
self.cleanup_selectable_profiles()
self.logger.info("=== Test: Replace Current Profile PASSED ===")
def test_replace_current_profile_legacy_backup_copies_metadata(self):
"""
Tests that recovering a legacy backup with replaceCurrentProfile=true
copies the current profile's metadata (name, avatar, theme) to the
new profile.
"""
self.logger.info(
"=== Test: Replace Current Profile - Legacy Backup Copies Metadata ==="
)
self.logger.info("Step 1: Creating legacy backup (no selectable profiles)")
profile_name = self.register_profile_and_restart()
self._cleanups.append({"profile_name": profile_name})
self.set_prefs({
"browser.profiles.enabled": False,
"browser.profiles.created": False,
})
has_selectable = self.has_selectable_profiles()
self.assertFalse(has_selectable, "Should be a legacy profile")
self.logger.info("Verified profile is legacy")
self._archive_path = self.create_backup()
self._cleanups.append({"path": self._archive_path})
self.assertTrue(
os.path.exists(self._archive_path), "Backup archive should exist"
)
self.logger.info(f"Legacy backup created at: {self._archive_path}")
self.logger.info(
"Step 2: Setting up destination selectable profile with custom metadata"
)
self.marionette.quit()
self.marionette.instance.switch_profile()
self.marionette.start_session()
self.marionette.set_context("chrome")
recovery_profile_name = self.register_profile_and_restart()
self._cleanups.append({"profile_name": recovery_profile_name})
self.setup_selectable_profile()
custom_name = "My Custom Profile"
custom_avatar = "book"
custom_theme = {"themeBg": "#ff5500", "themeFg": "#ffffff"}
self.run_async(
"""
const { SelectableProfileService } = ChromeUtils.importESModule(
"resource:///modules/profiles/SelectableProfileService.sys.mjs"
);
let profile = SelectableProfileService.currentProfile;
profile.name = arguments[0];
profile.setAvatar(arguments[1]);
profile.theme = arguments[2];
""",
script_args=[custom_name, custom_avatar, custom_theme],
)
self.logger.info(
f"Set custom metadata: name={custom_name}, avatar={custom_avatar}"
)
self.logger.info(
"Step 3: Recovering legacy backup with replaceCurrentProfile=true"
)
self._recovery_path = os.path.join(
tempfile.gettempdir(), "legacy-replace-profile-recovery"
)
mozfile.remove(self._recovery_path)
self._cleanups.append({"path": self._recovery_path})
new_profile_root = self.run_async(
"""
let newProfileRoot = await IOUtils.createUniqueDirectory(
PathUtils.tempDir, "legacyReplaceTest"
);
return newProfileRoot;
"""
)
self._cleanups.append({"path": new_profile_root})
self.logger.info(f"Created profile root for recovery: {new_profile_root}")
self.run_async(
"""
const { BackupService } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupService.sys.mjs"
);
let [archivePath, recoveryPath, profileRoot] = arguments;
let bs = BackupService.get();
bs.deleteAndQuitCurrentSelectableProfile = async () => null;
await bs.recoverFromBackupArchive(
archivePath, null, false, recoveryPath, profileRoot, true
);
""",
script_args=[self._archive_path, self._recovery_path, new_profile_root],
)
self.logger.info("Step 4: Launching recovered profile to verify metadata")
recovered_profile_dirs = os.listdir(new_profile_root)
self.assertEqual(
len(recovered_profile_dirs), 1, "Should have exactly one recovered profile"
)
recovered_profile_path = os.path.join(
new_profile_root, recovered_profile_dirs[0]
)
self.logger.info(f"Recovered profile path: {recovered_profile_path}")
self._new_profile_path = recovered_profile_path
self.marionette.quit()
self.marionette.instance.profile = recovered_profile_path
self.marionette.start_session()
self.marionette.set_context("chrome")
self.wait_for_post_recovery()
self.logger.info("Post-recovery complete")
self.logger.info("Step 5: Verifying recovered profile has copied metadata")
self.init_selectable_profile_service()
recovered_profile_metadata = self.run_async(
"""
const { SelectableProfileService } = ChromeUtils.importESModule(
"resource:///modules/profiles/SelectableProfileService.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
let filePath = new FileUtils.File(arguments[0]);
let profile = await SelectableProfileService.getProfileByPath(filePath);
if (!profile) return null;
return {
id: profile.id,
name: profile.name,
avatar: profile.avatar,
theme: profile.theme
};
""",
script_args=[recovered_profile_path],
)
self.assertIsNotNone(
recovered_profile_metadata, "Should have recovered profile metadata"
)
self.assertEqual(
recovered_profile_metadata["name"],
custom_name,
"Recovered profile should have the old profile's name",
)
self.assertEqual(
recovered_profile_metadata["avatar"],
custom_avatar,
"Recovered profile should have the old profile's avatar",
)
self.logger.info(
f"Verified metadata was copied: name={recovered_profile_metadata['name']}, "
f"avatar={recovered_profile_metadata['avatar']}"
)
self.logger.info("Step 6: Cleaning up")
self.cleanup_selectable_profiles()
self.logger.info(
"=== Test: Replace Current Profile - Legacy Backup Copies Metadata PASSED ==="
)