Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

# 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 json
import os
import shutil
import tempfile
import mozfile
from marionette_harness import MarionetteTestCase
class BackupTest(MarionetteTestCase):
# This is the DB key that will be computed for the http2-ca.pem certificate
# that's included in a support-file for this test.
_cert_db_key = "AAAAAAAAAAAAAAAUAAAAG0Wbze8lahTcE4RhwEqMtTpThrzjMBkxFzAVBgNVBAMMDiBIVFRQMiBUZXN0IENB"
def setUp(self):
MarionetteTestCase.setUp(self)
# We need to quit the browser and restart with the browser.backup.log
# pref already set to true in order for it to be displayed.
self.marionette.quit()
self.marionette.instance.prefs = {
"browser.backup.log": True,
}
# Now restart the browser.
self.marionette.instance.switch_profile()
self.marionette.start_session()
def test_backup(self):
self.marionette.set_context("chrome")
self.add_test_cookie()
self.add_test_login()
self.add_test_certificate()
self.add_test_saved_address()
self.add_test_identity_credential()
self.add_test_form_history()
self.add_test_activity_stream_snippets_data()
self.add_test_protections_data()
self.add_test_bookmarks()
self.add_test_history()
self.add_test_preferences()
self.add_test_permissions()
resourceKeys = self.marionette.execute_script(
"""
const DefaultBackupResources = ChromeUtils.importESModule("resource:///modules/backup/BackupResources.sys.mjs");
let resourceKeys = [];
for (const resourceName in DefaultBackupResources) {
let resource = DefaultBackupResources[resourceName];
resourceKeys.push(resource.key);
}
return resourceKeys;
"""
)
originalStagingPath = self.marionette.execute_async_script(
"""
const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
let bs = BackupService.init();
if (!bs) {
throw new Error("Could not get initialized BackupService.");
}
let [outerResolve] = arguments;
(async () => {
let { stagingPath } = await bs.createBackup();
if (!stagingPath) {
throw new Error("Could not create backup.");
}
return stagingPath;
})().then(outerResolve);
"""
)
# When we switch over to the recovered profile, the Marionette framework
# will blow away the profile directory of the one that we created the
# backup on, which ruins our ability to do postRecovery work, since
# that relies on the prior profile sticking around. We work around this
# by moving the staging folder we got back to the OS temporary
# directory, and telling the recovery method to use that instead of the
# one from the profile directory.
stagingPath = os.path.join(tempfile.gettempdir(), "staging-test")
# Delete the destination folder if it exists already
shutil.rmtree(stagingPath, ignore_errors=True)
shutil.move(originalStagingPath, stagingPath)
# First, ensure that the staging path exists
self.assertTrue(os.path.exists(stagingPath))
# Now, ensure that the backup-manifest.json file exists within it.
manifestPath = os.path.join(stagingPath, "backup-manifest.json")
self.assertTrue(os.path.exists(manifestPath))
# For now, we just do a cursory check to ensure that for the resources
# that are listed in the manifest as having been backed up, that we
# have at least one file in their respective staging directories.
# We don't check the contents of the files, just that they exist.
# Read the JSON manifest file
with open(manifestPath, "r") as f:
manifest = json.load(f)
# Ensure that the manifest has a "resources" key
self.assertIn("resources", manifest)
resources = manifest["resources"]
self.assertTrue(isinstance(resources, dict))
self.assertTrue(len(resources) > 0)
# We don't have encryption capabilities wired up yet, so we'll check
# that all default resources are represented in the manifest.
self.assertEqual(len(resources), len(resourceKeys))
for resourceKey in resourceKeys:
self.assertIn(resourceKey, resources)
# Iterate the resources dict keys
for resourceKey in resources:
print("Checking resource: %s" % resourceKey)
# Ensure that there are staging directories created for each
# resource that was backed up
resourceStagingDir = os.path.join(stagingPath, resourceKey)
self.assertTrue(os.path.exists(resourceStagingDir))
# Start a brand new profile, one without any of the data we created or
# backed up. This is the one that we'll be starting recovery from.
self.marionette.quit()
self.marionette.instance.profile = None
self.marionette.start_session()
self.marionette.set_context("chrome")
# Recover the created backup into a new profile directory. Also get out
# the client ID of this profile, because we're going to want to make
# sure that this client ID is inherited by the recovered profile.
[
newProfileName,
newProfilePath,
expectedClientID,
] = self.marionette.execute_async_script(
"""
const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
let bs = BackupService.get();
if (!bs) {
throw new Error("Could not get initialized BackupService.");
}
let [stagingPath, outerResolve] = arguments;
(async () => {
let newProfileRootPath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"recoverFromBackupTest-newProfileRoot"
);
let newProfile = await bs.recoverFromBackup(stagingPath, false, newProfileRootPath)
if (!newProfile) {
throw new Error("Could not create recovery profile.");
}
let expectedClientID = await ClientID.getClientID();
return [newProfile.name, newProfile.rootDir.path, expectedClientID];
})().then(outerResolve);
""",
script_args=[stagingPath],
)
print("Recovery name: %s" % newProfileName)
print("Recovery path: %s" % newProfilePath)
print("Expected clientID: %s" % expectedClientID)
self.marionette.quit()
originalProfile = self.marionette.instance.profile
self.marionette.instance.profile = newProfilePath
self.marionette.start_session()
self.marionette.set_context("chrome")
# Ensure that all postRecovery actions have completed.
self.marionette.execute_async_script(
"""
const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
let bs = BackupService.get();
if (!bs) {
throw new Error("Could not get initialized BackupService.");
}
let [outerResolve] = arguments;
(async () => {
await bs.postRecoveryComplete;
})().then(outerResolve);
"""
)
self.verify_recovered_test_cookie()
self.verify_recovered_test_login()
self.verify_recovered_test_certificate()
self.verify_recovered_saved_address()
self.verify_recovered_identity_credential()
self.verify_recovered_form_history()
self.verify_recovered_activity_stream_snippets_data()
self.verify_recovered_protections_data()
self.verify_recovered_bookmarks()
self.verify_recovered_history()
self.verify_recovered_preferences()
self.verify_recovered_permissions()
# Now also ensure that the recovered profile inherited the client ID
# from the profile that initiated recovery.
recoveredClientID = self.marionette.execute_async_script(
"""
const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
let [outerResolve] = arguments;
(async () => {
return ClientID.getClientID();
})().then(outerResolve);
"""
)
self.assertEqual(recoveredClientID, expectedClientID)
# Try not to pollute the profile list by getting rid of the one we just
# created.
self.marionette.quit()
self.marionette.instance.profile = originalProfile
self.marionette.start_session()
self.marionette.set_context("chrome")
self.marionette.execute_script(
"""
let newProfileName = arguments[0];
let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
let profile = profileSvc.getProfileByName(newProfileName);
profile.remove(true);
profileSvc.flush();
""",
script_args=[newProfileName],
)
# Cleanup the staging path that we moved
mozfile.remove(stagingPath)
def add_test_cookie(self):
self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
// We'll just add a single cookie, and then make sure that it shows
// up on the other side.
Services.cookies.removeAll();
Services.cookies.add(
".example.com",
"/",
"first",
"one",
false,
false,
false,
Date.now() / 1000 + 1,
{},
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SCHEME_HTTP
);
})().then(outerResolve);
"""
)
def verify_recovered_test_cookie(self):
cookiesLength = self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
let cookies = Services.cookies.getCookiesFromHost("example.com", {});
return cookies.length;
})().then(outerResolve);
"""
)
self.assertEqual(cookiesLength, 1)
def add_test_login(self):
self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
// Let's start with adding a single password
Services.logins.removeAllLogins();
const nsLoginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo,
"init"
);
const login1 = new nsLoginInfo(
null,
"notifyu1",
"notifyp1",
"user",
"pass"
);
await Services.logins.addLoginAsync(login1);
})().then(outerResolve);
"""
)
def verify_recovered_test_login(self):
loginsLength = self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
let logins = await Services.logins.searchLoginsAsync({
origin: "https://example.com",
});
return logins.length;
})().then(outerResolve);
"""
)
self.assertEqual(loginsLength, 1)
def add_test_certificate(self):
certPath = os.path.join(os.path.dirname(__file__), "http2-ca.pem")
self.marionette.execute_async_script(
"""
let [certPath, certDbKey, outerResolve] = arguments;
(async () => {
const { NetUtil } = ChromeUtils.importESModule(
);
let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
if (certDb.findCertByDBKey(certDbKey)) {
throw new Error("Should not have this certificate yet!");
}
let certFile = await IOUtils.getFile(certPath);
let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
Ci.nsIFileInputStream
);
fstream.init(certFile, -1, 0, 0);
let data = NetUtil.readInputStreamToString(fstream, fstream.available());
fstream.close();
let pem = data.replace(/-----BEGIN CERTIFICATE-----/, "")
.replace(/-----END CERTIFICATE-----/, "")
.replace(/[\\r\\n]/g, "");
let cert = certDb.addCertFromBase64(pem, "CTu,u,u");
if (cert.dbKey != certDbKey) {
throw new Error("The inserted certificate DB key is unexpected.");
}
})().then(outerResolve);
""",
script_args=[certPath, self._cert_db_key],
)
def verify_recovered_test_certificate(self):
certExists = self.marionette.execute_async_script(
"""
let [certDbKey, outerResolve] = arguments;
(async () => {
let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
return certDb.findCertByDBKey(certDbKey) != null;
})().then(outerResolve);
""",
script_args=[self._cert_db_key],
)
self.assertTrue(certExists)
def add_test_saved_address(self):
self.marionette.execute_async_script(
"""
const { formAutofillStorage } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
const TEST_ADDRESS_1 = {
"given-name": "John",
"additional-name": "R.",
"family-name": "Smith",
organization: "World Wide Web Consortium",
"street-address": "32 Vassar Street\\\nMIT Room 32-G524",
"address-level2": "Cambridge",
"address-level1": "MA",
"postal-code": "02139",
country: "US",
tel: "+15195555555",
email: "user@example.com",
};
await formAutofillStorage.initialize();
formAutofillStorage.addresses.removeAll();
await formAutofillStorage.addresses.add(TEST_ADDRESS_1);
})().then(outerResolve);
"""
)
def verify_recovered_saved_address(self):
addressesLength = self.marionette.execute_async_script(
"""
const { formAutofillStorage } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
await formAutofillStorage.initialize();
let addresses = await formAutofillStorage.addresses.getAll();
return addresses.length;
})().then(outerResolve);
"""
)
self.assertEqual(addressesLength, 1)
def add_test_identity_credential(self):
self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
.getService(Ci.nsIIdentityCredentialStorageService);
service.clear();
let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI("https://test.com/"),
{}
);
let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI("https://idp-test.com/"),
{}
);
service.setState(
testPrincipal,
idpPrincipal,
"ID",
true,
true
);
})().then(outerResolve);
"""
)
def verify_recovered_identity_credential(self):
[registered, allowLogout] = self.marionette.execute_async_script(
"""
let [outerResolve] = arguments;
(async () => {
let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
.getService(Ci.nsIIdentityCredentialStorageService);
let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI("https://test.com/"),
{}
);
let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI("https://idp-test.com/"),
{}
);
let registered = {};
let allowLogout = {};
service.getState(
testPrincipal,
idpPrincipal,
"ID",
registered,
allowLogout
);
return [registered.value, allowLogout.value];
})().then(outerResolve);
"""
)
self.assertTrue(registered)
self.assertTrue(allowLogout)
def add_test_form_history(self):
self.marionette.execute_async_script(
"""
const { FormHistory } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
await FormHistory.update({
op: "add",
fieldname: "some-test-field",
value: "I was recovered!",
timesUsed: 1,
firstUsed: 0,
lastUsed: 0,
});
})().then(outerResolve);
"""
)
def verify_recovered_form_history(self):
formHistoryResultsLength = self.marionette.execute_async_script(
"""
const { FormHistory } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
let results = await FormHistory.search(
["guid"],
{ fieldname: "some-test-field" }
);
return results.length;
})().then(outerResolve);
"""
)
self.assertEqual(formHistoryResultsLength, 1)
def add_test_activity_stream_snippets_data(self):
self.marionette.execute_async_script(
"""
const { ActivityStreamStorage } = ChromeUtils.importESModule(
);
const SNIPPETS_TABLE_NAME = "snippets";
let [outerResolve] = arguments;
(async () => {
let storage = new ActivityStreamStorage({
storeNames: [SNIPPETS_TABLE_NAME],
});
let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
await snippetsTable.set("backup-test", "some-test-value");
})().then(outerResolve);
"""
)
def verify_recovered_activity_stream_snippets_data(self):
snippetsResult = self.marionette.execute_async_script(
"""
const { ActivityStreamStorage } = ChromeUtils.importESModule(
);
const SNIPPETS_TABLE_NAME = "snippets";
let [outerResolve] = arguments;
(async () => {
let storage = new ActivityStreamStorage({
storeNames: [SNIPPETS_TABLE_NAME],
});
let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
return await snippetsTable.get("backup-test");
})().then(outerResolve);
"""
)
self.assertEqual(snippetsResult, "some-test-value")
def add_test_protections_data(self):
self.marionette.execute_async_script(
"""
const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
.getService(Ci.nsITrackingDBService);
let [outerResolve] = arguments;
(async () => {
let entry = {
[Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
],
};
await TrackingDBService.clearAll();
await TrackingDBService.saveEvents(JSON.stringify(entry));
})().then(outerResolve);
"""
)
def verify_recovered_protections_data(self):
eventsSum = self.marionette.execute_async_script(
"""
const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
.getService(Ci.nsITrackingDBService);
let [outerResolve] = arguments;
(async () => {
return TrackingDBService.sumAllEvents();
})().then(outerResolve);
"""
)
self.assertEqual(eventsSum, 1)
def add_test_bookmarks(self):
self.marionette.execute_async_script(
"""
const { PlacesUtils } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
await PlacesUtils.bookmarks.eraseEverything();
await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
title: "Some test page",
url: Services.io.newURI("https://www.backup.test/"),
});
})().then(outerResolve);
"""
)
def verify_recovered_bookmarks(self):
bookmarkExists = self.marionette.execute_async_script(
"""
const { PlacesUtils } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
let url = Services.io.newURI("https://www.backup.test/");
let bookmark = await PlacesUtils.bookmarks.fetch({ url });
return bookmark != null;
})().then(outerResolve);
"""
)
self.assertTrue(bookmarkExists)
def add_test_history(self):
self.marionette.execute_async_script(
"""
const { PlacesUtils } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
await PlacesUtils.history.clear();
let entry = {
visits: [{ transition: PlacesUtils.history.TRANSITION_LINK }],
};
await PlacesUtils.history.insertMany([entry]);
})().then(outerResolve);
"""
)
def verify_recovered_history(self):
historyExists = self.marionette.execute_async_script(
"""
const { PlacesUtils } = ChromeUtils.importESModule(
);
let [outerResolve] = arguments;
(async () => {
let entry = await PlacesUtils.history.fetch("http://my-restored-history.com");
return entry != null;
})().then(outerResolve);
"""
)
self.assertTrue(historyExists)
def add_test_preferences(self):
self.marionette.execute_script(
"""
Services.prefs.setBoolPref("test-pref-for-backup", true)
"""
)
def verify_recovered_preferences(self):
prefExists = self.marionette.execute_script(
"""
return Services.prefs.getBoolPref("test-pref-for-backup", false);
"""
)
self.assertTrue(prefExists)
def add_test_permissions(self):
self.marionette.execute_script(
"""
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
);
Services.perms.addFromPrincipal(
principal,
"desktop-notification",
Services.perms.ALLOW_ACTION
);
"""
)
def verify_recovered_permissions(self):
permissionExists = self.marionette.execute_script(
"""
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
);
let perms = Services.perms.getAllForPrincipal(principal);
if (perms.length != 1) {
throw new Error("Got an unexpected number of permissions");
}
return perms[0].type == "desktop-notification"
"""
)
self.assertTrue(permissionExists)