diff options
Diffstat (limited to 'browser/components/backup/tests/xpcshell')
-rw-r--r-- | browser/components/backup/tests/xpcshell/head.js | 167 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_BackupResource.js (renamed from browser/components/backup/tests/xpcshell/test_BrowserResource.js) | 26 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js | 113 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js | 226 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js | 132 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_createBackup.js | 74 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_measurements.js | 547 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/xpcshell.toml | 14 |
8 files changed, 1287 insertions, 12 deletions
diff --git a/browser/components/backup/tests/xpcshell/head.js b/browser/components/backup/tests/xpcshell/head.js new file mode 100644 index 0000000000..2402870a13 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/head.js @@ -0,0 +1,167 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { BackupService } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupService.sys.mjs" +); + +const { BackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupResource.sys.mjs" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const BYTES_IN_KB = 1000; + +do_get_profile(); + +/** + * Some fake backup resource classes to test with. + */ +class FakeBackupResource1 extends BackupResource { + static get key() { + return "fake1"; + } + static get requiresEncryption() { + return false; + } +} + +/** + * Another fake backup resource class to test with. + */ +class FakeBackupResource2 extends BackupResource { + static get key() { + return "fake2"; + } + static get requiresEncryption() { + return true; + } +} + +/** + * Yet another fake backup resource class to test with. + */ +class FakeBackupResource3 extends BackupResource { + static get key() { + return "fake3"; + } + static get requiresEncryption() { + return false; + } +} + +/** + * Create a file of a given size in kilobytes. + * + * @param {string} path the path where the file will be created. + * @param {number} sizeInKB size file in Kilobytes. + * @returns {Promise<undefined>} + */ +async function createKilobyteSizedFile(path, sizeInKB) { + let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB); + await IOUtils.write(path, bytes); +} + +/** + * @typedef {object} TestFileObject + * @property {(string|Array.<string>)} path + * The relative path of the file. It can be a string or an array of strings + * in the event that directories need to be created. For example, this is + * an array of valid TestFileObjects. + * + * [ + * { path: "file1.txt" }, + * { path: ["dir1", "file2.txt"] }, + * { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 }, + * { path: "file4.txt" }, + * ] + * + * @property {number} [sizeInKB=10] + * The size of the created file in kilobytes. Defaults to 10. + */ + +/** + * Easily creates a series of test files and directories under parentPath. + * + * @param {string} parentPath + * The path to the parent directory where the files will be created. + * @param {TestFileObject[]} testFilesArray + * An array of TestFileObjects describing what test files to create within + * the parentPath. + * @see TestFileObject + * @returns {Promise<undefined>} + */ +async function createTestFiles(parentPath, testFilesArray) { + for (let { path, sizeInKB } of testFilesArray) { + if (Array.isArray(path)) { + // Make a copy of the array of path elements, chopping off the last one. + // We'll assume the unchopped items are directories, and make sure they + // exist first. + let folders = path.slice(0, -1); + await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders)); + } + + if (sizeInKB === undefined) { + sizeInKB = 10; + } + + // This little piece of cleverness coerces a string into an array of one + // if path is a string, or just leaves it alone if it's already an array. + let filePath = PathUtils.join(parentPath, ...[].concat(path)); + await createKilobyteSizedFile(filePath, sizeInKB); + } +} + +/** + * Checks that files exist within a particular folder. The filesize is not + * checked. + * + * @param {string} parentPath + * The path to the parent directory where the files should exist. + * @param {TestFileObject[]} testFilesArray + * An array of TestFileObjects describing what test files to search for within + * parentPath. + * @see TestFileObject + * @returns {Promise<undefined>} + */ +async function assertFilesExist(parentPath, testFilesArray) { + for (let { path } of testFilesArray) { + let copiedFileName = PathUtils.join(parentPath, ...[].concat(path)); + Assert.ok( + await IOUtils.exists(copiedFileName), + `${copiedFileName} should exist in the staging folder` + ); + } +} + +/** + * Remove a file or directory at a path if it exists and files are unlocked. + * + * @param {string} path path to remove. + */ +async function maybeRemovePath(path) { + try { + await IOUtils.remove(path, { ignoreAbsent: true, recursive: true }); + } catch (error) { + // Sometimes remove() throws when the file is not unlocked soon + // enough. + if (error.name != "NS_ERROR_FILE_IS_LOCKED") { + // Ignoring any errors, as the temp folder will be cleaned up. + console.error(error); + } + } +} diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BackupResource.js index 23c8e077a5..6623f4cd77 100644 --- a/browser/components/backup/tests/xpcshell/test_BrowserResource.js +++ b/browser/components/backup/tests/xpcshell/test_BackupResource.js @@ -3,16 +3,12 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { BackupResource } = ChromeUtils.importESModule( +const { bytesToFuzzyKilobytes } = ChromeUtils.importESModule( "resource:///modules/backup/BackupResource.sys.mjs" ); const EXPECTED_KILOBYTES_FOR_XULSTORE = 1; -add_setup(() => { - do_get_profile(); -}); - /** * Tests that BackupService.getFileSize will get the size of a file in kilobytes. */ @@ -35,7 +31,7 @@ add_task(async function test_getFileSize() { }); /** - * Tests that BackupService.getFileSize will get the total size of all the files in a directory and it's children in kilobytes. + * Tests that BackupService.getDirectorySize will get the total size of all the files in a directory and it's children in kilobytes. */ add_task(async function test_getDirectorySize() { let file = do_get_file("data/test_xulstore.json"); @@ -61,3 +57,21 @@ add_task(async function test_getDirectorySize() { await IOUtils.remove(testDir, { recursive: true }); }); + +/** + * Tests that bytesToFuzzyKilobytes will convert bytes to kilobytes + * and round up to the nearest tenth kilobyte. + */ +add_task(async function test_bytesToFuzzyKilobytes() { + let largeSize = bytesToFuzzyKilobytes(1234000); + + Assert.equal( + largeSize, + 1230, + "1234 bytes is rounded up to the nearest tenth kilobyte, 1230" + ); + + let smallSize = bytesToFuzzyKilobytes(3); + + Assert.equal(smallSize, 1, "Sizes under 10 kilobytes return 1 kilobyte"); +}); diff --git a/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js new file mode 100644 index 0000000000..e57dd50cd3 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { MiscDataBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/MiscDataBackupResource.sys.mjs" +); + +/** + * Tests that we can measure miscellaneous files in the profile directory. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_MISC_KILOBYTES_SIZE = 241; + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-measurement-test" + ); + + const mockFiles = [ + { path: "times.json", sizeInKB: 5 }, + { path: "enumerate_devices.txt", sizeInKB: 1 }, + { path: "protections.sqlite", sizeInKB: 100 }, + { path: "SiteSecurityServiceState.bin", sizeInKB: 10 }, + { path: ["storage", "permanent", "chrome", "123ABC.sqlite"], sizeInKB: 40 }, + { path: ["storage", "permanent", "chrome", "456DEF.sqlite"], sizeInKB: 40 }, + { + path: ["storage", "permanent", "chrome", "mockIDBDir", "890HIJ.sqlite"], + sizeInKB: 40, + }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let miscDataBackupResource = new MiscDataBackupResource(); + await miscDataBackupResource.measure(tempDir); + + let measurement = Glean.browserBackup.miscDataSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.misc_data_size", + measurement, + "Glean and telemetry measurements for misc data should be equal" + ); + Assert.equal( + measurement, + EXPECTED_MISC_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for misc files" + ); + + await maybeRemovePath(tempDir); +}); + +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let miscDataBackupResource = new MiscDataBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-staging-test" + ); + + const simpleCopyFiles = [ + { path: "times.json" }, + { path: "enumerate_devices.txt" }, + { path: "SiteSecurityServiceState.bin" }, + ]; + await createTestFiles(sourcePath, simpleCopyFiles); + + // We have no need to test that Sqlite.sys.mjs's backup method is working - + // this is something that is tested in Sqlite's own tests. We can just make + // sure that it's being called using sinon. Unfortunately, we cannot do the + // same thing with IOUtils.copy, as its methods are not stubbable. + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await miscDataBackupResource.backup(stagingPath, sourcePath); + + await assertFilesExist(stagingPath, simpleCopyFiles); + + // Next, we'll make sure that the Sqlite connection had `backup` called on it + // with the right arguments. + Assert.ok( + fakeConnection.backup.calledOnce, + "Called backup the expected number of times for all connections" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "protections.sqlite") + ), + "Called backup on the protections.sqlite Sqlite connection" + ); + + // Bug 1890585 - we don't currently have the ability to copy the + // chrome-privileged IndexedDB databases under storage/permanent/chrome, so + // we'll just skip testing that for now. + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js new file mode 100644 index 0000000000..de97281372 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PlacesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/PlacesBackupResource.sys.mjs" +); +const { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +const HISTORY_ENABLED_PREF = "places.history.enabled"; +const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown"; + +registerCleanupFunction(() => { + /** + * Even though test_backup_no_saved_history clears user prefs too, + * clear them here as well in case that test fails and we don't + * reach the end of the test, which handles the cleanup. + */ + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that we can measure Places DB related files in the profile directory. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_PLACES_DB_SIZE = 5240; + const EXPECTED_FAVICONS_DB_SIZE = 5240; + + // Create resource files in temporary directory + const tempDir = PathUtils.tempDir; + let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite"); + let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite"); + await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE); + await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE); + + let placesBackupResource = new PlacesBackupResource(); + await placesBackupResource.measure(tempDir); + + let placesMeasurement = Glean.browserBackup.placesSize.testGetValue(); + let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.places_size", + placesMeasurement, + "Glean and telemetry measurements for places.sqlite should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.favicons_size", + faviconsMeasurement, + "Glean and telemetry measurements for favicons.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + placesMeasurement, + EXPECTED_PLACES_DB_SIZE, + "Should have collected the correct glean measurement for places.sqlite" + ); + Assert.equal( + faviconsMeasurement, + EXPECTED_FAVICONS_DB_SIZE, + "Should have collected the correct glean measurement for favicons.sqlite" + ); + + await maybeRemovePath(tempPlacesDBPath); + await maybeRemovePath(tempFaviconsDBPath); +}); + +/** + * Tests that the backup method correctly copies places.sqlite and + * favicons.sqlite from the profile directory into the staging directory. + */ +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.calledTwice, + "Backup should have been called twice" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "places.sqlite") + ), + "places.sqlite should have been backed up first" + ); + Assert.ok( + fakeConnection.backup.secondCall.calledWith( + PathUtils.join(stagingPath, "favicons.sqlite") + ), + "favicons.sqlite should have been backed up second" + ); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when users + * don't want history saved, even on shutdown. + */ +add_task(async function test_backup_no_saved_history() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + /** + * First verify that remember history pref alone affects backup file type for places, + * despite sanitize on shutdown pref value. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with remember history disabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + await IOUtils.remove(PathUtils.join(stagingPath, "bookmarks.jsonlz4")); + + /** + * Now verify that the sanitize shutdown pref alone affects backup file type for places, + * even if the user is okay with remembering history while browsing. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, true); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true); + + fakeConnection.backup.resetHistory(); + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with sanitize shutdown enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when + * permanent private browsing mode is enabled. + */ +add_task(async function test_backup_private_browsing() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with permanent private browsing enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js new file mode 100644 index 0000000000..6845431bb8 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PreferencesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/PreferencesBackupResource.sys.mjs" +); + +/** + * Test that the measure method correctly collects the disk-sizes of things that + * the PreferencesBackupResource is meant to back up. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_PREFERENCES_KILOBYTES_SIZE = 415; + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-measure-test" + ); + const mockFiles = [ + { path: "prefs.js", sizeInKB: 20 }, + { path: "xulstore.json", sizeInKB: 1 }, + { path: "permissions.sqlite", sizeInKB: 100 }, + { path: "content-prefs.sqlite", sizeInKB: 260 }, + { path: "containers.json", sizeInKB: 1 }, + { path: "handlers.json", sizeInKB: 1 }, + { path: "search.json.mozlz4", sizeInKB: 1 }, + { path: "user.js", sizeInKB: 2 }, + { path: ["chrome", "userChrome.css"], sizeInKB: 5 }, + { path: ["chrome", "userContent.css"], sizeInKB: 5 }, + { path: ["chrome", "css", "mockStyles.css"], sizeInKB: 5 }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let preferencesBackupResource = new PreferencesBackupResource(); + + await preferencesBackupResource.measure(tempDir); + + let measurement = Glean.browserBackup.preferencesSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.preferences_size", + measurement, + "Glean and telemetry measurements for preferences data should be equal" + ); + Assert.equal( + measurement, + EXPECTED_PREFERENCES_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for preferences files" + ); + + await maybeRemovePath(tempDir); +}); + +/** + * Test that the backup method correctly copies items from the profile directory + * into the staging directory. + */ +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let preferencesBackupResource = new PreferencesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-staging-test" + ); + + const simpleCopyFiles = [ + { path: "xulstore.json" }, + { path: "containers.json" }, + { path: "handlers.json" }, + { path: "search.json.mozlz4" }, + { path: "user.js" }, + { path: ["chrome", "userChrome.css"] }, + { path: ["chrome", "userContent.css"] }, + { path: ["chrome", "childFolder", "someOtherStylesheet.css"] }, + ]; + await createTestFiles(sourcePath, simpleCopyFiles); + + // We have no need to test that Sqlite.sys.mjs's backup method is working - + // this is something that is tested in Sqlite's own tests. We can just make + // sure that it's being called using sinon. Unfortunately, we cannot do the + // same thing with IOUtils.copy, as its methods are not stubbable. + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await preferencesBackupResource.backup(stagingPath, sourcePath); + + await assertFilesExist(stagingPath, simpleCopyFiles); + + // Next, we'll make sure that the Sqlite connection had `backup` called on it + // with the right arguments. + Assert.ok( + fakeConnection.backup.calledTwice, + "Called backup the expected number of times for all connections" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "permissions.sqlite") + ), + "Called backup on the permissions.sqlite Sqlite connection" + ); + Assert.ok( + fakeConnection.backup.secondCall.calledWith( + PathUtils.join(stagingPath, "content-prefs.sqlite") + ), + "Called backup on the content-prefs.sqlite Sqlite connection" + ); + + // And we'll make sure that preferences were properly written out. + Assert.ok( + await IOUtils.exists(PathUtils.join(stagingPath, "prefs.js")), + "prefs.js should exist in the staging folder" + ); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_createBackup.js b/browser/components/backup/tests/xpcshell/test_createBackup.js new file mode 100644 index 0000000000..fcace695ef --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_createBackup.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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. + */ +add_task(async function test_createBackup() { + let sandbox = sinon.createSandbox(); + sandbox + .stub(FakeBackupResource1.prototype, "backup") + .resolves({ fake1: "hello from 1" }); + sandbox + .stub(FakeBackupResource2.prototype, "backup") + .rejects(new Error("Some failure to backup")); + sandbox + .stub(FakeBackupResource3.prototype, "backup") + .resolves({ fake3: "hello from 3" }); + + let bs = new BackupService({ + FakeBackupResource1, + FakeBackupResource2, + FakeBackupResource3, + }); + + let fakeProfilePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "createBackupTest" + ); + + await bs.createBackup({ profilePath: fakeProfilePath }); + + // For now, we expect a staging folder to exist under the fakeProfilePath, + // and we should find a folder for each fake BackupResource. + let stagingPath = PathUtils.join(fakeProfilePath, "backups", "staging"); + Assert.ok(await IOUtils.exists(stagingPath), "Staging folder exists"); + + for (let backupResourceClass of [ + FakeBackupResource1, + FakeBackupResource2, + FakeBackupResource3, + ]) { + let expectedResourceFolder = PathUtils.join( + stagingPath, + backupResourceClass.key + ); + Assert.ok( + await IOUtils.exists(expectedResourceFolder), + `BackupResource staging folder exists for ${backupResourceClass.key}` + ); + Assert.ok( + backupResourceClass.prototype.backup.calledOnce, + `Backup was called for ${backupResourceClass.key}` + ); + Assert.ok( + backupResourceClass.prototype.backup.calledWith( + expectedResourceFolder, + fakeProfilePath + ), + `Backup was passed the right paths for ${backupResourceClass.key}` + ); + } + + // 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 }); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js index e5726126b2..0dece6b370 100644 --- a/browser/components/backup/tests/xpcshell/test_measurements.js +++ b/browser/components/backup/tests/xpcshell/test_measurements.js @@ -3,22 +3,59 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { BackupService } = ChromeUtils.importESModule( - "resource:///modules/backup/BackupService.sys.mjs" +const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs" +); +const { AddonsBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/AddonsBackupResource.sys.mjs" +); +const { CookiesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/CookiesBackupResource.sys.mjs" +); + +const { FormHistoryBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/FormHistoryBackupResource.sys.mjs" ); -const { TelemetryTestUtils } = ChromeUtils.importESModule( - "resource://testing-common/TelemetryTestUtils.sys.mjs" +const { SessionStoreBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/SessionStoreBackupResource.sys.mjs" ); add_setup(() => { - do_get_profile(); // FOG needs to be initialized in order for data to flow. Services.fog.initializeFOG(); Services.telemetry.clearScalars(); }); /** + * Tests that calling `BackupService.takeMeasurements` will call the measure + * method of all registered BackupResource classes. + */ +add_task(async function test_takeMeasurements() { + let sandbox = sinon.createSandbox(); + sandbox.stub(FakeBackupResource1.prototype, "measure").resolves(); + sandbox + .stub(FakeBackupResource2.prototype, "measure") + .rejects(new Error("Some failure to measure")); + + let bs = new BackupService({ FakeBackupResource1, FakeBackupResource2 }); + await bs.takeMeasurements(); + + for (let backupResourceClass of [FakeBackupResource1, FakeBackupResource2]) { + Assert.ok( + backupResourceClass.prototype.measure.calledOnce, + "Measure was called" + ); + Assert.ok( + backupResourceClass.prototype.measure.calledWith(PathUtils.profileDir), + "Measure was called with the profile directory argument" + ); + } + + sandbox.restore(); +}); + +/** * Tests that we can measure the disk space available in the profile directory. */ add_task(async function test_profDDiskSpace() { @@ -38,3 +75,503 @@ add_task(async function test_profDDiskSpace() { "device" ); }); + +/** + * Tests that we can measure credentials related files in the profile directory. + */ +add_task(async function test_credentialsAndSecurityBackupResource() { + Services.fog.testResetFOG(); + + const EXPECTED_CREDENTIALS_KILOBYTES_SIZE = 413; + const EXPECTED_SECURITY_KILOBYTES_SIZE = 231; + + // Create resource files in temporary directory + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "CredentialsAndSecurityBackupResource-measurement-test" + ); + + const mockFiles = [ + // Set up credentials files + { path: "key4.db", sizeInKB: 300 }, + { path: "logins.json", sizeInKB: 1 }, + { path: "logins-backup.json", sizeInKB: 1 }, + { path: "autofill-profiles.json", sizeInKB: 1 }, + { path: "credentialstate.sqlite", sizeInKB: 100 }, + { path: "signedInUser.json", sizeInKB: 5 }, + // Set up security files + { path: "cert9.db", sizeInKB: 230 }, + { path: "pkcs11.txt", sizeInKB: 1 }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let credentialsAndSecurityBackupResource = + new CredentialsAndSecurityBackupResource(); + await credentialsAndSecurityBackupResource.measure(tempDir); + + let credentialsMeasurement = + Glean.browserBackup.credentialsDataSize.testGetValue(); + let securityMeasurement = Glean.browserBackup.securityDataSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Credentials measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.credentials_data_size", + credentialsMeasurement, + "Glean and telemetry measurements for credentials data should be equal" + ); + + Assert.equal( + credentialsMeasurement, + EXPECTED_CREDENTIALS_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for credentials files" + ); + + // Security measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.security_data_size", + securityMeasurement, + "Glean and telemetry measurements for security data should be equal" + ); + Assert.equal( + securityMeasurement, + EXPECTED_SECURITY_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for security files" + ); + + // Cleanup + await maybeRemovePath(tempDir); +}); + +/** + * Tests that we can measure the Cookies db in a profile directory. + */ +add_task(async function test_cookiesBackupResource() { + const EXPECTED_COOKIES_DB_SIZE = 1230; + + Services.fog.testResetFOG(); + + // Create resource files in temporary directory + let tempDir = PathUtils.tempDir; + let tempCookiesDBPath = PathUtils.join(tempDir, "cookies.sqlite"); + await createKilobyteSizedFile(tempCookiesDBPath, EXPECTED_COOKIES_DB_SIZE); + + let cookiesBackupResource = new CookiesBackupResource(); + await cookiesBackupResource.measure(tempDir); + + let cookiesMeasurement = Glean.browserBackup.cookiesSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.cookies_size", + cookiesMeasurement, + "Glean and telemetry measurements for cookies.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + cookiesMeasurement, + EXPECTED_COOKIES_DB_SIZE, + "Should have collected the correct glean measurement for cookies.sqlite" + ); + + await maybeRemovePath(tempCookiesDBPath); +}); + +/** + * Tests that we can measure the Form History db in a profile directory. + */ +add_task(async function test_formHistoryBackupResource() { + const EXPECTED_FORM_HISTORY_DB_SIZE = 500; + + Services.fog.testResetFOG(); + + // Create resource files in temporary directory + let tempDir = PathUtils.tempDir; + let tempFormHistoryDBPath = PathUtils.join(tempDir, "formhistory.sqlite"); + await createKilobyteSizedFile( + tempFormHistoryDBPath, + EXPECTED_FORM_HISTORY_DB_SIZE + ); + + let formHistoryBackupResource = new FormHistoryBackupResource(); + await formHistoryBackupResource.measure(tempDir); + + let formHistoryMeasurement = + Glean.browserBackup.formHistorySize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.form_history_size", + formHistoryMeasurement, + "Glean and telemetry measurements for formhistory.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + formHistoryMeasurement, + EXPECTED_FORM_HISTORY_DB_SIZE, + "Should have collected the correct glean measurement for formhistory.sqlite" + ); + + await IOUtils.remove(tempFormHistoryDBPath); +}); + +/** + * Tests that we can measure the Session Store JSON and backups directory. + */ +add_task(async function test_sessionStoreBackupResource() { + const EXPECTED_KILOBYTES_FOR_BACKUPS_DIR = 1000; + Services.fog.testResetFOG(); + + // Create the sessionstore-backups directory. + let tempDir = PathUtils.tempDir; + let sessionStoreBackupsPath = PathUtils.join( + tempDir, + "sessionstore-backups", + "restore.jsonlz4" + ); + await createKilobyteSizedFile( + sessionStoreBackupsPath, + EXPECTED_KILOBYTES_FOR_BACKUPS_DIR + ); + + let sessionStoreBackupResource = new SessionStoreBackupResource(); + await sessionStoreBackupResource.measure(tempDir); + + let sessionStoreBackupsDirectoryMeasurement = + Glean.browserBackup.sessionStoreBackupsDirectorySize.testGetValue(); + let sessionStoreMeasurement = + Glean.browserBackup.sessionStoreSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.session_store_backups_directory_size", + sessionStoreBackupsDirectoryMeasurement, + "Glean and telemetry measurements for session store backups directory should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.session_store_size", + sessionStoreMeasurement, + "Glean and telemetry measurements for session store should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + sessionStoreBackupsDirectoryMeasurement, + EXPECTED_KILOBYTES_FOR_BACKUPS_DIR, + "Should have collected the correct glean measurement for the sessionstore-backups directory" + ); + + // Session store measurement is from `getCurrentState`, so exact size is unknown. + Assert.greater( + sessionStoreMeasurement, + 0, + "Should have collected a measurement for the session store" + ); + + await IOUtils.remove(sessionStoreBackupsPath); +}); + +/** + * Tests that we can measure the size of all the addons & extensions data. + */ +add_task(async function test_AddonsBackupResource() { + Services.fog.testResetFOG(); + Services.telemetry.clearScalars(); + + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON = 250; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE = 500; + const EXPECTED_KILOBYTES_FOR_STORAGE_SYNC = 50; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A = 600; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B = 400; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C = 150; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY = 1000; + const EXPECTED_KILOBYTES_FOR_EXTENSION_DATA = 100; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE = 200; + + let tempDir = PathUtils.tempDir; + + // Create extensions json files (all the same size). + const extensionsFilePath = PathUtils.join(tempDir, "extensions.json"); + await createKilobyteSizedFile( + extensionsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const extensionSettingsFilePath = PathUtils.join( + tempDir, + "extension-settings.json" + ); + await createKilobyteSizedFile( + extensionSettingsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const extensionsPrefsFilePath = PathUtils.join( + tempDir, + "extension-preferences.json" + ); + await createKilobyteSizedFile( + extensionsPrefsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const addonStartupFilePath = PathUtils.join(tempDir, "addonStartup.json.lz4"); + await createKilobyteSizedFile( + addonStartupFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + + // Create the extension store permissions data file. + let extensionStorePermissionsDataSize = PathUtils.join( + tempDir, + "extension-store-permissions", + "data.safe.bin" + ); + await createKilobyteSizedFile( + extensionStorePermissionsDataSize, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE + ); + + // Create the storage sync database file. + let storageSyncPath = PathUtils.join(tempDir, "storage-sync-v2.sqlite"); + await createKilobyteSizedFile( + storageSyncPath, + EXPECTED_KILOBYTES_FOR_STORAGE_SYNC + ); + + // Create the extensions directory with XPI files. + let extensionsXpiAPath = PathUtils.join( + tempDir, + "extensions", + "extension-b.xpi" + ); + let extensionsXpiBPath = PathUtils.join( + tempDir, + "extensions", + "extension-a.xpi" + ); + await createKilobyteSizedFile( + extensionsXpiAPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A + ); + await createKilobyteSizedFile( + extensionsXpiBPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B + ); + // Should be ignored. + let extensionsXpiStagedPath = PathUtils.join( + tempDir, + "extensions", + "staged", + "staged-test-extension.xpi" + ); + let extensionsXpiTrashPath = PathUtils.join( + tempDir, + "extensions", + "trash", + "trashed-test-extension.xpi" + ); + let extensionsXpiUnpackedPath = PathUtils.join( + tempDir, + "extensions", + "unpacked-extension.xpi", + "manifest.json" + ); + await createKilobyteSizedFile( + extensionsXpiStagedPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + await createKilobyteSizedFile( + extensionsXpiTrashPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + await createKilobyteSizedFile( + extensionsXpiUnpackedPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + + // Create the browser extension data directory. + let browserExtensionDataPath = PathUtils.join( + tempDir, + "browser-extension-data", + "test-file" + ); + await createKilobyteSizedFile( + browserExtensionDataPath, + EXPECTED_KILOBYTES_FOR_EXTENSION_DATA + ); + + // Create the extensions storage directory. + let extensionsStoragePath = PathUtils.join( + tempDir, + "storage", + "default", + "moz-extension+++test-extension-id", + "idb", + "data.sqlite" + ); + // Other storage files that should not be counted. + let otherStoragePath = PathUtils.join( + tempDir, + "storage", + "default", + "https+++accounts.firefox.com", + "ls", + "data.sqlite" + ); + + await createKilobyteSizedFile( + extensionsStoragePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE + ); + await createKilobyteSizedFile( + otherStoragePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE + ); + + // Measure all the extensions data. + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionsJsonSizeMeasurement = + Glean.browserBackup.extensionsJsonSize.testGetValue(); + Assert.equal( + extensionsJsonSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON * 4, // There are 4 equally sized files. + "Should have collected the correct measurement of the total size of all extensions JSON files" + ); + + let extensionStorePermissionsDataSizeMeasurement = + Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue(); + Assert.equal( + extensionStorePermissionsDataSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE, + "Should have collected the correct measurement of the size of the extension store permissions data" + ); + + let storageSyncSizeMeasurement = + Glean.browserBackup.storageSyncSize.testGetValue(); + Assert.equal( + storageSyncSizeMeasurement, + EXPECTED_KILOBYTES_FOR_STORAGE_SYNC, + "Should have collected the correct measurement of the size of the storage sync database" + ); + + let extensionsXpiDirectorySizeMeasurement = + Glean.browserBackup.extensionsXpiDirectorySize.testGetValue(); + Assert.equal( + extensionsXpiDirectorySizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY, + "Should have collected the correct measurement of the size 2 equally sized XPI files in the extensions directory" + ); + + let browserExtensionDataSizeMeasurement = + Glean.browserBackup.browserExtensionDataSize.testGetValue(); + Assert.equal( + browserExtensionDataSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSION_DATA, + "Should have collected the correct measurement of the size of the browser extension data directory" + ); + + let extensionsStorageSizeMeasurement = + Glean.browserBackup.extensionsStorageSize.testGetValue(); + Assert.equal( + extensionsStorageSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE, + "Should have collected the correct measurement of all the extensions storage" + ); + + // Compare glean vs telemetry measurements + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_json_size", + extensionsJsonSizeMeasurement, + "Glean and telemetry measurements for extensions JSON should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extension_store_permissions_data_size", + extensionStorePermissionsDataSizeMeasurement, + "Glean and telemetry measurements for extension store permissions data should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.storage_sync_size", + storageSyncSizeMeasurement, + "Glean and telemetry measurements for storage sync database should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_xpi_directory_size", + extensionsXpiDirectorySizeMeasurement, + "Glean and telemetry measurements for extensions directory should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.browser_extension_data_size", + browserExtensionDataSizeMeasurement, + "Glean and telemetry measurements for browser extension data should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_storage_size", + extensionsStorageSizeMeasurement, + "Glean and telemetry measurements for extensions storage should be equal" + ); + + await maybeRemovePath(tempDir); +}); + +/** + * Tests that we can handle the extension store permissions data not existing. + */ +add_task( + async function test_AddonsBackupResource_no_extension_store_permissions_data() { + Services.fog.testResetFOG(); + + let tempDir = PathUtils.tempDir; + + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionStorePermissionsDataSizeMeasurement = + Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue(); + Assert.equal( + extensionStorePermissionsDataSizeMeasurement, + null, + "Should NOT have collected a measurement for the missing data" + ); + } +); + +/** + * Tests that we can handle a profile with no moz-extension IndexedDB databases. + */ +add_task( + async function test_AddonsBackupResource_no_extension_storage_databases() { + Services.fog.testResetFOG(); + + let tempDir = PathUtils.tempDir; + + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionsStorageSizeMeasurement = + Glean.browserBackup.extensionsStorageSize.testGetValue(); + Assert.equal( + extensionsStorageSizeMeasurement, + null, + "Should NOT have collected a measurement for the missing data" + ); + } +); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml index fb6dcd6846..07e517f1f2 100644 --- a/browser/components/backup/tests/xpcshell/xpcshell.toml +++ b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -1,8 +1,20 @@ [DEFAULT] +head = "head.js" firefox-appdir = "browser" skip-if = ["os == 'android'"] +prefs = [ + "browser.backup.log=true", +] -["test_BrowserResource.js"] +["test_BackupResource.js"] support-files = ["data/test_xulstore.json"] +["test_MiscDataBackupResource.js"] + +["test_PlacesBackupResource.js"] + +["test_PreferencesBackupResource.js"] + +["test_createBackup.js"] + ["test_measurements.js"] |