Bug 394611 - Always prompt the user before changing a stored password. r=gavin
authorJustin Dolske <dolske@mozilla.com>
Sun, 12 Oct 2008 20:05:11 -0700
changeset 20360 44d774cce259826bca9a630261bfbe4e1572ea5d
parent 20359 1ce7fddcb2ce959367f38a874559e905294f6a67
child 20361 979c128d2e6325aa0c1b8cbb7afe814f239e5be0
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin
bugs394611
milestone1.9.1b2pre
Bug 394611 - Always prompt the user before changing a stored password. r=gavin (relanding with workaround to avoid leak)
toolkit/components/passwordmgr/src/nsLoginManager.js
toolkit/components/passwordmgr/src/nsLoginManagerPrompter.js
toolkit/components/passwordmgr/test/Makefile.in
toolkit/components/passwordmgr/test/notification_common.js
toolkit/components/passwordmgr/test/subtst_notifications_10.html
toolkit/components/passwordmgr/test/subtst_notifications_8.html
toolkit/components/passwordmgr/test/subtst_notifications_9.html
toolkit/components/passwordmgr/test/test_notifications.html
toolkit/components/passwordmgr/test/test_prompt.html
--- a/toolkit/components/passwordmgr/src/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/src/nsLoginManager.js
@@ -866,36 +866,21 @@ LoginManager.prototype = {
                 existingLogin = login;
                 break;
             }
         }
 
         if (existingLogin) {
             this.log("Found an existing login matching this form submission");
 
-            /*
-             * Change password if needed.
-             *
-             * If the login has a username, change the password w/o prompting
-             * (because we can be fairly sure there's only one password
-             * associated with the username). But for logins without a
-             * username, ask the user... Some sites use a password-only "login"
-             * in different contexts (enter your PIN, answer a security
-             * question, etc), and without a username we can't be sure if
-             * modifying an existing login is the right thing to do.
-             */
+            // Change password if needed.
             if (existingLogin.password != formLogin.password) {
-                if (formLogin.username) {
-                    this.log("...Updating password for existing login.");
-                    this.modifyLogin(existingLogin, formLogin);
-                } else {
-                    this.log("...passwords differ, prompting to change.");
-                    prompter = getPrompter(win);
-                    prompter.promptToChangePassword(existingLogin, formLogin);
-                }
+                this.log("...passwords differ, prompting to change.");
+                prompter = getPrompter(win);
+                prompter.promptToChangePassword(existingLogin, formLogin);
             }
 
             return;
         }
 
 
         // Prompt user to save login (via dialog or notification bar)
         prompter = getPrompter(win);
--- a/toolkit/components/passwordmgr/src/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/src/nsLoginManagerPrompter.js
@@ -424,17 +424,17 @@ LoginManagerPrompter.prototype = {
 
             this.log("===== promptAuth called =====");
 
             // If the user submits a login but it fails, we need to remove the
             // notification bar that was displayed. Conveniently, the user will
             // be prompted for authentication again, which brings us here.
             var notifyBox = this._getNotifyBox();
             if (notifyBox)
-                this._removeSaveLoginNotification(notifyBox);
+                this._removeLoginNotifications(notifyBox);
 
             var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
 
 
             // Looks for existing logins to prefill the prompt with.
             var foundLogins = this._pwmgr.findLogins({},
                                         hostname, null, httpRealm);
             this.log("found " + foundLogins.length + " matching logins.");
@@ -498,18 +498,21 @@ LoginManagerPrompter.prototype = {
                     this._showSaveLoginNotification(notifyBox, newLogin);
                 else
                     this._pwmgr.addLogin(newLogin);
 
             } else if (password != selectedLogin.password) {
 
                 this.log("Updating password for " + username +
                          " @ " + hostname + " (" + httpRealm + ")");
-                // update password
-                this._pwmgr.modifyLogin(selectedLogin, newLogin);
+                if (notifyBox)
+                    this._showChangeLoginNotification(notifyBox,
+                                                      selectedLogin, newLogin);
+                else
+                    this._pwmgr.modifyLogin(selectedLogin, newLogin);
 
             } else {
                 this.log("Login unchanged, no further action needed.");
             }
         } catch (e) {
             Components.utils.reportError("LoginManagerPrompter: " +
                 "Fail2 in promptAuth: " + e + "\n");
         }
@@ -659,27 +662,31 @@ LoginManagerPrompter.prototype = {
         ];
 
         this._showLoginNotification(aNotifyBox, "password-save",
              notificationText, buttons);
     },
 
 
     /*
-     * _removeSaveLoginNotification
+     * _removeLoginNotifications
      *
      */
-    _removeSaveLoginNotification : function (aNotifyBox) {
-
+    _removeLoginNotifications : function (aNotifyBox) {
         var oldBar = aNotifyBox.getNotificationWithValue("password-save");
-
         if (oldBar) {
             this.log("Removing save-password notification bar.");
             aNotifyBox.removeNotification(oldBar);
         }
+
+        oldBar = aNotifyBox.getNotificationWithValue("password-change");
+        if (oldBar) {
+            this.log("Removing change-password notification bar.");
+            aNotifyBox.removeNotification(oldBar);
+        }
     },
 
 
     /*
      * _showSaveLoginDialog
      *
      * Called when we detect a new login in a form submission,
      * asks the user what to do.
--- a/toolkit/components/passwordmgr/test/Makefile.in
+++ b/toolkit/components/passwordmgr/test/Makefile.in
@@ -72,24 +72,28 @@ MOCHI_TESTS = \
 		test_xhr.html \
 		test_xml_load.html \
 		test_zzz_finish.html \
 		$(NULL)
 
 MOCHI_CONTENT = \
 		pwmgr_common.js \
 		prompt_common.js \
+		notification_common.js \
 		authenticate.sjs \
 		formsubmit.sjs \
 		subtst_notifications_1.html \
 		subtst_notifications_2.html \
 		subtst_notifications_3.html \
 		subtst_notifications_4.html \
 		subtst_notifications_5.html \
 		subtst_notifications_6.html \
+		subtst_notifications_8.html \
+		subtst_notifications_9.html \
+		subtst_notifications_10.html \
 		$(NULL)
 
 XPCSHELL_TESTS  = unit
 
 # This test doesn't pass because we can't ensure a cross-platform
 # event that occurs between DOMContentLoaded and Pageload
 # test_bug_221634.html
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/notification_common.js
@@ -0,0 +1,54 @@
+/*
+ *  getNotificationBox
+ *
+ * Fetches the notification box for the specified window.
+ */
+function getNotificationBox(aWindow) {
+    var chromeWin = aWindow
+                        .QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIWebNavigation)
+                        .QueryInterface(Ci.nsIDocShellTreeItem)
+                        .rootTreeItem
+                        .QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDOMWindow)
+                        .QueryInterface(Ci.nsIDOMChromeWindow);
+
+    // Don't need .wrappedJSObject here, unlike when chrome does this.
+    var notifyBox = chromeWin.getNotificationBox(aWindow);
+    return notifyBox;
+}
+
+
+/*
+ * getNotificationBar
+ *
+ */
+function getNotificationBar(aBox, aKind) {
+    ok(true, "Looking for " + aKind + " notification bar");
+    // Sometimes callers wants a bar, sometimes not. Allow 0 or 1, but not 2+.
+    ok(aBox.allNotifications.length <= 1, "Checking for multiple notifications");
+    return aBox.getNotificationWithValue(aKind);
+}
+
+
+/*
+ * clickNotificationButton
+ *
+ * Clicks the specified notification button.
+ */
+function clickNotificationButton(aBar, aButtonName) {
+    // This is a bit of a hack. The notification doesn't have an API to
+    // trigger buttons, so we dive down into the implementation and twiddle
+    // the buttons directly.
+    var buttons = aBar.getElementsByTagName("button");
+    var clicked = false;
+    for (var i = 0; i < buttons.length; i++) {
+        if (buttons[i].label == aButtonName) {
+            buttons[i].click();
+            clicked = true;
+            break;
+        }
+    }
+
+    ok(clicked, "Clicked \"" + aButtonName + "\" button"); 
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_notifications_10.html
@@ -0,0 +1,25 @@
+<html>
+<head>
+  <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 10</h2>
+<form id="form" action="formsubmit.sjs">
+  <input id="pass" name="pass" type="password">
+  <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+  passField.value = "notifyp1";
+  form.submit();
+}
+
+window.onload = submitForm;
+var form      = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_notifications_8.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+  <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 8</h2>
+<form id="form" action="formsubmit.sjs">
+  <input id="user" name="user">
+  <input id="pass" name="pass" type="password">
+  <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+  userField.value = "notifyu1";
+  passField.value = "pass2";
+  form.submit();
+}
+
+window.onload = submitForm;
+var form      = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_notifications_9.html
@@ -0,0 +1,27 @@
+<html>
+<head>
+  <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 9</h2>
+<form id="form" action="formsubmit.sjs">
+  <input id="user" name="user">
+  <input id="pass" name="pass" type="password">
+  <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+  userField.value = "";
+  passField.value = "pass2";
+  form.submit();
+}
+
+window.onload = submitForm;
+var form      = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
--- a/toolkit/components/passwordmgr/test/test_notifications.html
+++ b/toolkit/components/passwordmgr/test/test_notifications.html
@@ -1,15 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for Login Manager</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>  
   <script type="text/javascript" src="pwmgr_common.js"></script>
+  <script type="text/javascript" src="notification_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: notifications
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
@@ -31,89 +32,23 @@ var subtests = [
                    "subtst_notifications_1.html", // 8
                    "subtst_notifications_2.html", // 9
                    "subtst_notifications_3.html", // 10
                    "subtst_notifications_4.html", // 11
                    "subtst_notifications_5.html", // 12
                    "subtst_notifications_1.html", // 13
                    "subtst_notifications_6.html", // 14
                    "subtst_notifications_1.html", // 15
-                   "subtst_notifications_6.html"
+                   "subtst_notifications_6.html", // 16
+                   "subtst_notifications_8.html", // 17
+                   "subtst_notifications_8.html", // 18
+                   "subtst_notifications_9.html", // 19
+                   "subtst_notifications_10.html"  // 20
                ];
 
-/*
- *  getNotificationBox
- *
- * Fetches the notification box for the specified window.
- */
-function getNotificationBox(aWindow) {
-/*
-    var chromeWin = aWindow
-                        .QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Ci.nsIWebNavigation)
-                        .QueryInterface(Ci.nsIDocShellTreeItem)
-                        .rootTreeItem
-                        .QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Ci.nsIDOMWindow);
-    // Don't need .wrappedJSObject here, unlike when chrome does this.
-    var browserWin = chromeWin.browserDOMWindow;
-    var notifyBox = browserWin.getNotificationBox(aWindow);
-    return notifyBox;
-*/
-    // Find the <browser> which contains aWindow, by looking
-    // through all the open windows and all the <browsers> in each.
-    var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
-             getService(Ci.nsIWindowMediator);
-    var enumerator = wm.getEnumerator("navigator:browser");
-    var tabbrowser = null;
-    var foundBrowser = null;
-
-    while (!foundBrowser && enumerator.hasMoreElements()) {
-        var win = enumerator.getNext();
-        tabbrowser = win.getBrowser(); 
-        foundBrowser = tabbrowser.getBrowserForDocument(aWindow.document);
-    }
-
-    // Return the notificationBox associated with the browser.
-    return tabbrowser.getNotificationBox(foundBrowser);
-}
-
-
-/*
- * getNotificationBar
- *
- */
-function getNotificationBar(aBox, aKind) {
-    ok(true, "Looking for " + aKind + " notification bar");
-    return aBox.getNotificationWithValue(aKind);
-}
-
-
-/*
- * clickNotificationButton
- *
- * Clicks the specified notification button.
- */
-function clickNotificationButton(aBar, aButtonName) {
-    // This is a bit of a hack. The notification doesn't have an API to
-    // trigger buttons, so we dive down into the implementation and twiddle
-    // the buttons directly.
-    var buttons = aBar.getElementsByTagName("button");
-    var clicked = false;
-    for (var i = 0; i < buttons.length; i++) {
-        if (buttons[i].label == aButtonName) {
-            buttons[i].click();
-            clicked = true;
-            break;
-        }
-    }
-
-    ok(clicked, "Clicked \"" + aButtonName + "\" button"); 
-}
-
 
 var ignoreLoad = false;
 function handleLoad(aEvent) {
     netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
     // ignore every other load event ... We get one for loading the subtest (which
     // we want to ignore), and another when the subtest's form submits itself
     // (which we want to handle, to start the next test).
@@ -317,16 +252,69 @@ function checkTest() {
       case 16:
         // Check for notification bar when pw-only form doesn't match existing login.
         is(gotUser, "null",     "Checking submitted username");
         is(gotPass, "notifyp1", "Checking submitted password");
         bar = getNotificationBar(notifyBox, "password-save");
         ok(bar, "got notification bar");
         clickNotificationButton(bar, "Not Now");
         pwmgr.removeLogin(login1B);
+
+        // Add login for the next tests
+        pwmgr.addLogin(login1);
+        break;
+
+      case 17:
+        // Check for change-password bar, u+p login on u+p form. (not changed)
+        is(gotUser, "notifyu1", "Checking submitted username");
+        is(gotPass, "pass2",    "Checking submitted password");
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Don't Change");
+        break;
+
+      case 18:
+        // Check for change-password bar, u+p login on u+p form.
+        is(gotUser, "notifyu1", "Checking submitted username");
+        is(gotPass, "pass2",    "Checking submitted password");
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Change");
+
+        // cleanup
+        login1.password = "pass2";
+        pwmgr.removeLogin(login1);
+        login1.password = "notifyp1";
+
+        // Add login for the next test
+        pwmgr.addLogin(login2);
+        break;
+
+      // ...can't change a u+p login on a p-only form...
+
+      case 19:
+        // Check for change-password bar, p-only login on u+p form.
+        // (needed a different subtest for this because the login created in
+        // test_0init was interfering)
+        is(gotUser, "",         "Checking submitted username");
+        is(gotPass, "pass2",    "Checking submitted password");
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Change");
+        break;
+
+      case 20:
+        // Check for change-password bar, p-only login on p-only form.
+        is(gotUser, "null",     "Checking submitted username");
+        is(gotPass, "notifyp1", "Checking submitted password");
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Change");
+
+        pwmgr.removeLogin(login2);
         break;
 
       default:
         ok(false, "Unexpected call to checkTest for test #" + testNum);
 
     }
 
     // TODO:
--- a/toolkit/components/passwordmgr/test/test_prompt.html
+++ b/toolkit/components/passwordmgr/test/test_prompt.html
@@ -1,79 +1,96 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for Login Manager</title>
   <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>  
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="notification_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: username/password prompts
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Login Manager: username / password prompts. **/
 netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
-var pwmgr, login1, login2A, login2B, login3A, login3B;
+var pwmgr, tmplogin, login1, login2A, login2B, login3A, login3B, login4;
 
 function initLogins() {
   pwmgr = Cc["@mozilla.org/login-manager;1"].
           getService(Ci.nsILoginManager);
 
+  tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+             createInstance(Ci.nsILoginInfo);
+
   login1  = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login3A = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
   login3B = Cc["@mozilla.org/login-manager/loginInfo;1"].
             createInstance(Ci.nsILoginInfo);
+  login4  = Cc["@mozilla.org/login-manager/loginInfo;1"].
+            createInstance(Ci.nsILoginInfo);
 
   login1.init("http://example.com", null, "http://example.com",
               "", "examplepass", "", "");
   login2A.init("http://example2.com", null, "http://example2.com",
                "user1name", "user1pass", "", "");
   login2B.init("http://example2.com", null, "http://example2.com",
                "user2name", "user2pass", "", "");
-
   login3A.init("http://localhost:8888", null, "mochitest",
                "mochiuser1", "mochipass1", "", "");
   login3B.init("http://localhost:8888", null, "mochitest2",
                "mochiuser2", "mochipass2", "", "");
+  login4.init("http://localhost:8888", null, "mochitest3",
+               "mochiuser3", "mochipass3-old", "", "");
 
   pwmgr.addLogin(login1);
   pwmgr.addLogin(login2A);
   pwmgr.addLogin(login2B);
   pwmgr.addLogin(login3A);
   pwmgr.addLogin(login3B);
+  pwmgr.addLogin(login4);
 }
 
 function finishTest() {
   ok(true, "finishTest removing testing logins...");
   pwmgr.removeLogin(login1);
   pwmgr.removeLogin(login2A);
   pwmgr.removeLogin(login2B);
   pwmgr.removeLogin(login3A);
   pwmgr.removeLogin(login3B);
+  pwmgr.removeLogin(login4);
 
   SimpleTest.finish();
 }
 
+/*
+ * handleDialog
+ *
+ * Invoked a short period of time after calling startCallbackTimer(), and
+ * allows testing the actual auth dialog while it's being displayed. Tests
+ * should call startCallbackTimer() each time the auth dialog is expected (the
+ * timer is a one-shot).
+ */
 function handleDialog(doc, testNum) {
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
   ok(true, "handleDialog running for test " + testNum);
 
   var clickOK = true;
   var userfield = doc.getElementById("loginTextbox");
   var passfield = doc.getElementById("password1Textbox");
   var username = userfield.getAttribute("value");
@@ -230,31 +247,57 @@ function handleDialog(doc, testNum) {
         is(password, "mochipass1", "Checking filled password");
         break;
 
     case 1001:
         is(username, "mochiuser2", "Checking filled username");
         is(password, "mochipass2", "Checking filled password");
         break;
 
+    // (1002 doesn't trigger a dialog)
+
+    case 1003:
+        is(username, "mochiuser1", "Checking filled username");
+        is(password, "mochipass1", "Checking filled password");
+        passfield.setAttribute("value", "mochipass1-new");
+        break;
+
+    case 1004:
+        is(username, "mochiuser3", "Checking filled username");
+        is(password, "mochipass3-old", "Checking filled password");
+        passfield.setAttribute("value", "mochipass3-new");
+        break;
+
+    case 1005:
+        is(username, "", "Checking filled username");
+        is(password, "", "Checking filled password");
+        userfield.setAttribute("value", "mochiuser3");
+        passfield.setAttribute("value", "mochipass3-old");
+        break;
+
     default:
         ok(false, "Uhh, unhandled switch for testNum #" + testNum);
         break;
   }
 
   if (clickOK)
     dialog.acceptDialog();
   else
     dialog.cancelDialog();
 
   ok(true, "handleDialog done");
   didDialog = true;
 }
 
 
+/*
+ * handleLoad
+ *
+ * Called when a load event is fired at the subtest's iframe.
+ */
 function handleLoad() {
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
   ok(true, "handleLoad running for test " + testNum);
 
   if (testNum != 1002)
     ok(didDialog, "handleDialog was invoked");
 
   // The server echos back the user/pass it received.
@@ -290,16 +333,83 @@ function handleLoad() {
 
     case 1002:
         testNum++;
         ok(!didDialog, "handleDialog was NOT invoked");
         is(authok, "PASS", "Checking for successful authentication");
         is(username, "mochiuser1", "Checking for echoed username");
         is(password, "mochipass1", "Checking for echoed password");
 
+        // Same realm we've already authenticated to, but with a different
+        // expected password (to trigger an auth prompt, and change-password
+        // notification bar).
+        startCallbackTimer();
+        iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
+        break;
+
+    case 1003:
+        testNum++;
+        is(authok, "PASS", "Checking for successful authentication");
+        is(username, "mochiuser1", "Checking for echoed username");
+        is(password, "mochipass1-new", "Checking for echoed password");
+
+        // Check for the notification bar, and change the password.
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Change");
+
+        // Housekeeping: change it back
+        tmpLogin.init("http://localhost:8888", null, "mochitest",
+                      "mochiuser1", "mochipass1-new", "", "");
+        pwmgr.modifyLogin(tmpLogin, login3A);
+
+        // Same as last test, but for a realm we haven't already authenticated
+        // to (but have an existing saved login for, so that we'll trigger
+        // a change-password notification bar.
+        startCallbackTimer();
+        iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
+        break;
+
+    case 1004:
+        testNum++;
+        is(authok, "PASS", "Checking for successful authentication");
+        is(username, "mochiuser3", "Checking for echoed username");
+        is(password, "mochipass3-new", "Checking for echoed password");
+
+        // Check for the notification bar, and change the password.
+        bar = getNotificationBar(notifyBox, "password-change");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Change");
+
+        // Housekeeping: change it back to the original login4. Actually,
+        // just delete it and we'll re-add it as the next test.
+        tmpLogin.init("http://localhost:8888", null, "mochitest3",
+                      "mochiuser3", "mochipass3-new", "", "");
+        pwmgr.removeLogin(tmpLogin);
+        // Clear cached auth from this subtest, and avoid leaking due to bug 459620.
+        var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].
+                      getService(Ci.nsIHttpAuthManager);
+        authMgr.clearAll();
+
+        // Trigger a new prompt, so we can test adding a new login.
+        startCallbackTimer();
+        iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
+        break;
+
+    case 1005:
+        testNum++;
+        is(authok, "PASS", "Checking for successful authentication");
+        is(username, "mochiuser3", "Checking for echoed username");
+        is(password, "mochipass3-old", "Checking for echoed password");
+
+        // Check for the notification bar, and change the password.
+        bar = getNotificationBar(notifyBox, "password-save");
+        ok(bar, "got notification bar");
+        clickNotificationButton(bar, "Remember");
+
         finishTest();
         break;
 
     default:
         ok(false, "Uhh, unhandled switch for testNum #" + testNum);
         break;
   }
 
@@ -332,17 +442,24 @@ var prompter2 = promptFac.getPrompt(wind
 
 function dialogTitle() { return "nsILoginManagerPrompter test #" + testNum; }
 var dialogText  = "This dialog should be modified and dismissed by the test.";
 var uname  = { value : null };
 var pword  = { value : null };
 var result = { value : null };
 var isOk;
 
-// XXX Add test for host that doesn't yet exist to test login-saving logic
+// The notification box (not *bar*) is a constant, per-tab container. So, we
+// only need to fetch it once.
+var notifyBox = getNotificationBox(window.top);
+ok(notifyBox, "Got notification box");
+
+// Remove any notification bars that might be left over from other tests.
+notifyBox.removeAllNotifications(true);
+
 
 // ===== test 1 ===== 
 var testNum = 1;
 startCallbackTimer();
 isOk = prompter1.prompt(dialogTitle(), dialogText, "http://example.com",
                         Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
 
 ok(isOk, "Checking dialog return value (accept)");
@@ -684,18 +801,18 @@ is(authinfo.password, "user2pass", "Chec
 // XXX check NTLM domain stuff
 
 
 var iframe = document.getElementById("iframe");
 iframe.onload = handleLoad;
 
 // clear plain HTTP auth sessions before the test, to allow
 // running them more than once.
-var authMgr = Components.classes['@mozilla.org/network/http-auth-manager;1']
-                        .getService(Components.interfaces.nsIHttpAuthManager);
+var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].
+              getService(Ci.nsIHttpAuthManager);
 authMgr.clearAll();
 
 // ===== test 1000 =====
 testNum = 1000;
 startCallbackTimer();
 iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
 
 // ...remaining tests are driven by handleLoad()...