Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
ChromeUtils.defineLazyGetter(this, "nsLocalFile", () =>
Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath")
);
const BACKUP_DIR_PREF_NAME = "browser.backup.location";
const TEST_PASSWORD = "correcthorsebatterystaple";
const kKnownMappings = Object.freeze({
OneDrPD: "onedrive",
Docs: "documents",
});
const gDirectoryServiceProvider = {
getFile(prop, persistent) {
persistent.value = false;
// We only expect a narrow range of calls.
let folder = gBase.clone();
if (prop === "ProfD") {
return folder;
}
if (prop in kKnownMappings) {
folder.append("dirsvc");
folder.append(prop + "-dir");
return folder;
}
console.error(`Access to unexpected directory '${prop}'`);
return Cr.NS_ERROR_FAILURE;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
};
let gBase;
add_setup(function setup() {
setupProfile();
gBase = do_get_profile();
Services.dirsvc
.QueryInterface(Ci.nsIDirectoryService)
.registerProvider(gDirectoryServiceProvider);
});
/**
* Gets a telemetry event and checks that it looks the same between Glean and
* legacy telemetry, i.e. that the extra data is equal.
*
* @param {string} name
* The Glean programming name of the event, e.g. turnOn instead of turn_on.
* @returns {object}
* The extra data associated with the event.
*/
function assertSingleTelemetryEvent(name) {
let value = Glean.browserBackup[name].testGetValue();
Assert.equal(value.length, 1, `${name} Glean event was recorded once.`);
let snakeName = name.replace(/([A-Z])/g, "_$1").toLowerCase();
let legacy = TelemetryTestUtils.getEvents(
{ category: "browser.backup", method: snakeName, object: "BackupService" },
{ process: "parent" }
);
Assert.equal(legacy.length, 1, `${name} legacy event was recorded once.`);
Assert.deepEqual(
legacy[0].extra,
value[0].extra,
"Legacy telemetry measured the same data as Glean."
);
return value[0].extra;
}
/**
* Checks that the recorded event's 'encrypted' and 'location' extra keys
* match `destPath` and `encrypted`. Reset telemetry before if needed!
*
* @param {string} name
* The name of the Glean event that should have been recorded.
* @param {string} destPath
* The path that the backup was stored to.
* @param {boolean} encrypted
* Whether the backup was encrypted or not.
*/
function assertEventMatches(name, destPath, encrypted) {
let extra = assertSingleTelemetryEvent(name);
Assert.equal(
extra.encrypted,
String(encrypted),
`Glean event indicates the backup is ${encrypted ? "" : "NOT "}encrypted.`
);
// This is returned from the mock of classifyLocationForTelemetry, and
// checks that the correct path was passed in.
Assert.equal(
extra.location,
`[classifying: ${relativeToProfile(destPath)}]`,
"Glean event has right location"
);
return extra;
}
/**
* Determines the path to 'source' from the profile directory to reduce the
* length and avoid truncation within legacy telemetry.
*
* @param {string} path
* The file that should be pointed to.
* @returns {string}
* The relative path from 'base' to 'source'.
*/
function relativeToProfile(path) {
let file = nsLocalFile(path);
return file.getRelativePath(gBase);
}
add_task(function test_relativeToProfile() {
// This aims to check that the direction is right.
const file = gBase.clone();
file.append("abc");
Assert.equal(
relativeToProfile(file.path),
"abc",
"relativeToProfile computes the right path."
);
});
add_task(async function test_created_encrypted_noreason() {
await template("testCreatedEncryptedNoReason", true, undefined);
});
add_task(async function test_created_nonencrypted_noreason() {
await template("testCreatedNonencryptedNoReason", false, undefined);
});
add_task(async function test_created_encrypted_with_reason() {
await template("testCreatedEncryptedWithReason", true, "I said so");
});
async function template(name, encrypted, reason) {
let bs = new BackupService();
let profilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
name
);
const backupDir = PathUtils.join(PathUtils.tempDir, name + "_dest");
Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
if (encrypted) {
await bs.enableEncryption(TEST_PASSWORD, profilePath);
}
sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
return `[classifying: ${relativeToProfile(file)}]`;
});
// To ensure that the backup_start event happens before the actual backup,
// take the lock for ourselves. Then we can unblock the backup once we've
// checked the telemetry is finished.
let resolver = Promise.withResolvers();
locks.request(BackupService.WRITE_BACKUP_LOCK_NAME, () => {
Services.fog.testResetFOG();
Services.telemetry.clearEvents();
let promise = bs.createBackup({ profilePath, reason });
let startedEvents = Glean.browserBackup.backupStart.testGetValue();
Assert.equal(
startedEvents.length,
1,
"Found the backup_start Glean event."
);
Assert.equal(
startedEvents[0].extra.reason,
reason ?? "unknown",
"Found the reason for starting the backup in the Glean event."
);
// Don't await on it, since createBackup needs the lock!
resolver.resolve(promise);
});
await resolver.promise;
let value = assertEventMatches("created", backupDir, encrypted);
// Not sure how big it is, and we're not testing the fuzzByteSize
// function, so just check that it's plausible.
Assert.greater(Number(value.size), 0, "Telemetry event has nonzero size");
}
add_task(async function test_toggleOn() {
let bs = new BackupService();
let backupDir = PathUtils.join(PathUtils.tempDir, "toggleOn_dest");
Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir);
let profilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"toggleOn"
);
if (bs.state.scheduledBackupsEnabled) {
// The test assumes that this is false. Do this before resetting telemetry
// so it doesn't affect the results.
bs.onUpdateScheduledBackups(false);
}
sinon.stub(bs, "classifyLocationForTelemetry").callsFake(file => {
return `[classifying: ${relativeToProfile(file)}]`;
});
Services.fog.testResetFOG();
Services.telemetry.clearEvents();
bs.onUpdateScheduledBackups(true);
assertEventMatches("toggleOn", backupDir, false);
Services.fog.testResetFOG();
Services.telemetry.clearEvents();
bs.onUpdateScheduledBackups(false);
assertSingleTelemetryEvent("toggleOff");
await bs.enableEncryption(TEST_PASSWORD, profilePath);
Services.fog.testResetFOG();
Services.telemetry.clearEvents();
bs.onUpdateScheduledBackups(true);
assertEventMatches("toggleOn", backupDir, true);
Services.fog.testResetFOG();
Services.telemetry.clearEvents();
bs.onUpdateScheduledBackups(false);
assertSingleTelemetryEvent("toggleOff");
});
add_task(async function test_classifyLocationForTelemetry() {
let bs = new BackupService();
for (const prop of Object.keys(kKnownMappings)) {
let file = Services.dirsvc.get(prop, Ci.nsIFile);
Assert.equal(
bs.classifyLocationForTelemetry(file.path),
"other",
`'${file.path}' was correctly classified.`
);
file.append("child");
Assert.equal(
bs.classifyLocationForTelemetry(file.path),
kKnownMappings[prop],
`'${file.path}' was correctly classified.`
);
file = file.parent.parent;
Assert.equal(
bs.classifyLocationForTelemetry(file.path),
"other",
`'${file.path}' was correctly classified.`
);
}
Assert.equal(
bs.classifyLocationForTelemetry(gBase.path),
"other",
"Unrelated path is not classified anywhere."
);
Assert.equal(
bs.classifyLocationForTelemetry("path"),
"Error: NS_ERROR_FILE_UNRECOGNIZED_PATH",
"Invalid path returns an error name."
);
});
add_task(async function test_idleDispatchPassesOptionsThrough() {
let bs = new BackupService();
let stub = sinon.stub(bs, "createBackupOnIdleDispatch").resolves();
let options = {};
bs.createBackupOnIdleDispatch(options);
Assert.equal(
stub.firstCall.args[0],
options,
"Options were passed as-is into createBackup."
);
});