Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
*
* Tests the AS RustLogins write-only mirror
*/
("use strict");
const { LoginManagerRustStorage } = ChromeUtils.importESModule(
"resource://gre/modules/storage-rust.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
);
/**
* Tests addLogin gets synced to Rust Storage
*/
add_task(async function test_mirror_addLogin() {
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
const loginInfo = LoginTestUtils.testData.formLogin({
username: "username",
password: "password",
});
await Services.logins.addLoginAsync(loginInfo);
// note LoginManagerRustStorage is a singleton and already initialized when
// Services.logins gets initialized.
const rustStorage = new LoginManagerRustStorage();
const storedLoginInfos = await Services.logins.getAllLogins();
const rustStoredLoginInfos = await rustStorage.getAllLogins();
LoginTestUtils.assertLoginListsEqual(storedLoginInfos, rustStoredLoginInfos);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Tests modifyLogin gets synced to Rust Storage
*/
add_task(async function test_mirror_modifyLogin() {
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
const loginInfo = LoginTestUtils.testData.formLogin({
username: "username",
password: "password",
});
await Services.logins.addLoginAsync(loginInfo);
const rustStorage = new LoginManagerRustStorage();
const [storedLoginInfo] = await Services.logins.getAllLogins();
const modifiedLoginInfo = LoginTestUtils.testData.formLogin({
username: "username",
password: "password",
usernameField: "new_form_field_username",
passwordField: "new_form_field_password",
});
Services.logins.modifyLogin(storedLoginInfo, modifiedLoginInfo);
const [storedModifiedLoginInfo] = await Services.logins.getAllLogins();
const [rustStoredModifiedLoginInfo] = await rustStorage.searchLoginsAsync({
guid: storedLoginInfo.guid,
});
LoginTestUtils.assertLoginListsEqual(
[storedModifiedLoginInfo],
[rustStoredModifiedLoginInfo]
);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Tests removeLogin gets synced to Rust Storage
*/
add_task(async function test_mirror_removeLogin() {
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
const loginInfo = LoginTestUtils.testData.formLogin({
username: "username",
password: "password",
});
await Services.logins.addLoginAsync(loginInfo);
const rustStorage = new LoginManagerRustStorage();
const [storedLoginInfo] = await Services.logins.getAllLogins();
Services.logins.removeLogin(storedLoginInfo);
const allLogins = await rustStorage.getAllLogins();
Assert.equal(allLogins.length, 0);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Verifies that the migration is triggered by according pref change
*/
add_task(async function test_migration_is_triggered_by_pref_change() {
// enable rust mirror, triggering migration
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
Assert.equal(
Services.prefs.getBoolPref("signon.rustMirror.migrationNeeded", false),
true,
"migrationNeeded is set to true"
);
const prefChangePromise = TestUtils.waitForPrefChange(
"signon.rustMirror.migrationNeeded"
);
// enable rust mirror, triggering migration
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
await prefChangePromise;
Assert.equal(
Services.prefs.getBoolPref("signon.rustMirror.migrationNeeded", false),
false,
"migrationNeeded is set to false"
);
await SpecialPowers.flushPrefEnv();
});
/**
* Verifies that the migration is idempotent by ensuring that running
* it multiple times does not create duplicate logins in the Rust store.
*/
add_task(async function test_migration_is_idempotent() {
// ensure mirror is on
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
const login = LoginTestUtils.testData.formLogin({
username: "test-user",
password: "secure-password",
});
await Services.logins.addLoginAsync(login);
const rustStorage = new LoginManagerRustStorage();
let rustLogins = await rustStorage.getAllLogins();
Assert.equal(
rustLogins.length,
1,
"Rust store contains login after first migration"
);
// trigger again
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
// using the migrationNeeded pref change as an indicator that the migration did run
const prefChangePromise = TestUtils.waitForPrefChange(
"signon.rustMirror.migrationNeeded"
);
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
await prefChangePromise;
rustLogins = await rustStorage.getAllLogins();
Assert.equal(rustLogins.length, 1, "No duplicate after second migration");
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Verify that the migration:
* - continues when some rows fail (partial failure),
* - still migrates valid logins,
*/
add_task(async function test_migration_partial_failure() {
// ensure mirror is off
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
const rustStorage = new LoginManagerRustStorage();
// Save the first (valid) login into Rust for real, then simulate results
sinon.stub(rustStorage, "addLoginsAsync").callsFake(async (logins, _cont) => {
await rustStorage.addWithMeta(logins[0]);
return [
{ login: {}, error: null }, // row 0 success
{ login: null, error: { message: "row failed" } }, // row 1 failure
];
});
const login_ok = LoginTestUtils.testData.formLogin({
username: "test-user-ok",
password: "secure-password",
});
await Services.logins.addLoginAsync(login_ok);
const login_bad = LoginTestUtils.testData.formLogin({
username: "test-user-bad",
password: "secure-password",
});
await Services.logins.addLoginAsync(login_bad);
// trigger again
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
// and wait a little, due to the lack of a migration-complete event.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 200));
const rustLogins = await rustStorage.getAllLogins();
Assert.equal(rustLogins.length, 1, "only valid login migrated");
sinon.restore();
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Verify that when the bulk add operation rejects (hard failure),
* the migration itself rejects.
*/
add_task(async function test_migration_rejects_when_bulk_add_rejects() {
// turn mirror off
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
const rustStorage = new LoginManagerRustStorage();
// force the bulk add to fail
sinon.stub(rustStorage, "addLoginsAsync").rejects(new Error("bulk failed"));
const login = LoginTestUtils.testData.formLogin({
username: "test-user",
password: "secure-password",
});
await Services.logins.addLoginAsync(login);
// trigger again
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
// and wait a little, due to the lack of a migration-complete event.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 200));
const rustLogins = await rustStorage.getAllLogins();
Assert.equal(rustLogins.length, 0, "zero logins migrated");
const newPrefValue = Services.prefs.getBoolPref(
"signon.rustMirror.migrationNeeded",
false
);
Assert.equal(newPrefValue, true, "pref has not been reset");
sinon.restore();
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Tests that rust_migration_failure events are recorded
* when a migration run encounters entry errors.
*/
add_task(async function test_rust_migration_failure_event() {
// ensure mirror is off first
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
Services.fog.testResetFOG();
const rustStorage = new LoginManagerRustStorage();
// Stub addLoginsAsync to simulate a failure for one entry
sinon
.stub(rustStorage, "addLoginsAsync")
.callsFake(async (_logins, _cont) => {
return [
{ login: {}, error: null }, // success
{ login: null, error: { message: "simulated migration failure" } }, // failure
];
});
// Add two logins to JSON so migration has something to work on
const login_ok = LoginTestUtils.testData.formLogin({
username: "ok-user",
password: "secure-password",
});
await Services.logins.addLoginAsync(login_ok);
const login_bad = LoginTestUtils.testData.formLogin({
username: "bad-user",
password: "secure-password",
});
await Services.logins.addLoginAsync(login_bad);
// Trigger migration
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustMigrationFailure.testGetValue()?.length == 1,
"event has been emitted"
);
const [evt] = Glean.pwmgr.rustMigrationFailure.testGetValue();
Assert.ok(evt.extra?.run_id, "event has a run_id");
Assert.equal(
evt.extra?.error_message,
"simulated migration failure",
"event has the expected error message"
);
Assert.equal(evt.name, "rust_migration_failure", "event has correct name");
sinon.restore();
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Ensures that migrating a large number of logins (100) from the JSON store to
* the Rust store completes within a reasonable time frame (under 1 second).
**/
add_task(async function test_migration_time_under_threshold() {
// ensure mirror is off
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
const numberOfLogins = 100;
const logins = Array.from({ length: numberOfLogins }, (_, i) =>
LoginTestUtils.testData.formLogin({
origin: `https://www${i}.example.com`,
username: `user${i}`,
})
);
await Services.logins.addLogins(logins);
await LoginTestUtils.reloadData();
const rustStorage = new LoginManagerRustStorage();
const start = Date.now();
// using the migrationNeeded pref change as an indicator that the migration did run
const prefChangePromise = TestUtils.waitForPrefChange(
"signon.rustMirror.migrationNeeded"
);
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
await prefChangePromise;
const duration = Date.now() - start;
Assert.less(duration, 2000, "Migration should complete under 2s");
Assert.equal(rustStorage.countLogins("", "", ""), numberOfLogins);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/*
* Tests that an error is logged when adding an invalid login to the Rust store.
* The Rust store is stricter than the JSON store and rejects some formats,
* such as single-dot origins.
*/
add_task(async function test_rust_mirror_addLogin_failure() {
// ensure mirror is on, and reset poisoned flag
await SpecialPowers.pushPrefEnv({
set: [
["signon.rustMirror.enabled", true],
["signon.rustMirror.poisoned", false],
],
});
Services.fog.testResetFOG();
// This login will be accepted by JSON but rejected by Rust
const badLogin = LoginTestUtils.testData.formLogin({
origin: ".",
passwordField: ".",
});
await Services.logins.addLoginAsync(badLogin);
const allLoginsJson = await Services.logins.getAllLogins();
Assert.equal(
allLoginsJson.length,
1,
"single dot origin login saved to JSON"
);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 1,
"event has been emitted"
);
const rustStorage = new LoginManagerRustStorage();
const allLogins = await rustStorage.getAllLogins();
Assert.equal(
allLogins.length,
0,
"single dot origin login not saved to Rust"
);
const [evt] = Glean.pwmgr.rustMirrorStatus.testGetValue();
Assert.ok(evt, "event has been emitted");
Assert.equal(evt.extra?.operation, "add", "event has operation");
Assert.equal(evt.extra?.status, "failure", "event has status=failure");
Assert.equal(
evt.extra?.error_message,
"Invalid login: Login has illegal origin",
"event has error_message"
);
Assert.equal(evt.extra?.poisoned, "false", "event is not poisoned");
Assert.equal(evt.name, "rust_mirror_status", "event has name");
// produce another failure
const badLogin2 = LoginTestUtils.testData.formLogin({
username: "another-bad-login",
origin: ".",
passwordField: ".",
});
await Services.logins.addLoginAsync(badLogin2);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustMirrorStatus.testGetValue()?.length == 2,
"two events have been emitted"
);
// eslint-disable-next-line no-unused-vars
const [_, evt2] = Glean.pwmgr.rustMirrorStatus.testGetValue();
Assert.equal(evt2.extra?.poisoned, "true", "event is poisoned now");
LoginTestUtils.clearData();
await SpecialPowers.flushPrefEnv();
});
/*
* Tests that we collect telemetry if non-ASCII origins get punycoded.
*/
add_task(async function test_punycode_origin_metric() {
// ensure mirror is on
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
Services.fog.testResetFOG();
const punicodeOrigin = "https://münich.example.com";
const login = LoginTestUtils.testData.formLogin({
origin: punicodeOrigin,
formActionOrigin: "https://example.com",
username: "user1",
password: "pass1",
});
await Services.logins.addLoginAsync(login);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1,
"event has been emitted"
);
const rustStorage = new LoginManagerRustStorage();
const allLogins = await rustStorage.getAllLogins();
Assert.equal(allLogins.length, 1, "punicode origin login saved to Rust");
const [rustLogin] = allLogins;
Assert.equal(
rustLogin.origin,
"origin has been punicoded on the Rust side"
);
const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue();
Assert.equal(evt.extra?.issue, "nonAsciiOrigin");
Assert.equal(evt.extra?.operation, "add");
Assert.ok("run_id" in evt.extra);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/*
* Tests that we collect telemetry if non-ASCII formorigins get punycoded.
*/
add_task(async function test_punycode_formActionOrigin_metric() {
// ensure mirror is on
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
Services.fog.testResetFOG();
const punicodeOrigin = "https://münich.example.com";
const login = LoginTestUtils.testData.formLogin({
formActionOrigin: punicodeOrigin,
origin: "https://example.com",
username: "user1",
password: "pass1",
});
await Services.logins.addLoginAsync(login);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1,
"event has been emitted"
);
const rustStorage = new LoginManagerRustStorage();
const allLogins = await rustStorage.getAllLogins();
Assert.equal(allLogins.length, 1, "punicode origin login saved to Rust");
const [rustLogin] = allLogins;
Assert.equal(
rustLogin.formActionOrigin,
"origin has been punicoded on the Rust side"
);
const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue();
Assert.equal(evt.extra?.issue, "nonAsciiFormAction");
Assert.equal(evt.extra?.operation, "add");
Assert.ok("run_id" in evt.extra);
LoginTestUtils.clearData();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/*
* Tests that we collect telemetry for single dot in origin
*/
add_task(async function test_single_dot_in_origin() {
// ensure mirror is on
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
Services.fog.testResetFOG();
const badOrigin = ".";
const login = LoginTestUtils.testData.formLogin({
origin: badOrigin,
formActionOrigin: "https://example.com",
username: "user1",
password: "pass1",
});
await Services.logins.addLoginAsync(login);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1,
"event has been emitted"
);
const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue();
Assert.equal(evt.extra?.issue, "dotOrigin");
Assert.equal(evt.extra?.operation, "add");
Assert.ok("run_id" in evt.extra);
LoginTestUtils.clearData();
await SpecialPowers.flushPrefEnv();
});
/*
* Tests that we collect telemetry if the username contains line breaks.
*/
add_task(async function test_username_linebreak_metric() {
// ensure mirror is on
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
Services.fog.testResetFOG();
const login = LoginTestUtils.testData.formLogin({
origin: "https://example.com",
formActionOrigin: "https://example.com",
username: "user\nname",
password: "pass1",
});
await Services.logins.addLoginAsync(login);
await BrowserTestUtils.waitForCondition(
() => Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue()?.length == 1,
"event has been emitted"
);
const [evt] = Glean.pwmgr.rustIncompatibleLoginFormat.testGetValue();
Assert.equal(evt.extra?.issue, "usernameLineBreak");
Assert.equal(evt.extra?.operation, "add");
Assert.ok("run_id" in evt.extra);
LoginTestUtils.clearData();
const rustStorage = new LoginManagerRustStorage();
rustStorage.removeAllLogins();
await SpecialPowers.flushPrefEnv();
});
/**
* Tests that a rust_migration_performance event is recorded after migration,
* containing both duration and total number of migrated logins.
*/
add_task(async function test_migration_performance_probe() {
// ensure mirror is off
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", false]],
});
Services.fog.testResetFOG();
const login = LoginTestUtils.testData.formLogin({
username: "perf-user",
password: "perf-password",
});
await Services.logins.addLoginAsync(login);
// using the migrationNeeded pref change as an indicator that the migration did run
const prefChangePromise = TestUtils.waitForPrefChange(
"signon.rustMirror.migrationNeeded"
);
await SpecialPowers.pushPrefEnv({
set: [["signon.rustMirror.enabled", true]],
});
await prefChangePromise;
const [evt] = Glean.pwmgr.rustMigrationStatus.testGetValue();
Assert.ok(evt, "rustMigrationStatus event should have been emitted");
Assert.equal(
evt.extra?.number_of_logins_to_migrate,
1,
"event should record number of logins to migrate"
);
Assert.equal(
evt.extra?.number_of_logins_migrated,
1,
"event should record number of logins migrated"
);
Assert.equal(
evt.extra?.had_errors,
"false",
"event should record a boolean indicating migration errors"
);
Assert.greaterOrEqual(
parseInt(evt.extra?.duration_ms, 10),
0,
"event should record non-negative duration in ms"
);
sinon.restore();
LoginTestUtils.clearData();
await SpecialPowers.flushPrefEnv();
});