summaryrefslogtreecommitdiffstats
path: root/browser/components/backup/tests/xpcshell/test_BackupService.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/backup/tests/xpcshell/test_BackupService.js')
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupService.js451
1 files changed, 451 insertions, 0 deletions
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService.js b/browser/components/backup/tests/xpcshell/test_BackupService.js
new file mode 100644
index 0000000000..33fb9fbb99
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_BackupService.js
@@ -0,0 +1,451 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { JsonSchemaValidator } = ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs"
+);
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+
+add_setup(function () {
+ // Much of this setup is copied from toolkit/profile/xpcshell/head.js. It is
+ // needed in order to put the xpcshell test environment into the state where
+ // it thinks its profile is the one pointed at by
+ // nsIToolkitProfileService.currentProfile.
+ let gProfD = do_get_profile();
+ let gDataHome = gProfD.clone();
+ gDataHome.append("data");
+ gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let gDataHomeLocal = gProfD.clone();
+ gDataHomeLocal.append("local");
+ gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
+ Ci.nsIXREDirProvider
+ );
+ xreDirProvider.setUserDataDirectory(gDataHome, false);
+ xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
+
+ let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+
+ let createdProfile = {};
+ let didCreate = profileSvc.selectStartupProfile(
+ ["xpcshell"],
+ false,
+ AppConstants.UPDATE_CHANNEL,
+ "",
+ {},
+ {},
+ createdProfile
+ );
+ Assert.ok(didCreate, "Created a testing profile and set it to current.");
+ Assert.equal(
+ profileSvc.currentProfile,
+ createdProfile.value,
+ "Profile set to current"
+ );
+});
+
+/**
+ * A utility function for testing BackupService.createBackup. This helper
+ * function:
+ *
+ * 1. Ensures that `backup` will be called on BackupResources with the service
+ * 2. Ensures that a backup-manifest.json will be written and contain the
+ * ManifestEntry data returned by each BackupResource.
+ * 3. Ensures that a `staging` folder will be written to and renamed properly
+ * once the backup creation is complete.
+ *
+ * Once this is done, a task function can be run. The task function is passed
+ * the parsed backup-manifest.json object as its only argument.
+ *
+ * @param {object} sandbox
+ * The Sinon sandbox to be used stubs and mocks. The test using this helper
+ * is responsible for creating and resetting this sandbox.
+ * @param {Function} taskFn
+ * A function that is run once all default checks are done on the manifest
+ * and staging folder. After this function returns, the staging folder will
+ * be cleaned up.
+ * @returns {Promise<undefined>}
+ */
+async function testCreateBackupHelper(sandbox, taskFn) {
+ const EXPECTED_CLIENT_ID = await ClientID.getClientID();
+
+ let fake1ManifestEntry = { fake1: "hello from 1" };
+ sandbox
+ .stub(FakeBackupResource1.prototype, "backup")
+ .resolves(fake1ManifestEntry);
+
+ sandbox
+ .stub(FakeBackupResource2.prototype, "backup")
+ .rejects(new Error("Some failure to backup"));
+
+ let fake3ManifestEntry = { fake3: "hello from 3" };
+ sandbox
+ .stub(FakeBackupResource3.prototype, "backup")
+ .resolves(fake3ManifestEntry);
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let fakeProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "createBackupTest"
+ );
+
+ await bs.createBackup({ profilePath: fakeProfilePath });
+
+ // We expect the staging folder to exist then be renamed under the fakeProfilePath.
+ // We should also find a folder for each fake BackupResource.
+ let backupsFolderPath = PathUtils.join(fakeProfilePath, "backups");
+ let stagingPath = PathUtils.join(backupsFolderPath, "staging");
+
+ // For now, we expect a single backup only to be saved.
+ let backups = await IOUtils.getChildren(backupsFolderPath);
+ Assert.equal(
+ backups.length,
+ 1,
+ "There should only be 1 backup in the backups folder"
+ );
+
+ let renamedFilename = await PathUtils.filename(backups[0]);
+ let expectedFormatRegex = /^\d{4}(-\d{2}){2}T(\d{2}-){2}\d{2}Z$/;
+ Assert.ok(
+ renamedFilename.match(expectedFormatRegex),
+ "Renamed staging folder should have format YYYY-MM-DDTHH-mm-ssZ"
+ );
+
+ let stagingPathRenamed = PathUtils.join(backupsFolderPath, renamedFilename);
+
+ for (let backupResourceClass of [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ]) {
+ let expectedResourceFolderBeforeRename = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+ let expectedResourceFolderAfterRename = PathUtils.join(
+ stagingPathRenamed,
+ backupResourceClass.key
+ );
+
+ Assert.ok(
+ await IOUtils.exists(expectedResourceFolderAfterRename),
+ `BackupResource folder exists for ${backupResourceClass.key} after rename`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledOnce,
+ `Backup was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledWith(
+ expectedResourceFolderBeforeRename,
+ fakeProfilePath
+ ),
+ `Backup was called in the staging folder for ${backupResourceClass.key} before rename`
+ );
+ }
+
+ // Check that resources were called from highest to lowest backup priority.
+ sinon.assert.callOrder(
+ FakeBackupResource3.prototype.backup,
+ FakeBackupResource2.prototype.backup,
+ FakeBackupResource1.prototype.backup
+ );
+
+ let manifestPath = PathUtils.join(
+ stagingPathRenamed,
+ BackupService.MANIFEST_FILE_NAME
+ );
+
+ Assert.ok(await IOUtils.exists(manifestPath), "Manifest file exists");
+ let manifest = await IOUtils.readJSON(manifestPath);
+
+ let schema = await BackupService.MANIFEST_SCHEMA;
+ let validationResult = JsonSchemaValidator.validate(manifest, schema);
+ Assert.ok(validationResult.valid, "Schema matches manifest");
+ Assert.deepEqual(
+ Object.keys(manifest.resources).sort(),
+ ["fake1", "fake3"],
+ "Manifest contains all expected BackupResource keys"
+ );
+ Assert.deepEqual(
+ manifest.resources.fake1,
+ fake1ManifestEntry,
+ "Manifest contains the expected entry for FakeBackupResource1"
+ );
+ Assert.deepEqual(
+ manifest.resources.fake3,
+ fake3ManifestEntry,
+ "Manifest contains the expected entry for FakeBackupResource3"
+ );
+ Assert.equal(
+ manifest.meta.legacyClientID,
+ EXPECTED_CLIENT_ID,
+ "The client ID was stored properly."
+ );
+
+ taskFn(manifest);
+
+ // After createBackup is more fleshed out, we're going to want to make sure
+ // that we're writing the manifest file and that it contains the expected
+ // ManifestEntry objects, and that the staging folder was successfully
+ // renamed with the current date.
+ await IOUtils.remove(fakeProfilePath, { recursive: true });
+}
+
+/**
+ * Tests that calling BackupService.createBackup will call backup on each
+ * registered BackupResource, and that each BackupResource will have a folder
+ * created for them to write into. Tests in the signed-out state.
+ */
+add_task(async function test_createBackup_signed_out() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_NOT_CONFIGURED });
+ await testCreateBackupHelper(sandbox, manifest => {
+ Assert.equal(
+ manifest.meta.accountID,
+ undefined,
+ "Account ID should be undefined."
+ );
+ Assert.equal(
+ manifest.meta.accountEmail,
+ undefined,
+ "Account email should be undefined."
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that calling BackupService.createBackup will call backup on each
+ * registered BackupResource, and that each BackupResource will have a folder
+ * created for them to write into. Tests in the signed-in state.
+ */
+add_task(async function test_createBackup_signed_in() {
+ let sandbox = sinon.createSandbox();
+
+ const TEST_UID = "ThisIsMyTestUID";
+ const TEST_EMAIL = "foxy@mozilla.org";
+
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ uid: TEST_UID,
+ email: TEST_EMAIL,
+ });
+
+ await testCreateBackupHelper(sandbox, manifest => {
+ Assert.equal(
+ manifest.meta.accountID,
+ TEST_UID,
+ "Account ID should be set properly."
+ );
+ Assert.equal(
+ manifest.meta.accountEmail,
+ TEST_EMAIL,
+ "Account email should be set properly."
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Creates a directory that looks a lot like a decompressed backup archive,
+ * and then tests that BackupService.recoverFromBackup can create a new profile
+ * and recover into it.
+ */
+add_task(async function test_recoverFromBackup() {
+ let sandbox = sinon.createSandbox();
+ let fakeEntryMap = new Map();
+ let backupResourceClasses = [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ];
+
+ let i = 1;
+ for (let backupResourceClass of backupResourceClasses) {
+ let fakeManifestEntry = { [`fake${i}`]: `hello from backup - ${i}` };
+ sandbox
+ .stub(backupResourceClass.prototype, "backup")
+ .resolves(fakeManifestEntry);
+
+ let fakePostRecoveryEntry = { [`fake${i}`]: `hello from recover - ${i}` };
+ sandbox
+ .stub(backupResourceClass.prototype, "recover")
+ .resolves(fakePostRecoveryEntry);
+
+ fakeEntryMap.set(backupResourceClass, {
+ manifestEntry: fakeManifestEntry,
+ postRecoveryEntry: fakePostRecoveryEntry,
+ });
+
+ ++i;
+ }
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let oldProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "recoverFromBackupTest"
+ );
+ let newProfileRootPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "recoverFromBackupTest-newProfileRoot"
+ );
+
+ let { stagingPath } = await bs.createBackup({ profilePath: oldProfilePath });
+
+ let testTelemetryStateObject = {
+ clientID: "ed209123-04a1-04a1-04a1-c0ffeec0ffee",
+ };
+ await IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, "datareporting", "state.json"),
+ testTelemetryStateObject
+ );
+
+ let profile = await bs.recoverFromBackup(
+ stagingPath,
+ false /* shouldLaunch */,
+ newProfileRootPath
+ );
+ Assert.ok(profile, "An nsIToolkitProfile was created.");
+ let newProfilePath = profile.rootDir.path;
+
+ let postRecoveryFilePath = PathUtils.join(
+ newProfilePath,
+ "post-recovery.json"
+ );
+ let postRecovery = await IOUtils.readJSON(postRecoveryFilePath);
+
+ for (let backupResourceClass of backupResourceClasses) {
+ let expectedResourceFolder = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+
+ let { manifestEntry, postRecoveryEntry } =
+ fakeEntryMap.get(backupResourceClass);
+
+ Assert.ok(
+ backupResourceClass.prototype.recover.calledOnce,
+ `Recover was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.recover.calledWith(
+ manifestEntry,
+ expectedResourceFolder,
+ newProfilePath
+ ),
+ `Recover was passed the right arguments for ${backupResourceClass.key}`
+ );
+ Assert.deepEqual(
+ postRecoveryEntry,
+ postRecovery[backupResourceClass.key],
+ "The post recovery data is as expected"
+ );
+ }
+
+ let newProfileTelemetryStateObject = await IOUtils.readJSON(
+ PathUtils.join(newProfileRootPath, "datareporting", "state.json")
+ );
+ Assert.deepEqual(
+ testTelemetryStateObject,
+ newProfileTelemetryStateObject,
+ "Recovered profile inherited telemetry state from the profile that " +
+ "initiated recovery"
+ );
+
+ await IOUtils.remove(oldProfilePath, { recursive: true });
+ await IOUtils.remove(newProfileRootPath, { recursive: true });
+ sandbox.restore();
+});
+
+/**
+ * Tests that if there's a post-recovery.json file in the profile directory
+ * when checkForPostRecovery() is called, that it is processed, and the
+ * postRecovery methods on the associated BackupResources are called with the
+ * entry values from the file.
+ */
+add_task(async function test_checkForPostRecovery() {
+ let sandbox = sinon.createSandbox();
+
+ let testProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "checkForPostRecoveryTest"
+ );
+ let fakePostRecoveryObject = {
+ [FakeBackupResource1.key]: "test 1",
+ [FakeBackupResource3.key]: "test 3",
+ };
+ await IOUtils.writeJSON(
+ PathUtils.join(testProfilePath, BackupService.POST_RECOVERY_FILE_NAME),
+ fakePostRecoveryObject
+ );
+
+ sandbox.stub(FakeBackupResource1.prototype, "postRecovery").resolves();
+ sandbox.stub(FakeBackupResource2.prototype, "postRecovery").resolves();
+ sandbox.stub(FakeBackupResource3.prototype, "postRecovery").resolves();
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ await bs.checkForPostRecovery(testProfilePath);
+ await bs.postRecoveryComplete;
+
+ Assert.ok(
+ FakeBackupResource1.prototype.postRecovery.calledOnce,
+ "FakeBackupResource1.postRecovery was called once"
+ );
+ Assert.ok(
+ FakeBackupResource2.prototype.postRecovery.notCalled,
+ "FakeBackupResource2.postRecovery was not called"
+ );
+ Assert.ok(
+ FakeBackupResource3.prototype.postRecovery.calledOnce,
+ "FakeBackupResource3.postRecovery was called once"
+ );
+ Assert.ok(
+ FakeBackupResource1.prototype.postRecovery.calledWith(
+ fakePostRecoveryObject[FakeBackupResource1.key]
+ ),
+ "FakeBackupResource1.postRecovery was called with the expected argument"
+ );
+ Assert.ok(
+ FakeBackupResource3.prototype.postRecovery.calledWith(
+ fakePostRecoveryObject[FakeBackupResource3.key]
+ ),
+ "FakeBackupResource3.postRecovery was called with the expected argument"
+ );
+
+ await IOUtils.remove(testProfilePath, { recursive: true });
+ sandbox.restore();
+});