summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/pwmgr_common.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/pwmgr_common.js')
-rw-r--r--toolkit/components/passwordmgr/test/pwmgr_common.js509
1 files changed, 509 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/pwmgr_common.js b/toolkit/components/passwordmgr/test/pwmgr_common.js
new file mode 100644
index 000000000..fa7c4fd85
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/pwmgr_common.js
@@ -0,0 +1,509 @@
+const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
+
+/**
+ * Returns the element with the specified |name| attribute.
+ */
+function $_(formNum, name) {
+ var form = document.getElementById("form" + formNum);
+ if (!form) {
+ logWarning("$_ couldn't find requested form " + formNum);
+ return null;
+ }
+
+ var element = form.children.namedItem(name);
+ if (!element) {
+ logWarning("$_ couldn't find requested element " + name);
+ return null;
+ }
+
+ // Note that namedItem is a bit stupid, and will prefer an
+ // |id| attribute over a |name| attribute when looking for
+ // the element. Login Mananger happens to use .namedItem
+ // anyway, but let's rigorously check it here anyway so
+ // that we don't end up with tests that mistakenly pass.
+
+ if (element.getAttribute("name") != name) {
+ logWarning("$_ got confused.");
+ return null;
+ }
+
+ return element;
+}
+
+/**
+ * Check a form for expected values. If an argument is null, a field's
+ * expected value will be the default value.
+ *
+ * <form id="form#">
+ * checkForm(#, "foo");
+ */
+function checkForm(formNum, val1, val2, val3) {
+ var e, form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ var numToCheck = arguments.length - 1;
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[0];
+ if (val1 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val1, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[1];
+ if (val2 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val2, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[2];
+ if (val3 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val3, "Test value of field " + e.name +
+ " in form " + formNum);
+}
+
+/**
+ * Check a form for unmodified values from when page was loaded.
+ *
+ * <form id="form#">
+ * checkUnmodifiedForm(#);
+ */
+function checkUnmodifiedForm(formNum) {
+ var form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ for (var i = 0; i < form.elements.length; i++) {
+ var ele = form.elements[i];
+
+ // No point in checking form submit/reset buttons.
+ if (ele.type == "submit" || ele.type == "reset")
+ continue;
+
+ is(ele.value, ele.defaultValue, "Test to default value of field " +
+ ele.name + " in form " + formNum);
+ }
+}
+
+/**
+ * Mochitest gives us a sendKey(), but it's targeted to a specific element.
+ * This basically sends an untargeted key event, to whatever's focused.
+ */
+function doKey(aKey, modifier) {
+ var keyName = "DOM_VK_" + aKey.toUpperCase();
+ var key = KeyEvent[keyName];
+
+ // undefined --> null
+ if (!modifier)
+ modifier = null;
+
+ // Window utils for sending fake sey events.
+ var wutils = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+
+ if (wutils.sendKeyEvent("keydown", key, 0, modifier)) {
+ wutils.sendKeyEvent("keypress", key, 0, modifier);
+ }
+ wutils.sendKeyEvent("keyup", key, 0, modifier);
+}
+
+/**
+ * Init with a common login
+ * If selfFilling is true or non-undefined, fires an event at the page so that
+ * the test can start checking filled-in values. Tests that check observer
+ * notifications might be confused by this.
+ */
+function commonInit(selfFilling) {
+ var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"].
+ getService(SpecialPowers.Ci.nsILoginManager);
+ ok(pwmgr != null, "Access LoginManager");
+
+ // Check that initial state has no logins
+ var logins = pwmgr.getAllLogins();
+ is(logins.length, 0, "Not expecting logins to be present");
+ var disabledHosts = pwmgr.getAllDisabledHosts();
+ if (disabledHosts.length) {
+ ok(false, "Warning: wasn't expecting disabled hosts to be present.");
+ for (var host of disabledHosts)
+ pwmgr.setLoginSavingEnabled(host, true);
+ }
+
+ // Add a login that's used in multiple tests
+ var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(SpecialPowers.Ci.nsILoginInfo);
+ login.init("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "testuser", "testpass", "uname", "pword");
+ pwmgr.addLogin(login);
+
+ // Last sanity check
+ logins = pwmgr.getAllLogins();
+ is(logins.length, 1, "Checking for successful init login");
+ disabledHosts = pwmgr.getAllDisabledHosts();
+ is(disabledHosts.length, 0, "Checking for no disabled hosts");
+
+ if (selfFilling)
+ return;
+
+ if (this.sendAsyncMessage) {
+ sendAsyncMessage("registerRunTests");
+ } else {
+ registerRunTests();
+ }
+}
+
+function registerRunTests() {
+ return new Promise(resolve => {
+ // We provide a general mechanism for our tests to know when they can
+ // safely run: we add a final form that we know will be filled in, wait
+ // for the login manager to tell us that it's filled in and then continue
+ // with the rest of the tests.
+ window.addEventListener("DOMContentLoaded", (event) => {
+ var form = document.createElement('form');
+ form.id = 'observerforcer';
+ var username = document.createElement('input');
+ username.name = 'testuser';
+ form.appendChild(username);
+ var password = document.createElement('input');
+ password.name = 'testpass';
+ password.type = 'password';
+ form.appendChild(password);
+
+ var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
+ var formLikeRoot = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
+ if (formLikeRoot.id !== 'observerforcer')
+ return;
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ formLikeRoot.remove();
+ SimpleTest.executeSoon(() => {
+ var runTestEvent = new Event("runTests");
+ window.dispatchEvent(runTestEvent);
+ resolve();
+ });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+
+ document.body.appendChild(form);
+ });
+ });
+}
+
+const masterPassword = "omgsecret!";
+
+function enableMasterPassword() {
+ setMasterPassword(true);
+}
+
+function disableMasterPassword() {
+ setMasterPassword(false);
+}
+
+function setMasterPassword(enable) {
+ var oldPW, newPW;
+ if (enable) {
+ oldPW = "";
+ newPW = masterPassword;
+ } else {
+ oldPW = masterPassword;
+ newPW = "";
+ }
+ // Set master password. Note that this does not log you in, so the next
+ // invocation of pwmgr can trigger a MP prompt.
+
+ var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
+ var token = pk11db.findTokenByName("");
+ info("MP change from " + oldPW + " to " + newPW);
+ token.changePassword(oldPW, newPW);
+}
+
+function logoutMasterPassword() {
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+}
+
+function dumpLogins(pwmgr) {
+ var logins = pwmgr.getAllLogins();
+ ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
+ for (var i = 0; i < logins.length; i++)
+ dumpLogin("login #" + i + " --- ", logins[i]);
+}
+
+function dumpLogin(label, login) {
+ var loginText = "";
+ loginText += "host: ";
+ loginText += login.hostname;
+ loginText += " / formURL: ";
+ loginText += login.formSubmitURL;
+ loginText += " / realm: ";
+ loginText += login.httpRealm;
+ loginText += " / user: ";
+ loginText += login.username;
+ loginText += " / pass: ";
+ loginText += login.password;
+ loginText += " / ufield: ";
+ loginText += login.usernameField;
+ loginText += " / pfield: ";
+ loginText += login.passwordField;
+ ok(true, label + loginText);
+}
+
+function getRecipeParent() {
+ var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
+ if (!LoginManagerParent.recipeParentPromise) {
+ return null;
+ }
+ return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
+ return SpecialPowers.wrap(recipeParent);
+ });
+}
+
+/**
+ * Resolves when a specified number of forms have been processed.
+ */
+function promiseFormsProcessed(expectedCount = 1) {
+ var processedCount = 0;
+ return new Promise((resolve, reject) => {
+ function onProcessedForm(subject, topic, data) {
+ processedCount++;
+ if (processedCount == expectedCount) {
+ SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
+ resolve(SpecialPowers.Cu.waiveXrays(subject), data);
+ }
+ }
+ SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false);
+ });
+}
+
+function loadRecipes(recipes) {
+ info("Loading recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("loadedRecipes", function loaded() {
+ chromeScript.removeMessageListener("loadedRecipes", loaded);
+ resolve(recipes);
+ });
+ chromeScript.sendAsyncMessage("loadRecipes", recipes);
+ });
+}
+
+function resetRecipes() {
+ info("Resetting recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("recipesReset", function reset() {
+ chromeScript.removeMessageListener("recipesReset", reset);
+ resolve();
+ });
+ chromeScript.sendAsyncMessage("resetRecipes");
+ });
+}
+
+function promiseStorageChanged(expectedChangeTypes) {
+ return new Promise((resolve, reject) => {
+ function onStorageChanged({ topic, data }) {
+ let changeType = expectedChangeTypes.shift();
+ is(data, changeType, "Check expected passwordmgr-storage-changed type");
+ if (expectedChangeTypes.length === 0) {
+ chromeScript.removeMessageListener("storageChanged", onStorageChanged);
+ resolve();
+ }
+ }
+ chromeScript.addMessageListener("storageChanged", onStorageChanged);
+ });
+}
+
+function promisePromptShown(expectedTopic) {
+ return new Promise((resolve, reject) => {
+ function onPromptShown({ topic, data }) {
+ is(topic, expectedTopic, "Check expected prompt topic");
+ chromeScript.removeMessageListener("promptShown", onPromptShown);
+ resolve();
+ }
+ chromeScript.addMessageListener("promptShown", onPromptShown);
+ });
+}
+
+/**
+ * Run a function synchronously in the parent process and destroy it in the test cleanup function.
+ * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
+ * or the URL to a JS file.
+ * @return {Object} - the return value of loadChromeScript providing message-related methods.
+ * @see loadChromeScript in specialpowersAPI.js
+ */
+function runInParent(aFunctionOrURL) {
+ let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+ return chromeScript;
+}
+
+/**
+ * Run commonInit synchronously in the parent then run the test function after the runTests event.
+ *
+ * @param {Function} aFunction The test function to run
+ */
+function runChecksAfterCommonInit(aFunction = null) {
+ SimpleTest.waitForExplicitFinish();
+ let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+ if (aFunction) {
+ window.addEventListener("runTests", aFunction);
+ pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests());
+ }
+ pwmgrCommonScript.sendSyncMessage("setupParent");
+ return pwmgrCommonScript;
+}
+
+// Code to run when loaded as a chrome script in tests via loadChromeScript
+if (this.addMessageListener) {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ var SpecialPowers = { Cc, Ci, Cr, Cu, };
+ var ok, is;
+ // Ignore ok/is in commonInit since they aren't defined in a chrome script.
+ ok = is = () => {}; // eslint-disable-line no-native-reassign
+
+ Cu.import("resource://gre/modules/LoginHelper.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ function onStorageChanged(subject, topic, data) {
+ sendAsyncMessage("storageChanged", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed", false);
+
+ function onPrompt(subject, topic, data) {
+ sendAsyncMessage("promptShown", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change", false);
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save", false);
+
+ addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
+ // Force LoginManagerParent to init for the tests since it's normally delayed
+ // by apps such as on Android.
+ LoginManagerParent.init();
+
+ commonInit(selfFilling);
+ sendAsyncMessage("doneSetup");
+ });
+
+ addMessageListener("loadRecipes", Task.async(function*(recipes) {
+ var recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.load(recipes);
+ sendAsyncMessage("loadedRecipes", recipes);
+ }));
+
+ addMessageListener("resetRecipes", Task.async(function*() {
+ let recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.reset();
+ sendAsyncMessage("recipesReset");
+ }));
+
+ addMessageListener("proxyLoginManager", msg => {
+ // Recreate nsILoginInfo objects from vanilla JS objects.
+ let recreatedArgs = msg.args.map((arg, index) => {
+ if (msg.loginInfoIndices.includes(index)) {
+ return LoginHelper.vanillaObjectToLogin(arg);
+ }
+
+ return arg;
+ });
+
+ let rv = Services.logins[msg.methodName](...recreatedArgs);
+ if (rv instanceof Ci.nsILoginInfo) {
+ rv = LoginHelper.loginToVanillaObject(rv);
+ }
+ return rv;
+ });
+
+ var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+ globalMM.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
+ sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
+ });
+} else {
+ // Code to only run in the mochitest pages (not in the chrome script).
+ SpecialPowers.pushPrefEnv({"set": [["signon.autofillForms.http", true],
+ ["security.insecure_field_warning.contextual.enabled", false]]
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.popPrefEnv();
+ runInParent(function cleanupParent() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+
+ // Remove all logins and disabled hosts
+ Services.logins.removeAllLogins();
+
+ let disabledHosts = Services.logins.getAllDisabledHosts();
+ disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
+
+ let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ if (LoginManagerParent._recipeManager) {
+ LoginManagerParent._recipeManager.reset();
+ }
+
+ // Cleanup PopupNotifications (if on a relevant platform)
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (chromeWin && chromeWin.PopupNotifications) {
+ let notes = chromeWin.PopupNotifications._currentNotifications;
+ if (notes.length > 0) {
+ dump("Removing " + notes.length + " popup notifications.\n");
+ }
+ for (let note of notes) {
+ note.remove();
+ }
+ }
+ });
+ });
+
+
+ let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
+ /**
+ * Proxy for Services.logins (nsILoginManager).
+ * Only supports arguments which support structured clone plus {nsILoginInfo}
+ * Assumes properties are methods.
+ */
+ this.LoginManager = new Proxy({}, {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let loginInfoIndices = [];
+ let cloneableArgs = args.map((val, index) => {
+ if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
+ loginInfoIndices.push(index);
+ return LoginHelper.loginToVanillaObject(val);
+ }
+
+ return val;
+ });
+
+ return chromeScript.sendSyncMessage("proxyLoginManager", {
+ args: cloneableArgs,
+ loginInfoIndices,
+ methodName: prop,
+ })[0][0];
+ };
+ },
+ });
+}