зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
75218afff9
Коммит
de25b2ae50
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче