Bug 1560042 - Part 3: Update matching login and remove empty-username login when persisting changes from update doorhanger. r=MattN

* Add an extra param to promptToChangePassword to indicate whether a duped/superceded login should be deleted
* Add a raft of tests for promptToChangePassword as it applies to generated passwords and auto-saved logins

Differential Revision: https://phabricator.services.mozilla.com/D40115

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Sam Foster 2019-08-21 22:07:21 +00:00
Родитель 75218afff9
Коммит de25b2ae50
10 изменённых файлов: 1139 добавлений и 87 удалений

Просмотреть файл

@ -570,19 +570,19 @@ this.LoginManagerParent = {
formActionOrigin,
});
let { browsingContext } = browser;
let framePrincipalOrigin =
browsingContext.currentWindowGlobal.documentPrincipal.origin;
let generatedPW = this._generatedPasswordsByPrincipalOrigin.get(
framePrincipalOrigin
);
// If we didn't find a username field, but seem to be changing a
// password, allow the user to select from a list of applicable
// logins to update the password for.
if (!usernameField && oldPasswordField && logins.length > 0) {
let prompter = this._getPrompter(browser, openerTopWindowID);
let { browsingContext } = browser;
let framePrincipalOrigin =
browsingContext.currentWindowGlobal.documentPrincipal.origin;
let generatedPW = this._generatedPasswordsByPrincipalOrigin.get(
framePrincipalOrigin
);
if (logins.length == 1) {
let oldLogin = logins[0];
@ -595,10 +595,25 @@ this.LoginManagerParent = {
return;
}
let autoSavedStorageGUID = "";
if (
generatedPW &&
generatedPW.storageGUID == oldLogin.guid &&
generatedPW.value == formLogin.password
) {
// this login has a generated password, auto-saved in this session
autoSavedStorageGUID = generatedPW.storageGUID;
}
formLogin.username = oldLogin.username;
formLogin.usernameField = oldLogin.usernameField;
prompter.promptToChangePassword(oldLogin, formLogin, dismissedPrompt);
prompter.promptToChangePassword(
oldLogin,
formLogin,
dismissedPrompt,
false, // notifySaved
autoSavedStorageGUID
);
return;
} else if (!generatedPW || generatedPW.value != newPasswordField.value) {
// Note: It's possible that that we already have the correct u+p saved
@ -648,6 +663,15 @@ this.LoginManagerParent = {
if (existingLogin) {
log("Found an existing login matching this form submission");
let autoSavedStorageGUID = "";
if (
generatedPW &&
generatedPW.storageGUID == existingLogin.guid &&
generatedPW.value == formLogin.password
) {
// this login has a generated password, auto-saved in this session
autoSavedStorageGUID = generatedPW.storageGUID;
}
// Change password if needed.
if (existingLogin.password != formLogin.password) {
@ -656,7 +680,9 @@ this.LoginManagerParent = {
prompter.promptToChangePassword(
existingLogin,
formLogin,
dismissedPrompt
dismissedPrompt,
false, // notifySaved
autoSavedStorageGUID
);
} else if (!existingLogin.username && formLogin.username) {
log("...empty username update, prompting to change.");
@ -664,7 +690,9 @@ this.LoginManagerParent = {
prompter.promptToChangePassword(
existingLogin,
formLogin,
dismissedPrompt
dismissedPrompt,
false, // notifySaved
autoSavedStorageGUID
);
} else {
recordLoginUse(existingLogin);
@ -860,11 +888,24 @@ this.LoginManagerParent = {
if (loginToChange) {
// Show a change doorhanger to allow modifying an already-saved login
// e.g. to add a username or update the password.
let autoSavedStorageGUID = "";
if (
generatedPW.value == loginToChange.password &&
generatedPW.storageGUID == loginToChange.guid
) {
autoSavedStorageGUID = generatedPW.storageGUID;
}
log(
"_onGeneratedPasswordFilledOrEdited: promptToChangePassword with autoSavedStorageGUID: " +
autoSavedStorageGUID
);
prompter.promptToChangePassword(
loginToChange,
formLogin,
true, // dismissed prompt
autoSaveLogin // notifySaved
autoSaveLogin, // notifySaved
autoSavedStorageGUID // autoSavedLoginGuid
);
return;
}

Просмотреть файл

@ -958,7 +958,7 @@ LoginManagerPrompter.prototype = {
*
* @param {nsILoginInfo} login
* Login to save or change. For changes, this login should contain the
* new password.
* new password and/or username
* @param {string} type
* This is "password-save" or "password-change" depending on the
* original notification type. This is used for telemetry and tests.
@ -968,17 +968,23 @@ LoginManagerPrompter.prototype = {
* Whether to indicate to the user that the login was already saved.
* @param {string} [options.messageStringID = undefined]
* An optional string ID to override the default message.
* @param {string} [options.autoSavedLoginGuid = ""]
* A string guid value for the old login to be removed if the changes
* match it to an different login
*/
_showLoginCaptureDoorhanger(
login,
type,
showOptions = {},
{ notifySaved = false, messageStringID } = {}
{ notifySaved = false, messageStringID, autoSavedLoginGuid = "" } = {}
) {
let { browser } = this._getNotifyWindow();
if (!browser) {
return;
}
this.log(
`_showLoginCaptureDoorhanger, got autoSavedLoginGuid: ${autoSavedLoginGuid}`
);
let saveMsgNames = {
prompt: login.username === "" ? "saveLoginMsgNoUser" : "saveLoginMsg",
@ -1044,6 +1050,9 @@ LoginManagerPrompter.prototype = {
};
let updateButtonLabel = () => {
if (!currentNotification) {
Cu.reportError("updateButtonLabel, no currentNotification");
}
let foundLogins = LoginHelper.searchLoginsWithObject({
formActionOrigin: login.formActionOrigin,
origin: login.origin,
@ -1051,7 +1060,11 @@ LoginManagerPrompter.prototype = {
schemeUpgrades: LoginHelper.schemeUpgrades,
});
let logins = this._filterUpdatableLogins(login, foundLogins);
let logins = this._filterUpdatableLogins(
login,
foundLogins,
autoSavedLoginGuid
);
let msgNames = logins.length == 0 ? saveMsgNames : changeMsgNames;
// Update the label based on whether this will be a new login or not.
@ -1139,7 +1152,11 @@ LoginManagerPrompter.prototype = {
schemeUpgrades: LoginHelper.schemeUpgrades,
});
let logins = this._filterUpdatableLogins(login, foundLogins);
let logins = this._filterUpdatableLogins(
login,
foundLogins,
autoSavedLoginGuid
);
let resolveBy = ["scheme", "timePasswordChanged"];
logins = LoginHelper.dedupeLogins(
logins,
@ -1147,8 +1164,28 @@ LoginManagerPrompter.prototype = {
resolveBy,
login.origin
);
// sort exact username matches to the top
logins.sort(l => (l.username == login.username ? -1 : 1));
if (logins.length == 0) {
this.log(`persistData: Matched ${logins.length} logins`);
let loginToRemove;
let loginToUpdate = logins.shift();
if (logins.length && logins[0].guid == autoSavedLoginGuid) {
loginToRemove = logins.shift();
}
if (logins.length) {
this.log(
"multiple logins, loginToRemove:",
loginToRemove && loginToRemove.guid
);
Cu.reportError("Unexpected match of multiple logins.");
return;
}
if (!loginToUpdate) {
// Create a new login, don't update an original.
// The original login we have been provided with might have its own
// metadata, but we don't want it propagated to the newly created one.
Services.logins.addLogin(
@ -1162,18 +1199,21 @@ LoginManagerPrompter.prototype = {
login.passwordField
)
);
} else if (logins.length == 1) {
if (
logins[0].password == login.password &&
logins[0].username == login.username
) {
// We only want to touch the login's use count and last used time.
this._updateLogin(logins[0]);
} else {
this._updateLogin(logins[0], login);
}
} else if (
loginToUpdate.password == login.password &&
loginToUpdate.username == login.username
) {
// We only want to touch the login's use count and last used time.
this.log("persistData: Touch matched login", loginToUpdate.guid);
this._updateLogin(loginToUpdate);
} else {
Cu.reportError("Unexpected match of multiple logins.");
this.log("persistData: Update matched login", loginToUpdate.guid);
this._updateLogin(loginToUpdate, login);
}
if (loginToRemove) {
this.log("persistData: removing login", loginToRemove.guid);
Services.logins.removeLogin(loginToRemove);
}
};
@ -1414,15 +1454,21 @@ LoginManagerPrompter.prototype = {
* The old login we may want to update.
* @param {nsILoginInfo} aNewLogin
* The new login from the page form.
* @param dismissed
* A boolean indicating if the prompt should be automatically
* dismissed on being shown.
* @param notifySaved
* A boolean value indicating whether the notification should indicate that
* a login has been saved
* @param {boolean} [dismissed = false]
* If the prompt should be automatically dismissed on being shown.
* @param {boolean} [notifySaved = false]
* Whether the notification should indicate that a login has been saved
* @param {string} [autoSavedLoginGuid = ""]
* A guid value for the old login to be removed if the changes match it
* to a different login
*/
promptToChangePassword(aOldLogin, aNewLogin, dismissed, notifySaved) {
this.log("promptToChangePassword");
promptToChangePassword(
aOldLogin,
aNewLogin,
dismissed = false,
notifySaved = false,
autoSavedLoginGuid = ""
) {
let notifyObj = this._getPopupNote();
if (notifyObj) {
@ -1431,7 +1477,8 @@ LoginManagerPrompter.prototype = {
aOldLogin,
aNewLogin,
dismissed,
notifySaved
notifySaved,
autoSavedLoginGuid
);
} else {
this._showChangeLoginDialog(aOldLogin, aNewLogin);
@ -1461,7 +1508,8 @@ LoginManagerPrompter.prototype = {
aOldLogin,
aNewLogin,
dismissed = false,
notifySaved = false
notifySaved = false,
autoSavedLoginGuid = ""
) {
let login = aOldLogin.clone();
login.origin = aNewLogin.origin;
@ -1491,6 +1539,7 @@ LoginManagerPrompter.prototype = {
{
notifySaved,
messageStringID,
autoSavedLoginGuid,
}
);
@ -1882,21 +1931,26 @@ LoginManagerPrompter.prototype = {
* to match a submitted login, instead of creating a new one.
*
* Given a login and a loginList, it filters the login list
* to find every login with either the same username as aLogin
* or with the same password as aLogin and an empty username
* so the user can add a username.
* to find every login with either:
* - the same username as aLogin
* - the same password as aLogin and an empty username
* so the user can add a username.
* - the same guid as the given login when it has an empty username
*
* @param {nsILoginInfo} aLogin
* login to use as filter.
* @param {nsILoginInfo[]} aLoginList
* Array of logins to filter.
* @param {String} includeGUID
* guid value for login that not be filtered out
* @returns {nsILoginInfo[]} the filtered array of logins.
*/
_filterUpdatableLogins(aLogin, aLoginList) {
_filterUpdatableLogins(aLogin, aLoginList, includeGUID) {
return aLoginList.filter(
l =>
l.username == aLogin.username ||
(l.password == aLogin.password && !l.username)
(l.password == aLogin.password && !l.username) ||
(includeGUID && includeGUID == l.guid)
);
},
}; // end of LoginManagerPrompter implementation

Просмотреть файл

@ -71,11 +71,15 @@ interface nsILoginManagerPrompter : nsISupports {
* @param dismissed
* A boolean value indicating whether the save logins doorhanger should
* be dismissed automatically when shown.
* @param autoSavedLoginGuid
* A string guid value for the old login to be removed if the changes
* match it to a different login
*/
void promptToChangePassword(in nsILoginInfo aOldLogin,
in nsILoginInfo aNewLogin,
[optional] in boolean dismissed,
[optional] in boolean notifySaved);
[optional] in boolean notifySaved,
[optional] in AString autoSavedLoginGuid);
/**
* Ask the user if they want to change the password for one of

Просмотреть файл

@ -70,8 +70,15 @@ this.LoginTestUtils = {
(_, data) => data == "addLogin"
);
Services.logins.addLogin(login);
await storageChangedPromised;
return login;
let [savedLogin] = await storageChangedPromised;
return savedLogin;
},
resetGeneratedPasswordsCache() {
let { LoginManagerParent } = ChromeUtils.import(
"resource://gre/modules/LoginManagerParent.jsm"
);
LoginManagerParent._generatedPasswordsByPrincipalOrigin.clear();
},
/**

Просмотреть файл

@ -108,3 +108,4 @@ support-files =
subtst_privbrowsing_1.html
form_password_change.html
skip-if = fission
[browser_promptToChangePassword.js]

Просмотреть файл

@ -344,6 +344,7 @@ add_task(async function fill_generated_password_with_matching_logins() {
);
Services.logins.removeAllLogins();
LoginTestUtils.resetGeneratedPasswordsCache();
});
add_task(async function test_edited_generated_password_in_new_tab() {
@ -448,5 +449,6 @@ add_task(async function test_edited_generated_password_in_new_tab() {
);
Services.logins.removeAllLogins();
LoginTestUtils.resetGeneratedPasswordsCache();
await SpecialPowers.popPrefEnv();
});

Просмотреть файл

@ -38,6 +38,9 @@ add_task(async function test_disable() {
let promiseChanged = promiseStorageChanged("hostSavingDisabled");
await BrowserTestUtils.waitForEvent(dialog, "load");
await new Promise(resolve => {
waitForFocus(resolve, dialog);
});
Services.logins.setLoginSavingEnabled(LOGIN_HOST, false);
await promiseChanged;
is(countDisabledHosts(dialog), 1, "Verify disabled host added");
@ -49,6 +52,9 @@ add_task(async function test_enable() {
let promiseChanged = promiseStorageChanged("hostSavingEnabled");
await BrowserTestUtils.waitForEvent(dialog, "load");
await new Promise(resolve => {
waitForFocus(resolve, dialog);
});
Services.logins.setLoginSavingEnabled(LOGIN_HOST, true);
await promiseChanged;
is(countDisabledHosts(dialog), 0, "Verify disabled host removed");

Просмотреть файл

@ -13,11 +13,12 @@ const FORM_PAGE_PATH =
const passwordInputSelector = "#form-basic-password";
const usernameInputSelector = "#form-basic-username";
let login1;
async function setup_withOneLogin(username = "username", password = "pass1") {
// Reset to a single, known login
Services.logins.removeAllLogins();
login1 = await LoginTestUtils.addLogin({ username, password });
LoginTestUtils.resetGeneratedPasswordsCache();
let login = await LoginTestUtils.addLogin({ username, password });
return login;
}
async function setup_withNoLogins() {
@ -28,6 +29,7 @@ async function setup_withNoLogins() {
0,
"0 logins at the start of the test"
);
LoginTestUtils.resetGeneratedPasswordsCache();
}
async function fillGeneratedPasswordFromACPopup(
@ -216,17 +218,17 @@ async function openAndVerifyDoorhanger(browser, type, expected) {
expected.anchorExtraAttr,
"Check icon extraAttr attribute"
);
if (!PopupNotifications.isPanelOpen) {
let promiseShown = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popupshown"
);
await SimpleTest.promiseFocus(browser);
info("Clicking on anchor to show popup.");
notif.anchorElement.click();
let { panel } = PopupNotifications;
// if the doorhanged is dimissed, we will open it to check panel contents
if (panel.state !== "open") {
let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
if (panel.state !== "showing") {
// synthesize click on anchor as this also blurs the form field triggering
// a change event
EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
}
await promiseShown;
}
// if the doorhanged is dimissed, we will open it to check panel contents
let { passwordValue, usernameValue } = await checkPromptContents(
notif.anchorElement,
browser,
@ -326,7 +328,14 @@ add_task(async function autocomplete_generated_password_auto_saved() {
usernameValue: "",
passwordLength: LoginTestUtils.generation.LENGTH,
});
await clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
await promiseHidden;
// confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
ok(
!notif.anchorElement.hasAttribute("extraAttr"),
@ -394,7 +403,13 @@ add_task(async function autocomplete_generated_password_saved_empty_username() {
passwordLength: LoginTestUtils.generation.LENGTH,
});
await clickDoorhangerButton(notif, CHANGE_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, CHANGE_BUTTON);
await promiseHidden;
info("Waiting for modifyLogin");
await storageChangedPromise;
verifyLogins([
@ -454,7 +469,13 @@ add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() {
passwordLength: LoginTestUtils.generation.LENGTH,
});
await clickDoorhangerButton(notif, REMEMBER_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, REMEMBER_BUTTON);
await promiseHidden;
info("Waiting for addLogin");
await storageChangedPromise;
verifyLogins([
@ -518,7 +539,13 @@ add_task(async function contextfill_generated_password_saved_empty_username() {
passwordLength: LoginTestUtils.generation.LENGTH,
});
await clickDoorhangerButton(notif, CHANGE_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, CHANGE_BUTTON);
await promiseHidden;
info("Waiting for modifyLogin");
await storageChangedPromise;
verifyLogins([
@ -562,7 +589,14 @@ add_task(async function autocomplete_generated_password_edited_no_auto_save() {
usernameValue: "",
passwordLength: LoginTestUtils.generation.LENGTH,
});
await clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
await promiseHidden;
info("Waiting to verifyGeneratedPasswordWasFilled");
await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
@ -578,7 +612,13 @@ add_task(async function autocomplete_generated_password_edited_no_auto_save() {
usernameValue: "",
passwordLength: LoginTestUtils.generation.LENGTH + 2,
});
await clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
await promiseHidden;
verifyLogins([
{
@ -597,7 +637,13 @@ add_task(async function autocomplete_generated_password_edited_no_auto_save() {
passwordLength: LoginTestUtils.generation.LENGTH + 2,
});
await clickDoorhangerButton(notif, CHANGE_BUTTON);
promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, CHANGE_BUTTON);
await promiseHidden;
info("Waiting for modifyLogin");
await storageChangedPromise;
verifyLogins([
@ -659,7 +705,7 @@ add_task(async function contextmenu_fill_generated_password_and_set_username() {
browser,
passwordInputSelector
);
info("waiting for password-change doorhanger");
info("waiting for dismissed password-change notification");
await waitForDoorhanger(browser, "password-change");
// Make sure confirmation hint was shown
await hintPromiseShown;
@ -667,6 +713,7 @@ add_task(async function contextmenu_fill_generated_password_and_set_username() {
info("waiting for addLogin");
await storageChangedPromise;
// Check properties of the newly auto-saved login
verifyLogins([
null, // ignore the first one
@ -701,7 +748,14 @@ add_task(async function contextmenu_fill_generated_password_and_set_username() {
"passwordmgr-storage-changed",
(_, data) => data == "modifyLogin"
);
await clickDoorhangerButton(notif, CHANGE_BUTTON);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
clickDoorhangerButton(notif, CHANGE_BUTTON);
await promiseHidden;
info("Waiting for modifyLogin");
await storageChangedPromise;
verifyLogins([
@ -752,7 +806,7 @@ add_task(async function contextmenu_password_change_form_without_username() {
passwordInputSelector
);
info("waiting for password-change doorhanger");
info("waiting for dismissed password-change notification");
await waitForDoorhanger(browser, "password-change");
// Make sure confirmation hint was shown
await hintPromiseShown;
@ -810,3 +864,194 @@ add_task(async function contextmenu_password_change_form_without_username() {
}
);
});
add_task(async function autosaved_login_updated_to_existing_login() {
// test when filling with a generated password and editing the username in the
// doorhanger to match an existing login:
// * the matching login should be updated
// * the auto-saved login should be deleted
// * the metadata for the matching login should be updated
// * the by-origin cache for the password should point at the updated login
await setup_withOneLogin("user1", "xyzpassword");
await LoginTestUtils.addLogin({ username: "user2", password: "abcpassword" });
await openFormInNewTab(
TEST_ORIGIN + FORM_PAGE_PATH,
{
password: {
selector: passwordInputSelector,
expectedValue: "",
},
username: {
selector: usernameInputSelector,
expectedValue: "",
},
},
async function taskFn(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
let storageChangedPromise = TestUtils.topicObserved(
"passwordmgr-storage-changed",
(_, data) => data == "addLogin"
);
let confirmationHint = document.getElementById("confirmation-hint");
let hintPromiseShown = BrowserTestUtils.waitForEvent(
confirmationHint,
"popupshown"
);
info("waiting to fill generated password using context menu");
await doFillGeneratedPasswordContextMenuItem(
browser,
passwordInputSelector
);
info("waiting for dismissed password-change notification");
await waitForDoorhanger(browser, "password-change");
// Make sure confirmation hint was shown
await hintPromiseShown;
await verifyConfirmationHint(confirmationHint);
info("waiting for addLogin");
await storageChangedPromise;
info("addLogin promise resolved");
// Check properties of the newly auto-saved login
let [user1LoginSnapshot, unused, autoSavedLogin] = verifyLogins([
null, // ignore the first one
null, // ignore the 2nd one
{
timesUsed: 1,
username: "",
passwordLength: LoginTestUtils.generation.LENGTH,
},
]);
info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
info("unused, guid: " + unused.guid);
info("autoSavedLogin, guid: " + autoSavedLogin.guid);
info("verifyLogins ok");
let passwordCacheEntry = LoginManagerParent._generatedPasswordsByPrincipalOrigin.get(
"https://example.com"
);
ok(
passwordCacheEntry,
"Got the cached generated password entry for https://example.com"
);
is(
passwordCacheEntry.value,
autoSavedLogin.password,
"Cached password matches the auto-saved login password"
);
is(
passwordCacheEntry.storageGUID,
autoSavedLogin.guid,
"Cached password guid matches the auto-saved login guid"
);
let messagePromise = new Promise(resolve => {
const eventName = "PasswordManager:onGeneratedPasswordFilledOrEdited";
browser.messageManager.addMessageListener(
eventName,
function mgsHandler(msg) {
if (msg.target != browser) {
return;
}
browser.messageManager.removeMessageListener(eventName, mgsHandler);
info("Got onGeneratedPasswordFilledOrEdited, resolving");
// allow LMP to handle the message, then resolve
SimpleTest.executeSoon(resolve);
}
);
});
info("Waiting to openAndVerifyDoorhanger");
// also moves focus, producing another onGeneratedPasswordFilledOrEdited message from content
let notif = await openAndVerifyDoorhanger(browser, "password-change", {
dismissed: true,
anchorExtraAttr: "attention",
usernameValue: "",
password: autoSavedLogin.password,
});
ok(notif, "Got password-change notification");
// content sends a 2nd message when we blur the password field,
// wait for that before interacting with doorhanger
info("waiting for messagePromise");
await messagePromise;
info("Calling updateDoorhangerInputValues");
await updateDoorhangerInputValues({
username: "user1",
});
info("doorhanger inputs updated");
let loginModifiedPromise = TestUtils.topicObserved(
"passwordmgr-storage-changed",
(_, data) => {
if (data == "modifyLogin") {
info("passwordmgr-storage-changed, action: " + data);
info("subject: " + JSON.stringify(_));
return true;
}
return false;
}
);
let loginRemovedPromise = TestUtils.topicObserved(
"passwordmgr-storage-changed",
(_, data) => {
if (data == "removeLogin") {
info("passwordmgr-storage-changed, action: " + data);
info("subject: " + JSON.stringify(_));
return true;
}
return false;
}
);
let promiseHidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
info("clicking change button");
clickDoorhangerButton(notif, CHANGE_BUTTON);
await promiseHidden;
info("Waiting for modifyLogin promise");
await loginModifiedPromise;
info("Waiting for removeLogin promise");
await loginRemovedPromise;
info("storage-change promises resolved");
// Check the auto-saved login was removed and the original login updated
let savedLogins = verifyLogins([
{
username: "user1",
password: autoSavedLogin.password,
timeCreated: user1LoginSnapshot.timeCreated,
timeLastUsed: user1LoginSnapshot.timeLastUsed,
passwordChangedSince: autoSavedLogin.timePasswordChanged,
},
null, // ignore user2
]);
// Check we have no notifications at this point
ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
ok(
!PopupNotifications.getNotification("password", browser),
"No notifications"
);
// make sure the cache entry is up to date:
let updatedLogin = savedLogins[0];
passwordCacheEntry = LoginManagerParent._generatedPasswordsByPrincipalOrigin.get(
"https://example.com"
);
todo_is(
passwordCacheEntry.storageGUID,
updatedLogin.guid,
"Generated password cache entry points at the correct login"
);
}
);
});

Просмотреть файл

@ -0,0 +1,614 @@
/**
* Test result of different input to the promptToChangePassword doorhanger
*/
"use strict";
// The origin for the test URIs.
const TEST_ORIGIN = "https://example.com";
const passwordInputSelector = "#form-basic-password";
const usernameInputSelector = "#form-basic-username";
const availLoginsByValue = new Map();
let savedLoginsByName;
const finalLoginsByGuid = new Map();
let finalLogins;
const availLogins = {
emptyXYZ: LoginTestUtils.testData.formLogin({
username: "",
password: "xyz",
}),
bobXYZ: LoginTestUtils.testData.formLogin({
username: "bob",
password: "xyz",
}),
bobABC: LoginTestUtils.testData.formLogin({
username: "bob",
password: "abc",
}),
};
availLoginsByValue.set(availLogins.emptyXYZ, "emptyXYZ");
availLoginsByValue.set(availLogins.bobXYZ, "bobXYZ");
availLoginsByValue.set(availLogins.bobABC, "bobABC");
async function showChangePasswordDoorhanger(
browser,
oldLogin,
formLogin,
{ notificationType = "password-change", autoSavedLoginGuid = "" } = {}
) {
let prompter = LoginManagerParent._getPrompter(browser, null);
ok(!PopupNotifications.isPanelOpen, "Check the doorhanger isnt already open");
let promiseShown = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popupshown"
);
prompter.promptToChangePassword(
oldLogin,
formLogin,
false, // dimissed prompt
false, // notifySaved
autoSavedLoginGuid
);
await promiseShown;
let notif = getCaptureDoorhanger(notificationType);
ok(notif, `${notificationType} notification exists`);
let { panel } = PopupNotifications;
let notificationElement = panel.childNodes[0];
await BrowserTestUtils.waitForCondition(() => {
return (
notificationElement.querySelector("#password-notification-password")
.value == formLogin.password &&
notificationElement.querySelector("#password-notification-username")
.value == formLogin.username
);
}, "Wait for the notification panel to be populated");
return notif;
}
async function setupLogins(...logins) {
Services.logins.removeAllLogins();
let savedLogins = {};
let timesCreated = new Set();
for (let login of logins) {
let loginName = availLoginsByValue.get(login);
let savedLogin = await LoginTestUtils.addLogin(login);
// we rely on sorting by timeCreated so ensure none are identical
ok(
!timesCreated.has(savedLogin.timeCreated),
"Each login has a different timeCreated"
);
timesCreated.add(savedLogin.timeCreated);
savedLogins[loginName || savedLogin.guid] = savedLogin.clone();
}
return savedLogins;
}
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["signon.autofillForms", false]],
});
ok(!PopupNotifications.isPanelOpen, "No notifications panel open");
});
async function promptToChangePasswordTest(testData) {
info("Starting: " + testData.name);
savedLoginsByName = await setupLogins(...testData.initialSavedLogins);
await SimpleTest.promiseFocus();
info("got focus");
let oldLogin = savedLoginsByName[testData.promptArgs.oldLogin];
let changeLogin = LoginTestUtils.testData.formLogin(
testData.promptArgs.changeLogin
);
let options;
if (testData.autoSavedLoginName) {
options = {
autoSavedLoginGuid: savedLoginsByName[testData.autoSavedLoginName].guid,
};
}
info(
"Waiting for showChangePasswordDoorhanger, username: " +
changeLogin.username
);
await BrowserTestUtils.withNewTab(
{
gBrowser,
TEST_ORIGIN,
},
async function(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
let notif = await showChangePasswordDoorhanger(
browser,
oldLogin,
changeLogin,
options
);
await updateDoorhangerInputValues(testData.promptTextboxValues);
let mainActionButton = getDoorhangerButton(notif, CHANGE_BUTTON);
is(
mainActionButton.label,
testData.expectedButtonLabel,
"Check button label"
);
let { panel } = PopupNotifications;
let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
let storagePromise;
if (testData.expectedStorageChange) {
storagePromise = TestUtils.topicObserved("passwordmgr-storage-changed");
}
info("Clicking mainActionButton");
mainActionButton.doCommand();
info("Waiting for promiseHidden");
await promiseHidden;
info("Waiting for storagePromise");
await storagePromise;
// ensure the notification was removed to keep clean state for next run
await cleanupDoorhanger(notif);
info(testData.resultDescription);
finalLoginsByGuid.clear();
finalLogins = Services.logins.getAllLogins();
finalLogins.sort((a, b) => a.timeCreated > b.timeCreated);
for (let l of finalLogins) {
info(`saved login: ${l.guid}: ${l.username}/${l.password}`);
finalLoginsByGuid.set(l.guid, l);
}
info("verifyLogins next");
verifyLogins(testData.expectedResultLogins);
if (testData.resultCheck) {
testData.resultCheck();
}
}
);
}
let tests = [
{
name: "Add username to sole login",
initialSavedLogins: [availLogins.emptyXYZ],
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "zaphod",
password: "xyz",
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription: "The existing login just gets a new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "zaphod",
password: "xyz",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.emptyXYZ.guid, "Check guid");
},
},
{
name: "Change password of the sole login",
initialSavedLogins: [availLogins.bobXYZ],
promptArgs: {
oldLogin: "bobXYZ",
changeLogin: {
username: "bob",
password: "&*$",
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription: "The existing login just gets a new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "bob",
password: "&*$",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobXYZ.guid, "Check guid");
},
},
{
name: "Change password of the sole empty-username login",
initialSavedLogins: [availLogins.emptyXYZ],
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "",
password: "&*$",
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription: "The existing login just gets a new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "",
password: "&*$",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.emptyXYZ.guid, "Check guid");
},
},
{
name: "Add different username to empty-usernamed login",
initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "alice",
password: "xyz",
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription: "The existing login just gets a new username",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "alice",
password: "xyz",
},
{
username: "bob",
password: "abc",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.emptyXYZ.guid, "Check guid");
ok(
finalLogins[0].timeLastUsed > savedLoginsByName.emptyXYZ.timeLastUsed,
"Check timeLastUsed of 0th login"
);
},
},
{
name:
"Add username to autosaved login to match an existing usernamed login",
initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
autoSavedLoginName: "emptyXYZ",
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "bob",
password: availLogins.emptyXYZ.password,
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription:
"Empty-username login is removed, other login gets the empty-login's password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "bob",
password: "xyz",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
ok(
finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed changed"
);
},
},
{
name:
"Add username to non-autosaved login to match an existing usernamed login",
initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
autoSavedLoginName: "",
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "bob",
password: availLogins.emptyXYZ.password,
},
},
promptTextboxValues: {},
expectedButtonLabel: "Update",
resultDescription: "Multiple login matches, persistData will bail early",
expectedStorageChange: false,
expectedResultLogins: [
{
username: "",
password: "xyz",
},
{
username: "bob",
password: "abc",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.emptyXYZ.guid, "Check guid");
is(
finalLogins[0].timeLastUsed,
savedLoginsByName.emptyXYZ.timeLastUsed,
"Check timeLastUsed didnt change"
);
is(finalLogins[1].guid, savedLoginsByName.bobABC.guid, "Check guid");
is(
finalLogins[1].timeLastUsed,
savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed didnt change"
);
},
},
{
name:
"Username & password changes to an auto-saved login apply to matching usernamed-login",
// when we update an auto-saved login - changing both username & password, is
// the matching login updated and empty-username login removed?
initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
autoSavedLoginName: "emptyXYZ",
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "bob",
password: "xyz",
},
},
promptTextboxValues: {
// type a new password in the doorhanger
password: "newpassword",
},
expectedButtonLabel: "Update",
resultDescription:
"The empty-username login is removed, other login gets the new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "bob",
password: "newpassword",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
ok(
finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed did change"
);
},
},
{
name:
"Username & password changes to a non-auto-saved login matching usernamed-login",
// when we update a non-auto-saved login - changing both username & password, is
// the matching login updated and empty-username login unchanged?
initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
autoSavedLoginName: "", // no auto-saved logins for this session
promptArgs: {
oldLogin: "emptyXYZ",
changeLogin: {
username: "bob",
password: "xyz",
},
},
promptTextboxValues: {
// type a new password in the doorhanger
password: "newpassword",
},
expectedButtonLabel: "Update",
resultDescription:
"The empty-username login is not changed, other login gets the new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "",
password: "xyz",
},
{
username: "bob",
password: "newpassword",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.emptyXYZ.guid, "Check guid");
is(
finalLogins[0].timeLastUsed,
savedLoginsByName.emptyXYZ.timeLastUsed,
"Check timeLastUsed didn't change"
);
is(
finalLogins[0].timePasswordChanged,
savedLoginsByName.emptyXYZ.timePasswordChanged,
"Check timePasswordChanged didn't change"
);
is(finalLogins[1].guid, savedLoginsByName.bobABC.guid, "Check guid");
ok(
finalLogins[1].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed did change"
);
ok(
finalLogins[1].timePasswordChanged >
savedLoginsByName.bobABC.timePasswordChanged,
"Check timePasswordChanged did change"
);
},
},
{
name: "Remove the username and change password of autosaved login",
initialSavedLogins: [availLogins.bobABC],
autoSavedLoginName: "bobABC",
promptArgs: {
oldLogin: "bobABC",
changeLogin: {
username: "bob",
password: "abc!", // trigger change prompt with a password change
},
},
promptTextboxValues: {
username: "",
},
expectedButtonLabel: "Update",
resultDescription:
"The auto-saved login is updated with new empty-username login and new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "",
password: "abc!",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
ok(
finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed did change"
);
ok(
finalLogins[0].timePasswordChanged >
savedLoginsByName.bobABC.timePasswordChanged,
"Check timePasswordChanged did change"
);
},
},
{
name: "Remove the username and change password of non-autosaved login",
initialSavedLogins: [availLogins.bobABC],
// no autosaved guid
promptArgs: {
oldLogin: "bobABC",
changeLogin: {
username: "bob",
password: "abc!", // trigger change prompt with a password change
},
},
promptTextboxValues: {
username: "",
},
expectedButtonLabel: "Save",
resultDescription:
"A new empty-username login is created with the new password",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "bob",
password: "abc",
},
{
username: "",
password: "abc!",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
is(
finalLogins[0].timeLastUsed,
savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed didn't change"
);
is(
finalLogins[0].timePasswordChanged,
savedLoginsByName.bobABC.timePasswordChanged,
"Check timePasswordChanged didn't change"
);
},
},
{
name: "Remove username from the auto-saved sole login",
initialSavedLogins: [availLogins.bobABC],
autoSavedLoginName: "bobABC",
promptArgs: {
oldLogin: "bobABC",
changeLogin: {
username: "bob",
password: "abc!", // trigger change prompt with a password change
},
},
promptTextboxValues: {
username: "",
password: "abc", // put password back to what it was
},
expectedButtonLabel: "Update",
resultDescription: "The existing login is updated",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "",
password: "abc",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
ok(
finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed did change"
);
todo_is(
finalLogins[0].timePasswordChanged,
savedLoginsByName.bobABC.timePasswordChanged,
"Check timePasswordChanged didn't change"
);
},
},
{
name: "Remove username from the non-auto-saved sole login",
initialSavedLogins: [availLogins.bobABC],
// no autoSavedLoginGuid
promptArgs: {
oldLogin: "bobABC",
changeLogin: {
username: "bob",
password: "abc!", // trigger change prompt with a password change
},
},
promptTextboxValues: {
username: "",
password: "abc", // put password back to what it was
},
expectedButtonLabel: "Save",
resultDescription: "A new empty-username login is created",
expectedStorageChange: true,
expectedResultLogins: [
{
username: "bob",
password: "abc",
},
{
username: "",
password: "abc",
},
],
resultCheck() {
is(finalLogins[0].guid, savedLoginsByName.bobABC.guid, "Check guid");
is(
finalLogins[0].timeLastUsed,
savedLoginsByName.bobABC.timeLastUsed,
"Check timeLastUsed didn't change"
);
is(
finalLogins[0].timePasswordChanged,
savedLoginsByName.bobABC.timePasswordChanged,
"Check timePasswordChanged didn't change"
);
},
},
];
for (let testData of tests) {
let tmp = {
async [testData.name]() {
await promptToChangePasswordTest(testData);
},
};
add_task(tmp[testData.name]);
}

Просмотреть файл

@ -16,10 +16,10 @@ registerCleanupFunction(
async function cleanup_removeAllLoginsAndResetRecipes() {
await SpecialPowers.popPrefEnv();
Services.logins.removeAllLogins();
LoginTestUtils.clearData();
LoginTestUtils.resetGeneratedPasswordsCache();
clearHttpAuths();
Services.telemetry.clearEvents();
LMP._generatedPasswordsByPrincipalOrigin.clear();
let recipeParent = LoginTestUtils.recipes.getRecipeParent();
if (!recipeParent) {
@ -27,6 +27,13 @@ registerCleanupFunction(
return;
}
await recipeParent.then(recipeParentResult => recipeParentResult.reset());
await cleanupDoorhanger();
let notif;
while ((notif = PopupNotifications.getNotification("password"))) {
notif.remove();
}
await Promise.resolve();
}
);
@ -35,9 +42,11 @@ registerCleanupFunction(
*
* @param {array} expectedLogins
* An array of expected login properties
* @return {nsILoginInfo[]} - All saved logins sorted by timeCreated
*/
function verifyLogins(expectedLogins = []) {
let allLogins = Services.logins.getAllLogins();
allLogins.sort((a, b) => a.timeCreated > b.timeCreated);
is(
allLogins.length,
expectedLogins.length,
@ -62,11 +71,6 @@ function verifyLogins(expectedLogins = []) {
is(login.username, expected.username, "Check username");
}
if (typeof expected.password !== "undefined") {
info(
`verifyLogins, login has password: ${login.password}, expects: ${
expected.password
}`
);
is(login.password, expected.password, "Check password");
}
if (typeof expected.usedSince !== "undefined") {
@ -80,6 +84,7 @@ function verifyLogins(expectedLogins = []) {
}
}
}
return allLogins;
}
/**
@ -224,6 +229,20 @@ async function getCaptureDoorhangerThatMayOpen(
return notif;
}
function getDoorhangerButton(aPopup, aButtonIndex) {
let notifications = aPopup.owner.panel.children;
ok(notifications.length > 0, "at least one notification displayed");
ok(true, notifications.length + " notification(s)");
let notification = notifications[0];
if (aButtonIndex == "button") {
return notification.button;
} else if (aButtonIndex == "secondaryButton") {
return notification.secondaryButton;
}
return notification.menupopup.querySelectorAll("menuitem")[aButtonIndex];
}
/**
* Clicks the specified popup notification button.
*
@ -234,23 +253,15 @@ async function getCaptureDoorhangerThatMayOpen(
function clickDoorhangerButton(aPopup, aButtonIndex) {
ok(true, "Looking for action at index " + aButtonIndex);
let notifications = aPopup.owner.panel.children;
ok(notifications.length > 0, "at least one notification displayed");
ok(true, notifications.length + " notification(s)");
let notification = notifications[0];
let button = getDoorhangerButton(aPopup, aButtonIndex);
if (aButtonIndex == "button") {
ok(true, "Triggering main action");
notification.button.doCommand();
} else if (aButtonIndex == "secondaryButton") {
ok(true, "Triggering secondary action");
notification.secondaryButton.doCommand();
} else {
ok(true, "Triggering menuitem # " + aButtonIndex);
notification.menupopup
.querySelectorAll("menuitem")
[aButtonIndex].doCommand();
}
button.doCommand();
}
async function cleanupDoorhanger(notif) {
@ -260,7 +271,7 @@ async function cleanupDoorhanger(notif) {
}
let promiseHidden = PN.isPanelOpen
? BrowserTestUtils.waitForEvent(PN.panel, "popuphidden")
: Promise.resolve;
: Promise.resolve();
PN.panel.hidePopup();
await promiseHidden;
}
@ -290,6 +301,55 @@ async function checkDoorhangerUsernamePassword(username, password) {
);
}
/**
* Change the doorhanger's username and password input values.
*
* @param {object} newValues
* named values to update
* @param {string} [newValues.password = undefined]
* An optional string value to replace whatever is in the password field
* @param {string} [newValues.username = undefined]
* An optional string value to replace whatever is in the username field
*/
async function updateDoorhangerInputValues(newValues) {
let { panel } = PopupNotifications;
is(panel.state, "open", "Check the doorhanger is already open");
let notifElem = panel.childNodes[0];
// Note: setUserInput does not reliably dispatch input events from chrome elements?
async function setInputValue(target, value) {
info(`setInputValue: on target: ${target.id}, value: ${value}`);
target.focus();
target.select();
await EventUtils.synthesizeKey("KEY_Backspace");
info(
`setInputValue: target.value: ${target.value}, sending new value string`
);
await EventUtils.sendString(value);
await EventUtils.synthesizeKey("KEY_Tab");
return Promise.resolve();
}
let passwordField = notifElem.querySelector(
"#password-notification-password"
);
let usernameField = notifElem.querySelector(
"#password-notification-username"
);
if (typeof newValues.password !== "undefined") {
if (passwordField.value !== newValues.password) {
await setInputValue(passwordField, newValues.password);
}
}
if (typeof newValues.username !== "undefined") {
if (usernameField.value !== newValues.username) {
await setInputValue(usernameField, newValues.username);
}
}
}
// End popup notification (doorhanger) functions //
async function waitForPasswordManagerDialog(openingFunc) {
@ -452,9 +512,27 @@ async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) {
await ContentTaskUtils.waitForEvent(input, "input");
}
);
let messagePromise = new Promise(resolve => {
const eventName = "PasswordManager:onGeneratedPasswordFilledOrEdited";
browser.messageManager.addMessageListener(eventName, function mgsHandler(
msg
) {
if (msg.target != browser) {
return;
}
browser.messageManager.removeMessageListener(eventName, mgsHandler);
info(
"doFillGeneratedPasswordContextMenuItem: Got onGeneratedPasswordFilledOrEdited, resolving"
);
// allow LMP to handle the message, then resolve
SimpleTest.executeSoon(resolve);
});
});
generatedPasswordItem.doCommand();
info("Waiting for input event");
EventUtils.synthesizeMouseAtCenter(generatedPasswordItem, {});
info(
"doFillGeneratedPasswordContextMenuItem: Waiting for content input event"
);
await passwordChangedPromise;
document.getElementById("contentAreaContextMenu").hidePopup();
await messagePromise;
}