/** * Test LoginManagerParent._onPasswordEditedOrGenerated() */ "use strict"; const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); const { LoginManagerParent } = ChromeUtils.importESModule( "resource://gre/modules/LoginManagerParent.sys.mjs" ); const { LoginManagerPrompter } = ChromeUtils.importESModule( "resource://gre/modules/LoginManagerPrompter.sys.mjs" ); const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const loginTemplate = Object.freeze({ origin: "https://www.example.com", formActionOrigin: "https://www.mozilla.org", }); let LMP = new LoginManagerParent(); function stubPrompter() { let fakePromptToSavePassword = sinon.stub(); let fakePromptToChangePassword = sinon.stub(); sinon.stub(LMP, "_getPrompter").callsFake(() => { return { promptToSavePassword: fakePromptToSavePassword, promptToChangePassword: fakePromptToChangePassword, }; }); LMP._getPrompter().promptToSavePassword(); LMP._getPrompter().promptToChangePassword(); Assert.ok(LMP._getPrompter.calledTwice, "Checking _getPrompter stub"); Assert.ok( fakePromptToSavePassword.calledOnce, "Checking fakePromptToSavePassword stub" ); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking fakePromptToChangePassword stub" ); function resetPrompterHistory() { LMP._getPrompter.resetHistory(); fakePromptToSavePassword.resetHistory(); fakePromptToChangePassword.resetHistory(); } function restorePrompter() { LMP._getPrompter.restore(); } resetPrompterHistory(); return { fakePromptToSavePassword, fakePromptToChangePassword, resetPrompterHistory, restorePrompter, }; } async function stubGeneratedPasswordForBrowsingContextId(id) { Assert.ok( LoginManagerParent._browsingContextGlobal, "Check _browsingContextGlobal exists" ); Assert.ok( !LoginManagerParent._browsingContextGlobal.get(id), `BrowsingContext ${id} shouldn't exist yet` ); info(`Stubbing BrowsingContext.get(${id})`); let stub = sinon .stub(LoginManagerParent._browsingContextGlobal, "get") .withArgs(id) .callsFake(() => { return { currentWindowGlobal: { documentPrincipal: Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://www.example.com^userContextId=6" ), documentURI: Services.io.newURI("https://www.example.com"), }, get embedderElement() { info("returning embedderElement"); let browser = MockDocument.createTestDocument( "chrome://browser/content/browser.xhtml", ` `, "application/xml", true ).querySelector("browser"); MockDocument.mockBrowsingContextProperty(browser, this); return browser; }, get top() { return this; }, }; }); Assert.ok( LoginManagerParent._browsingContextGlobal.get(id), `Checking BrowsingContext.get(${id}) stub` ); const generatedPassword = await LMP.getGeneratedPassword(); notEqual(generatedPassword, null, "Check password was returned"); equal( generatedPassword.length, LoginTestUtils.generation.LENGTH, "Check password length" ); equal( LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().size, 1, "1 added to cache" ); equal( LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ).value, generatedPassword, "Cache key and value" ); LoginManagerParent._browsingContextGlobal.get.resetHistory(); return { stub, generatedPassword, }; } function checkEditTelemetryRecorded(expectedCount, msg) { info("Check that expected telemetry event was recorded"); const snapshot = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ); let resultsCount = 0; if ("parent" in snapshot) { const telemetryProps = Object.freeze({ category: "pwmgr", method: "filled_field_edited", object: "generatedpassword", }); const results = snapshot.parent.filter( ([time, category, method, object]) => { return ( category === telemetryProps.category && method === telemetryProps.method && object === telemetryProps.object ); } ); resultsCount = results.length; } equal( resultsCount, expectedCount, "Check count of pwmgr.filled_field_edited for generatedpassword: " + msg ); } async function startTestConditions(contextId) { LMP.useBrowsingContext(contextId); Assert.ok( LMP._onPasswordEditedOrGenerated, "LMP._onPasswordEditedOrGenerated exists" ); equal(await LMP.getGeneratedPassword(), null, "Null with no BrowsingContext"); equal( LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().size, 0, "Empty cache to start" ); equal( (await Services.logins.getAllLogins()).length, 0, "Should have no saved logins at the start of the test" ); } /* * Compare login details excluding usernameField and passwordField */ function assertLoginProperties(actualLogin, expected) { equal(actualLogin.origin, expected.origin, "Compare origin"); equal( actualLogin.formActionOrigin, expected.formActionOrigin, "Compare formActionOrigin" ); equal(actualLogin.httpRealm, expected.httpRealm, "Compare httpRealm"); equal(actualLogin.username, expected.username, "Compare username"); equal(actualLogin.password, expected.password, "Compare password"); } add_setup(async () => { // Get a profile for storage. do_get_profile(); // Force the feature to be enabled. Services.prefs.setBoolPref("signon.generation.available", true); Services.prefs.setBoolPref("signon.generation.enabled", true); await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(); }); add_task(async function test_onPasswordEditedOrGenerated_generatedPassword() { await startTestConditions(99); let { generatedPassword } = await stubGeneratedPasswordForBrowsingContextId( 99 ); let { fakePromptToChangePassword, restorePrompter } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); let storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "addLogin" ); equal( (await Services.logins.getAllLogins()).length, 0, "Should have no saved logins at the start of the test" ); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: generatedPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let [login] = await storageChangedPromised; let expected = new LoginInfo( "https://www.example.com", "https://www.mozilla.org", null, "", // verify we don't include the username when auto-saving a login generatedPassword ); Assert.ok(login.equals(expected), "Check added login"); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword had a truthy 'notifySaved' argument" ); info("Edit the password"); const newPassword = generatedPassword + "🔥"; storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "modifyLogin" ); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited boolean should be true"); equal(generatedPW.value, newPassword, "Cached password should be updated"); // login metadata should be updated let [dataArray] = await storageChangedPromised; login = dataArray.queryElementAt(1, Ci.nsILoginInfo); expected.password = newPassword; Assert.ok(login.equals(expected), "Check updated login"); equal( (await Services.logins.getAllLogins()).length, 1, "Should have 1 saved login still" ); info( "Simulate a second edit to check that the telemetry event for the first edit is not recorded twice" ); const newerPassword = newPassword + "🦊"; storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "modifyLogin" ); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newerPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited state should remain true"); equal(generatedPW.value, newerPassword, "Cached password should be updated"); [dataArray] = await storageChangedPromised; login = dataArray.queryElementAt(1, Ci.nsILoginInfo); expected.password = newerPassword; Assert.ok(login.equals(expected), "Check updated login"); equal( (await Services.logins.getAllLogins()).length, 1, "Should have 1 saved login still" ); checkEditTelemetryRecorded(1, "with auto-save"); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); }); add_task( async function test_onPasswordEditedOrGenerated_editToEmpty_generatedPassword() { await startTestConditions(99); let { generatedPassword } = await stubGeneratedPasswordForBrowsingContextId( 99 ); let { fakePromptToChangePassword, restorePrompter } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); let storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "addLogin" ); equal( (await Services.logins.getAllLogins()).length, 0, "Should have no saved logins at the start of the test" ); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: generatedPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let [login] = await storageChangedPromised; let expected = new LoginInfo( "https://www.example.com", "https://www.mozilla.org", null, "", // verify we don't include the username when auto-saving a login generatedPassword ); Assert.ok(login.equals(expected), "Check added login"); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword had a truthy 'notifySaved' argument" ); info("Edit the password to be empty"); const newPassword = ""; await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(!generatedPW.edited, "Cached edited boolean should be false"); equal( generatedPW.value, generatedPassword, "Cached password shouldn't be updated" ); checkEditTelemetryRecorded(0, "Blanking doesn't count as an edit"); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); } ); add_task(async function test_addUsernameBeforeAutoSaveEdit() { await startTestConditions(99); let { generatedPassword } = await stubGeneratedPasswordForBrowsingContextId( 99 ); let { fakePromptToChangePassword, restorePrompter, resetPrompterHistory } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); let fakePopupNotifications = { getNotification: sinon.stub().returns({ dismissed: true }), }; sinon.stub(LoginHelper, "getBrowserForPrompt").callsFake(() => { return { ownerGlobal: { PopupNotifications: fakePopupNotifications, }, }; }); let storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "addLogin" ); equal( (await Services.logins.getAllLogins()).length, 0, "Should have no saved logins at the start of the test" ); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: generatedPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let [login] = await storageChangedPromised; let expected = new LoginInfo( "https://www.example.com", "https://www.mozilla.org", null, "", // verify we don't include the username when auto-saving a login generatedPassword ); Assert.ok(login.equals(expected), "Check added login"); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword had a truthy 'notifySaved' argument" ); info("Checking the getNotification stub"); Assert.ok( !fakePopupNotifications.getNotification.called, "getNotification didn't get called yet" ); resetPrompterHistory(); info("Add a username to the auto-saved login in storage"); let loginWithUsername = login.clone(); loginWithUsername.username = "added_username"; LoginManagerPrompter._updateLogin(login, loginWithUsername); info("Edit the password"); const newPassword = generatedPassword + "🔥"; storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "modifyLogin" ); // will update the doorhanger with changed password await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited boolean should be true"); equal(generatedPW.value, newPassword, "Cached password should be updated"); let [dataArray] = await storageChangedPromised; login = dataArray.queryElementAt(1, Ci.nsILoginInfo); loginWithUsername.password = newPassword; // the password should be updated in storage, but not the username (until the user confirms the doorhanger) assertLoginProperties(login, loginWithUsername); Assert.ok(login.matches(loginWithUsername, false), "Check updated login"); equal( (await Services.logins.getAllLogins()).length, 1, "Should have 1 saved login still" ); Assert.ok( fakePopupNotifications.getNotification.calledOnce, "getNotification was called" ); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); // The generated password changed, so we expect notifySaved to be true Assert.ok( fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword should have a falsey 'notifySaved' argument" ); resetPrompterHistory(); info( "Simulate a second edit to check that the telemetry event for the first edit is not recorded twice" ); const newerPassword = newPassword + "🦊"; storageChangedPromised = TestUtils.topicObserved( "passwordmgr-storage-changed", (_, data) => data == "modifyLogin" ); info("Calling _onPasswordEditedOrGenerated again"); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newerPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited state should remain true"); equal(generatedPW.value, newerPassword, "Cached password should be updated"); [dataArray] = await storageChangedPromised; login = dataArray.queryElementAt(1, Ci.nsILoginInfo); loginWithUsername.password = newerPassword; assertLoginProperties(login, loginWithUsername); Assert.ok(login.matches(loginWithUsername, false), "Check updated login"); equal( (await Services.logins.getAllLogins()).length, 1, "Should have 1 saved login still" ); checkEditTelemetryRecorded(1, "with auto-save"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); equal( fakePromptToChangePassword.getCall(0).args[2].password, newerPassword, "promptToChangePassword had the updated password" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); LoginManagerParent._browsingContextGlobal.get.restore(); LoginHelper.getBrowserForPrompt.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); }); add_task(async function test_editUsernameOfFilledSavedLogin() { await startTestConditions(99); await stubGeneratedPasswordForBrowsingContextId(99); let { fakePromptToChangePassword, fakePromptToSavePassword, restorePrompter, resetPrompterHistory, } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); let fakePopupNotifications = { getNotification: sinon.stub().returns({ dismissed: true }), }; sinon.stub(LoginHelper, "getBrowserForPrompt").callsFake(() => { return { ownerGlobal: { PopupNotifications: fakePopupNotifications, }, }; }); let login0Props = Object.assign({}, loginTemplate, { username: "someusername", password: "qweqweq", }); info("Adding initial login: " + JSON.stringify(login0Props)); let savedLogin = await LoginTestUtils.addLogin(login0Props); let logins = await Services.logins.getAllLogins(); info("Saved initial login: " + JSON.stringify(logins[0])); equal(logins.length, 1, "Should have 1 saved login at the start of the test"); // first prompt to save a new login let newUsername = "differentuser"; let newPassword = login0Props.password + "🔥"; await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", autoFilledLoginGuid: savedLogin.guid, newPasswordField: { value: newPassword }, usernameField: { value: newUsername }, triggeredByFillingGenerated: false, } ); let expected = new LoginInfo( login0Props.origin, login0Props.formActionOrigin, null, newUsername, newPassword ); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); info("Checking the getNotification stub"); Assert.ok( !fakePopupNotifications.getNotification.called, "getNotification was not called" ); Assert.ok( fakePromptToSavePassword.calledOnce, "Checking promptToSavePassword was called" ); Assert.ok( fakePromptToSavePassword.getCall(0).args[2], "promptToSavePassword had a truthy 'dismissed' argument" ); Assert.ok( !fakePromptToSavePassword.getCall(0).args[3], "promptToSavePassword had a falsey 'notifySaved' argument" ); assertLoginProperties(fakePromptToSavePassword.getCall(0).args[1], expected); resetPrompterHistory(); // then prompt with matching username/password await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", autoFilledLoginGuid: savedLogin.guid, newPasswordField: { value: login0Props.password }, usernameField: { value: login0Props.username }, triggeredByFillingGenerated: false, } ); expected = new LoginInfo( login0Props.origin, login0Props.formActionOrigin, null, login0Props.username, login0Props.password ); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); info("Checking the getNotification stub"); Assert.ok( fakePopupNotifications.getNotification.called, "getNotification was called" ); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( !fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword had a falsey 'notifySaved' argument" ); assertLoginProperties( fakePromptToChangePassword.getCall(0).args[2], expected ); resetPrompterHistory(); LoginManagerParent._browsingContextGlobal.get.restore(); LoginHelper.getBrowserForPrompt.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); }); add_task( async function test_onPasswordEditedOrGenerated_generatedPassword_withDisabledLogin() { await startTestConditions(99); let { generatedPassword } = await stubGeneratedPasswordForBrowsingContextId( 99 ); let { restorePrompter } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); info("Disable login saving for the site"); Services.logins.setLoginSavingEnabled("https://www.example.com", false); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: generatedPassword }, triggeredByFillingGenerated: true, } ); equal( (await Services.logins.getAllLogins()).length, 0, "Should have no saved logins since saving is disabled" ); Assert.ok( LMP._getPrompter.notCalled, "Checking _getPrompter wasn't called" ); // Clean up LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.setLoginSavingEnabled("https://www.example.com", true); Services.logins.removeAllUserFacingLogins(); } ); add_task( async function test_onPasswordEditedOrGenerated_generatedPassword_withSavedEmptyUsername() { await startTestConditions(99); let login0Props = Object.assign({}, loginTemplate, { username: "", password: "qweqweq", }); info("Adding initial login: " + JSON.stringify(login0Props)); let expected = await LoginTestUtils.addLogin(login0Props); info( "Saved initial login: " + JSON.stringify(Services.logins.getAllLogins()[0]) ); let { generatedPassword: password1 } = await stubGeneratedPasswordForBrowsingContextId(99); let { restorePrompter, fakePromptToChangePassword } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: password1 }, triggeredByFillingGenerated: true, } ); let logins = await Services.logins.getAllLogins(); equal( logins.length, 1, "Should just have the previously-saved login with empty username" ); assertLoginProperties(logins[0], login0Props); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( !fakePromptToChangePassword.getCall(0).args[4], "promptToChangePassword had a falsey 'notifySaved' argument" ); info("Edit the password"); const newPassword = password1 + "🔥"; await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newPassword }, usernameField: { value: "someusername" }, triggeredByFillingGenerated: true, } ); let generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited boolean should be true"); equal(generatedPW.storageGUID, null, "Should have no storageGUID"); equal(generatedPW.value, newPassword, "Cached password should be updated"); logins = await Services.logins.getAllLogins(); assertLoginProperties(logins[0], login0Props); Assert.ok(logins[0].equals(expected), "Ensure no changes"); equal(logins.length, 1, "Should have 1 saved login still"); checkEditTelemetryRecorded(1, "Updating cache, not storage (no auto-save)"); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); } ); add_task( async function test_onPasswordEditedOrGenerated_generatedPassword_withSavedEmptyUsernameAndUsernameValue() { // Save as the above task but with a non-empty username field value. await startTestConditions(99); let login0Props = Object.assign({}, loginTemplate, { username: "", password: "qweqweq", }); info("Adding initial login: " + JSON.stringify(login0Props)); let expected = await LoginTestUtils.addLogin(login0Props); info( "Saved initial login: " + JSON.stringify(await Services.logins.getAllLogins()[0]) ); let { generatedPassword: password1 } = await stubGeneratedPasswordForBrowsingContextId(99); let { restorePrompter, fakePromptToChangePassword, fakePromptToSavePassword, } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: password1 }, usernameField: { value: "non-empty-username" }, triggeredByFillingGenerated: true, } ); let logins = await Services.logins.getAllLogins(); equal( logins.length, 1, "Should just have the previously-saved login with empty username" ); assertLoginProperties(logins[0], login0Props); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.notCalled, "Checking promptToChangePassword wasn't called" ); Assert.ok( fakePromptToSavePassword.calledOnce, "Checking promptToSavePassword was called" ); Assert.ok( fakePromptToSavePassword.getCall(0).args[2], "promptToSavePassword had a truthy 'dismissed' argument" ); Assert.ok( !fakePromptToSavePassword.getCall(0).args[3], "promptToSavePassword had a falsey 'notifySaved' argument" ); info("Edit the password"); const newPassword = password1 + "🔥"; await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: newPassword }, usernameField: { value: "non-empty-username" }, triggeredByFillingGenerated: true, } ); Assert.ok( fakePromptToChangePassword.notCalled, "Checking promptToChangePassword wasn't called" ); Assert.ok( fakePromptToSavePassword.calledTwice, "Checking promptToSavePassword was called again" ); Assert.ok( fakePromptToSavePassword.getCall(1).args[2], "promptToSavePassword had a truthy 'dismissed' argument" ); Assert.ok( !fakePromptToSavePassword.getCall(1).args[3], "promptToSavePassword had a falsey 'notifySaved' argument" ); let generatedPW = LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( "https://www.example.com^userContextId=6" ); Assert.ok(generatedPW.edited, "Cached edited boolean should be true"); equal(generatedPW.storageGUID, null, "Should have no storageGUID"); equal(generatedPW.value, newPassword, "Cached password should be updated"); logins = await Services.logins.getAllLogins(); assertLoginProperties(logins[0], login0Props); Assert.ok(logins[0].equals(expected), "Ensure no changes"); equal(logins.length, 1, "Should have 1 saved login still"); checkEditTelemetryRecorded( 1, "Updating cache, not storage (no auto-save) with username in field" ); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); Services.telemetry.clearEvents(); } ); add_task( async function test_onPasswordEditedOrGenerated_generatedPassword_withEmptyUsernameDifferentFormActionOrigin() { await startTestConditions(99); let login0Props = Object.assign({}, loginTemplate, { username: "", password: "qweqweq", }); await LoginTestUtils.addLogin(login0Props); let { generatedPassword: password1 } = await stubGeneratedPasswordForBrowsingContextId(99); let { restorePrompter, fakePromptToChangePassword } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.elsewhere.com", newPasswordField: { value: password1 }, triggeredByFillingGenerated: true, } ); let savedLogins = await Services.logins.getAllLogins(); equal( savedLogins.length, 2, "Should have saved the generated-password login" ); assertLoginProperties(savedLogins[0], login0Props); assertLoginProperties( savedLogins[1], Object.assign({}, loginTemplate, { formActionOrigin: "https://www.elsewhere.com", username: "", password: password1, }) ); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[2], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'notifySaved' argument" ); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); } ); add_task( async function test_onPasswordEditedOrGenerated_generatedPassword_withSavedUsername() { await startTestConditions(99); let login0Props = Object.assign({}, loginTemplate, { username: "previoususer", password: "qweqweq", }); await LoginTestUtils.addLogin(login0Props); let { generatedPassword: password1 } = await stubGeneratedPasswordForBrowsingContextId(99); let { restorePrompter, fakePromptToChangePassword } = stubPrompter(); let rootBrowser = LMP.getRootBrowser(); await LMP._onPasswordEditedOrGenerated( rootBrowser, "https://www.example.com", { browsingContextId: 99, formActionOrigin: "https://www.mozilla.org", newPasswordField: { value: password1 }, triggeredByFillingGenerated: true, } ); let savedLogins = await Services.logins.getAllLogins(); equal( savedLogins.length, 2, "Should have saved the generated-password login" ); assertLoginProperties(savedLogins[0], login0Props); assertLoginProperties( savedLogins[1], Object.assign({}, loginTemplate, { username: "", password: password1, }) ); Assert.ok(LMP._getPrompter.calledOnce, "Checking _getPrompter was called"); Assert.ok( fakePromptToChangePassword.calledOnce, "Checking promptToChangePassword was called" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[2], "promptToChangePassword had a truthy 'dismissed' argument" ); Assert.ok( fakePromptToChangePassword.getCall(0).args[3], "promptToChangePassword had a truthy 'notifySaved' argument" ); LoginManagerParent._browsingContextGlobal.get.restore(); restorePrompter(); LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); Services.logins.removeAllUserFacingLogins(); } );