Bug 1699705 - Allow autocomplete to use related realm credentials. r=sfoster,leplatrem

Differential Revision: https://phabricator.services.mozilla.com/D91205
This commit is contained in:
Tim Giles 2021-03-23 20:21:13 +00:00
parent 3f5687ca08
commit 20c7eb2867
21 changed files with 651 additions and 14 deletions

View File

@ -1935,6 +1935,7 @@ pref("signon.management.page.breachAlertUrl",
"https://monitor.firefox.com/breach-details/");
pref("signon.management.page.showPasswordSyncNotification", true);
pref("signon.passwordEditCapture.enabled", true);
pref("signon.relatedRealms.enabled", false);
pref("signon.showAutoCompleteFooter", true);
pref("signon.showAutoCompleteImport", "import");
pref("signon.suggestImportCount", 3);

View File

@ -3675,6 +3675,7 @@ pref("signon.userInputRequiredToCapture.enabled", true);
pref("signon.debug", false);
pref("signon.recipes.path", "resource://app/defaults/settings/main/password-recipes.json");
pref("signon.recipes.remoteRecipesEnabled", true);
pref("signon.relatedRealms.enabled", false);
pref("signon.schemeUpgrades", true);
pref("signon.includeOtherSubdomainsInLookup", true);

View File

@ -371,6 +371,8 @@ this.LoginHelper = {
privateBrowsingCaptureEnabled: null,
remoteRecipesEnabled: null,
remoteRecipesCollection: "password-recipes",
relatedRealmsEnabled: null,
relatedRealmsCollection: "websites-with-shared-credential-backends",
schemeUpgrades: null,
showAutoCompleteFooter: null,
showAutoCompleteImport: null,
@ -470,6 +472,9 @@ this.LoginHelper = {
this.remoteRecipesEnabled = Services.prefs.getBoolPref(
"signon.recipes.remoteRecipesEnabled"
);
this.relatedRealmsEnabled = Services.prefs.getBoolPref(
"signon.relatedRealms.enabled"
);
},
createLogger(aLogPrefix) {
@ -682,6 +687,8 @@ this.LoginHelper = {
schemeUpgrades: false,
acceptWildcardMatch: false,
acceptDifferentSubdomains: false,
acceptRelatedRealms: false,
relatedRealms: [],
}
) {
if (aLoginOrigin == aSearchOrigin) {
@ -718,6 +725,18 @@ this.LoginHelper = {
) {
return true;
}
if (
aOptions.acceptRelatedRealms &&
aOptions.relatedRealms.length &&
(loginURI.scheme == searchURI.scheme ||
(aOptions.schemeUpgrades && schemeMatches))
) {
for (let relatedOrigin of aOptions.relatedRealms) {
if (Services.eTLD.hasRootDomain(loginURI.host, relatedOrigin)) {
return true;
}
}
}
}
if (

View File

@ -322,7 +322,6 @@ LoginManager.prototype = {
if (matchingLogin) {
throw LoginHelper.createLoginAlreadyExistsError(matchingLogin.guid);
}
log.debug("Adding login");
return this._storage.addLogin(login);
},

View File

@ -22,6 +22,13 @@ XPCOMUtils.defineLazyGetter(this, "autocompleteFeature", () => {
return new ExperimentFeature("password-autocomplete");
});
XPCOMUtils.defineLazyGetter(this, "LoginRelatedRealmsParent", () => {
const { LoginRelatedRealmsParent } = ChromeUtils.import(
"resource://gre/modules/LoginRelatedRealms.jsm"
);
return new LoginRelatedRealmsParent();
});
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
XPCOMUtils.defineLazyModuleGetters(this, {
@ -186,6 +193,7 @@ class LoginManagerParent extends JSWindowActorParent {
* @param {origin?} options.httpRealm To match on. Omit this argument to match all realms.
* @param {boolean} options.acceptDifferentSubdomains Include results for eTLD+1 matches
* @param {boolean} options.ignoreActionAndRealm Include all form and HTTP auth logins for the site
* @param {string[]} options.relatedRealms Related realms to match against when searching
*/
static async searchAndDedupeLogins(
formOrigin,
@ -194,6 +202,7 @@ class LoginManagerParent extends JSWindowActorParent {
formActionOrigin,
httpRealm,
ignoreActionAndRealm,
relatedRealms,
} = {}
) {
let logins;
@ -209,6 +218,10 @@ class LoginManagerParent extends JSWindowActorParent {
matchData.httpRealm = httpRealm;
}
}
if (LoginHelper.relatedRealmsEnabled) {
matchData.acceptRelatedRealms = LoginHelper.relatedRealmsEnabled;
matchData.relatedRealms = relatedRealms;
}
try {
logins = await Services.logins.searchLoginsAsync(matchData);
} catch (e) {
@ -535,11 +548,22 @@ class LoginManagerParent extends JSWindowActorParent {
origin: formOrigin,
});
} else {
let relatedRealmsOrigins = [];
if (LoginHelper.relatedRealmsEnabled) {
relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms(
formOrigin
);
}
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
formActionOrigin: actionOrigin,
ignoreActionAndRealm: true,
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
relatedRealms: relatedRealmsOrigins,
});
debug(
"Adding related logins on page load",
logins.map(l => l.origin)
);
}
log("sendLoginDataToChild:", logins.length, "deduped logins");
@ -606,12 +630,18 @@ class LoginManagerParent extends JSWindowActorParent {
logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
} else {
log("Creating new autocomplete search result.");
let relatedRealmsOrigins = [];
if (LoginHelper.relatedRealmsEnabled) {
relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms(
formOrigin
);
}
// Autocomplete results do not need to match actionOrigin or exact origin.
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
formActionOrigin: actionOrigin,
ignoreActionAndRealm: true,
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
relatedRealms: relatedRealmsOrigins,
});
}

View File

@ -0,0 +1,118 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
RemoteSettings: "resource://services-settings/remote-settings.js",
});
XPCOMUtils.defineLazyGetter(this, "log", () => {
let logger = LoginHelper.createLogger("LoginRelatedRealms");
return logger;
});
const EXPORTED_SYMBOLS = ["LoginRelatedRealmsParent"];
class LoginRelatedRealmsParent extends JSWindowActorParent {
/**
* @type RemoteSettingsClient
*
* @memberof LoginRelatedRealmsParent
*/
_sharedCredentialsClient = null;
/**
* @type string[][]
*
* @memberof LoginRelatedRealmsParent
*/
_relatedDomainsList = [[]];
/**
* Handles the Remote Settings sync event
*
* @param {Object} aEvent
* @param {Array} aEvent.current Records that are currently in the collection after the sync event
* @param {Array} aEvent.created Records that were created
* @param {Array} aEvent.updated Records that were updated
* @param {Array} aEvent.deleted Records that were deleted
* @memberof LoginRelatedRealmsParent
*/
onRemoteSettingsSync(aEvent) {
let {
data: { current },
} = aEvent;
this._relatedDomainsList = current;
}
async getSharedCredentialsCollection() {
if (!this._sharedCredentialsClient) {
this._sharedCredentialsClient = RemoteSettings(
LoginHelper.relatedRealmsCollection
);
this._sharedCredentialsClient.on("sync", event =>
this.onRemoteSettingsSync(event)
);
this._relatedDomainsList = await this._sharedCredentialsClient.get();
log.debug("Initialized related realms", this._relatedDomainsList);
}
log.debug("this._relatedDomainsList", this._relatedDomainsList);
return this._relatedDomainsList;
}
/**
* Determine if there are any related realms of this `formOrigin` using the related realms collection
* @param {string} formOrigin A form origin
* @return {string[]} filteredRealms An array of domains related to the `formOrigin`
* @async
* @memberof LoginRelatedRealmsParent
*/
async findRelatedRealms(formOrigin) {
try {
let formOriginURI = Services.io.newURI(formOrigin);
let originDomain = formOriginURI.host;
let [
{ relatedRealms } = {},
] = await this.getSharedCredentialsCollection();
if (!relatedRealms) {
return [];
}
let filterOriginIndex;
let shouldInclude = false;
let filteredRealms = relatedRealms.filter(_realms => {
for (let relatedOrigin of _realms) {
// We can't have an origin that matches multiple entries in our related realms collection
// so we exit the loop early
if (shouldInclude) {
return false;
}
if (Services.eTLD.hasRootDomain(originDomain, relatedOrigin)) {
shouldInclude = true;
break;
}
}
return shouldInclude;
});
// * Filtered realms is a nested array due to its structure in Remote Settings
filteredRealms = filteredRealms.flat();
filterOriginIndex = filteredRealms.indexOf(originDomain);
// Removing the current formOrigin match if it exists in the related realms
// so that we don't return duplicates when we search for logins
if (filterOriginIndex !== -1) {
filteredRealms.splice(filterOriginIndex, 1);
}
return filteredRealms;
} catch (e) {
log.error(e);
return [];
}
}
}

View File

@ -41,6 +41,7 @@ EXTRA_JS_MODULES += [
"LoginManagerParent.jsm",
"LoginManagerPrompter.jsm",
"LoginRecipes.jsm",
"LoginRelatedRealms.jsm",
"NewPasswordModel.jsm",
"OSCrypto.jsm",
"PasswordGenerator.jsm",

View File

@ -475,7 +475,9 @@ class LoginManagerStorage_json {
// Some property names aren't field names but are special options to
// affect the search.
case "acceptDifferentSubdomains":
case "schemeUpgrades": {
case "schemeUpgrades":
case "acceptRelatedRealms":
case "relatedRealms": {
options[prop.name] = prop.value;
break;
}
@ -508,6 +510,8 @@ class LoginManagerStorage_json {
aOptions = {
schemeUpgrades: false,
acceptDifferentSubdomains: false,
acceptRelatedRealms: false,
relatedRealms: [],
},
candidateLogins = this._store.data.logins
) {

View File

@ -9,6 +9,14 @@
const EXPORTED_SYMBOLS = ["LoginTestUtils"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
RemoteSettings: "resource://services-settings/remote-settings.js",
});
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
let { Assert: AssertCls } = ChromeUtils.import(
@ -616,3 +624,27 @@ LoginTestUtils.file = {
return tmpFile;
},
};
LoginTestUtils.remoteSettings = {
relatedRealmsCollection: "websites-with-shared-credential-backends",
async setupWebsitesWithSharedCredentials(
relatedRealms = [["other-example.com", "example.com", "example.co.uk"]]
) {
let db = await RemoteSettings(this.relatedRealmsCollection).db;
await db.clear();
await db.create({
id: "some-fake-ID-abc",
relatedRealms,
});
await db.importChanges({}, 1234567);
},
async cleanWebsitesWithSharedCredentials() {
let db = await RemoteSettings(this.relatedRealmsCollection).db;
await db.clear();
await db.importChanges({}, 1234);
},
async updateTimestamp() {
let db = await RemoteSettings(this.relatedRealmsCollection).db;
await db.importChanges({}, 12345678);
},
};

View File

@ -28,6 +28,12 @@ add_task(async function common_initialize() {
["toolkit.telemetry.ipcBatchTimeout", 0],
],
});
if (LoginHelper.relatedRealmsEnabled) {
LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
registerCleanupFunction(function() {
LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
});
}
});
registerCleanupFunction(

View File

@ -7,6 +7,8 @@ prefs =
signon.testOnlyUserHasInteractedWithDocument=true
security.insecure_field_warning.contextual.enabled=false
network.auth.non-web-content-triggered-resources-http-auth-allow=true
# signon.relatedRealms.enabled pref needed until Bug 1699698 lands
signon.relatedRealms.enabled=true
support-files =
../../../prompts/test/chromeScript.js
@ -38,6 +40,8 @@ skip-if = toolkit == 'android' && !is_fennec # Don't run on GeckoView
# Note: new tests should use scheme = https unless they have a specific reason not to
[test_autocomplete_autofill_related_realms_no_dupes.html]
scheme = https
[test_autocomplete_basic_form.html]
skip-if = toolkit == 'android' || debug && (os == 'linux' || os == 'win') || os == 'linux' && tsan # android:autocomplete. Bug 1541945, Bug 1590928
scheme = https
@ -46,6 +50,8 @@ skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux:
[test_autocomplete_basic_form_formActionOrigin.html]
skip-if = toolkit == 'android' # android:autocomplete.
scheme = https
[test_autocomplete_basic_form_related_realms.html]
scheme = https
[test_autocomplete_basic_form_subdomain.html]
skip-if = toolkit == 'android' # android:autocomplete.
scheme = https

View File

@ -667,6 +667,23 @@ function resetRecipes() {
});
}
function resetWebsitesWithSharedCredential() {
info("Resetting the 'websites-with-shared-credential-backend' collection");
return new Promise(resolve => {
PWMGR_COMMON_PARENT.addMessageListener(
"resetWebsitesWithSharedCredential",
function reset() {
PWMGR_COMMON_PARENT.removeMessageListener(
"resetWebsitesWithSharedCredential",
reset
);
resolve();
}
);
PWMGR_COMMON_PARENT.sendAsyncMessage("resetWebsitesWithSharedCredential");
});
}
function promiseStorageChanged(expectedChangeTypes) {
return new Promise((resolve, reject) => {
function onStorageChanged({ topic, data }) {

View File

@ -21,6 +21,12 @@ var { LoginManagerParent } = ChromeUtils.import(
const { LoginTestUtils } = ChromeUtils.import(
"resource://testing-common/LoginTestUtils.jsm"
);
if (LoginHelper.relatedRealmsEnabled) {
let rsPromise = LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
async () => {
await rsPromise;
};
}
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
/**
@ -127,6 +133,7 @@ addMessageListener("cleanup", () => {
Services.obs.removeObserver(onStorageChanged, "passwordmgr-storage-changed");
Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-change");
Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-save");
Services.logins.removeAllUserFacingLogins();
});
// Begin message listeners

View File

@ -25,7 +25,7 @@ let FILE_PATH = "/tests/toolkit/components/passwordmgr/test/mochitest/slow_image
runInParent(function removeAll() {
let {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
Services.logins.removeAllUserFacingLogins();
})
});
let readyPromise = registerRunTests();

View File

@ -0,0 +1,101 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test no duplicate logins using autofill/autocomplete with related realms</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
Login Manager test: no duplicate logins when using autofill and autocomplete with related realms
<script>
addLoginsInParent(
// Simple related domain relationship where example.com and other-example.com are in the related domains list
["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
// Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
// The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
// where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
// for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
// Ensures there are no duplicates for the exact domain that the user is on
["https://example.com", "https://example.com", null, "exactUser1", "exactPass1", "uname", "pword"],
["https://www.example.com", "https://www.example.com", null, "exactWWWUser1", "exactWWWPass1", "uname", "pword"],
);
</script>
<p id="display"></p>
<div id="content">
<form id="form1" action="https://www.example.com" onsubmit="return false;">
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Login Manager: related realms autofill/autocomplete. **/
let uname = $_(1, "uname");
let pword = $_(1, "pword");
function restoreForm() {
uname.value = "";
pword.value = "";
uname.focus();
}
function sendFakeAutocompleteEvent(element) {
var acEvent = document.createEvent("HTMLEvents");
acEvent.initEvent("DOMAutoComplete", true, false);
element.dispatchEvent(acEvent);
}
function spinEventLoop() {
return Promise.resolve();
}
async function promiseACPopupClosed() {
return SimpleTest.promiseWaitForCondition(async () => {
let popupState = await getPopupState();
return !popupState.open;
}, "Wait for AC popup to be closed");
}
add_task(async function setup() {
listenForUnexpectedPopupShown();
});
add_task(async function test_no_duplicates_autocomplete_autofill() {
await promiseFormsProcessedInSameProcess();
await SimpleTest.promiseFocus(window);
let shownPromise = promiseACShown();
checkLoginForm(uname, "exactUser1", pword, "exactPass1")
restoreForm();
let results = await shownPromise;
let popupState = await getPopupState();
is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
let expectedMenuItems = ["exactUser1", "exactWWWUser1", "relatedUser1", "subUser1"];
checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
is(acEvents.length, 1, "One autocomplete event");
checkACTelemetryEvent(acEvents[0], uname, {
"hadPrevious": "0",
"login": expectedMenuItems.length + "",
"loginsFooter": "1"
});
restoreForm();
synthesizeKey("KEY_Escape");
});
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,131 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test login autocomplete with related realms</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
Login Manager test: related realms autocomplete
<script>
addLoginsInParent(
// Simple related domain relationship where example.com and other-example.com are in the related domains list
["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
// Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
// The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
// where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
// for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
);
</script>
<p id="display"></p>
<div id="content">
<form id="form1" onsubmit="return false;">
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Login Manager: related realms autocomplete. **/
let uname = $_(1, "uname");
let pword = $_(1, "pword");
function restoreForm() {
uname.value = "";
pword.value = "";
uname.focus();
}
function sendFakeAutocompleteEvent(element) {
var acEvent = document.createEvent("HTMLEvents");
acEvent.initEvent("DOMAutoComplete", true, false);
element.dispatchEvent(acEvent);
}
function spinEventLoop() {
return Promise.resolve();
}
async function promiseACPopupClosed() {
return SimpleTest.promiseWaitForCondition(async () => {
let popupState = await getPopupState();
return !popupState.open;
}, "Wait for AC popup to be closed");
}
add_task(async function setup() {
listenForUnexpectedPopupShown();
});
add_task(async function test_form1_initial_empty() {
await SimpleTest.promiseFocus(window);
// Make sure initial form is empty.
checkLoginForm(uname, "", pword, "");
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is initially closed");
});
add_task(async function test_form_related_domain_menuitems() {
await SimpleTest.promiseFocus(window);
let shownPromise = promiseACShown();
// Trigger autocomplete popup
restoreForm();
let results = await shownPromise;
let popupState = await getPopupState();
is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
let expectedMenuItems = ["relatedUser1", "subUser1"];
checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
is(acEvents.length, 1, "One autocomplete event");
checkACTelemetryEvent(acEvents[0], uname, {
"hadPrevious": "0",
"login": expectedMenuItems.length + "",
"loginsFooter": "1"
});
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by opening
synthesizeKey("KEY_ArrowDown"); // first item
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting
synthesizeKey("KEY_Enter");
await promiseFormsProcessedInSameProcess();
is(pword.value, "relatedPass1", "password should match the login that was selected");
checkLoginForm(uname, "relatedUser1", pword, "relatedPass1");
restoreForm();
shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown"); // open
await shownPromise;
synthesizeKey("KEY_ArrowDown"); // first item
synthesizeKey("KEY_ArrowDown"); // second item
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting
synthesizeKey("KEY_Enter");
await promiseFormsProcessedInSameProcess();
is(pword.value, "subPass1", "password should match the login that was selected");
checkLoginForm(uname, "subUser1", pword, "subPass1");
restoreForm();
});
</script>
</pre>
</body>
</html>

View File

@ -240,10 +240,6 @@ add_task(async function test_multiple_prefilled_focused_dynamic() {
usernameField.focus();
await shownPromise;
});
add_task(async function cleanup() {
removeFocus();
});
</script>
</pre>
</body>

View File

@ -52,7 +52,7 @@ const newPropertyBag = LoginHelper.newPropertyBag;
const NEW_PASSWORD_HEURISTIC_ENABLED_PREF =
"signon.generation.confidenceThreshold";
const RELATED_REALMS_ENABLED_PREF = "signon.relatedRealms.enabled";
/**
* All the tests are implemented with add_task, this starts them automatically.
*/
@ -91,6 +91,12 @@ add_task(async function test_common_initialize() {
// Ensure that the service and the storage module are initialized.
await Services.logins.initializationPromise;
Services.prefs.setBoolPref(RELATED_REALMS_ENABLED_PREF, true);
if (LoginHelper.relatedRealmsEnabled) {
// Ensure that there is a mocked Remote Settings database for the
// "websites-with-shared-credential-backends" collection
await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
}
});
add_task(async function test_common_prefs() {

View File

@ -0,0 +1,157 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { LoginRelatedRealmsParent } = ChromeUtils.import(
"resource://gre/modules/LoginRelatedRealms.jsm"
);
const { RemoteSettings } = ChromeUtils.import(
"resource://services-settings/remote-settings.js",
{}
);
const REMOTE_SETTINGS_COLLECTION = "websites-with-shared-credential-backends";
add_task(async function test_related_domain_matching() {
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
const records = await client.get();
console.log(records);
// Assumes that the test collection is a 2D array with one subarray
let relatedRealms = records[0].relatedRealms;
relatedRealms = relatedRealms.flat();
ok(relatedRealms);
let LRR = new LoginRelatedRealmsParent();
// We should not return unrelated realms
let result = await LRR.findRelatedRealms("https://not-example.com");
equal(result.length, 0, "Check that there were no related realms found");
// We should not return unrelated realms given an unrelated subdomain
result = await LRR.findRelatedRealms("https://sub.not-example.com");
equal(result.length, 0, "Check that there were no related realms found");
// We should return the related realms collection
result = await LRR.findRelatedRealms("https://sub.example.com");
equal(
result.length,
relatedRealms.length,
"Ensure that three related realms were found"
);
// We should return the related realms collection minus the base domain that we searched with
result = await LRR.findRelatedRealms("https://example.co.uk");
equal(
result.length,
relatedRealms.length - 1,
"Ensure that two related realms were found"
);
});
add_task(async function test_newly_synced_collection() {
// Initialize LoginRelatedRealmsParent so the sync handler is enabled
let LRR = new LoginRelatedRealmsParent();
await LRR.getSharedCredentialsCollection();
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
let records = await client.get();
const record1 = {
id: records[0].id,
relatedRealms: records[0].relatedRealms,
};
// Assumes that the test collection is a 2D array with one subarray
let originalRelatedRealms = records[0].relatedRealms;
originalRelatedRealms = originalRelatedRealms.flat();
ok(originalRelatedRealms);
const updatedRelatedRealms = ["completely-different.com", "example.com"];
const record2 = {
id: "some-other-ID",
relatedRealms: [updatedRelatedRealms],
};
const payload = {
current: [record2],
created: [record2],
updated: [],
deleted: [record1],
};
await RemoteSettings(REMOTE_SETTINGS_COLLECTION).emit("sync", {
data: payload,
});
let [{ id, relatedRealms }] = await LRR.getSharedCredentialsCollection();
equal(id, record2.id, "internal collection ID should be updated");
equal(
relatedRealms,
record2.relatedRealms,
"internal collection related realms should be updated"
);
// We should return only one result, and that result should be example.com
// NOT other-example.com or example.co.uk
let result = await LRR.findRelatedRealms("https://completely-different.com");
equal(
result.length,
updatedRelatedRealms.length - 1,
"Check that there is only one related realm found"
);
equal(
result[0],
"example.com",
"Ensure that the updated collection should only match example.com"
);
});
add_task(async function test_no_related_domains() {
await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
let records = await client.get();
equal(records.length, 0, "Check that there are no related realms");
let LRR = new LoginRelatedRealmsParent();
ok(LRR.findRelatedRealms, "Ensure findRelatedRealms exists");
let result = await LRR.findRelatedRealms("https://example.com");
equal(result.length, 0, "Assert that there were no related realms found");
});
add_task(async function test_unrelated_subdomains() {
await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
let testCollection = [
["slpl.bibliocommons.com", "slpl.overdrive.com"],
["springfield.overdrive.com", "coolcat.org"],
];
await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(
testCollection
);
let LRR = new LoginRelatedRealmsParent();
let result = await LRR.findRelatedRealms("https://evil.overdrive.com");
equal(result.length, 0, "Assert that there were no related realms found");
result = await LRR.findRelatedRealms("https://abc.slpl.bibliocommons.com");
equal(result.length, 2, "Assert that two related realms were found");
equal(result[0], testCollection[0][0]);
equal(result[1], testCollection[0][1]);
result = await LRR.findRelatedRealms("https://slpl.overdrive.com");
console.log("what is result: " + result);
equal(result.length, 1, "Assert that one related realm was found");
for (let item of result) {
notEqual(
item,
"coolcat.org",
"coolcat.org is not related to slpl.overdrive.com"
);
notEqual(
item,
"springfield.overdrive.com",
"springfield.overdrive.com is not related to slpl.overdrive.com"
);
}
});

View File

@ -3,6 +3,7 @@
/**
* Tests retrieving remote LoginRecipes in the parent process.
* See https://firefox-source-docs.mozilla.org/services/settings/#unit-tests for explanation of db.importChanges({}, 42);
*/
"use strict";
@ -16,13 +17,14 @@ const REMOTE_SETTINGS_COLLECTION = "password-recipes";
add_task(async function test_init_remote_recipe() {
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
await db.clear();
const record1 = {
id: "some-fake-ID",
hosts: ["www.testDomain.com"],
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
await db.importChanges({}, 42, [record1], { clear: true });
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
@ -54,6 +56,7 @@ add_task(async function test_init_remote_recipe() {
"Initially 1 recipe based on our test record"
);
await db.clear();
await db.importChanges({}, 42);
});
add_task(async function test_add_recipe_sync() {
@ -64,7 +67,7 @@ add_task(async function test_add_recipe_sync() {
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
await db.importChanges({}, 42, [record1], { clear: true });
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
@ -89,6 +92,7 @@ add_task(async function test_add_recipe_sync() {
"New recipe from sync event added successfully"
);
await db.clear();
await db.importChanges({}, 42);
});
add_task(async function test_remove_recipe_sync() {
@ -99,7 +103,7 @@ add_task(async function test_remove_recipe_sync() {
description: "Some description here",
usernameSelector: "#username",
};
await db.create(record1);
await db.importChanges({}, 42, [record1], { clear: true });
let parent = new LoginRecipesParent({ defaults: true });
let recipesParent = await parent.initializationPromise;
@ -129,7 +133,7 @@ add_task(async function test_malformed_recipes_in_db() {
usernameSelector: "#username",
fieldThatDoesNotExist: "value",
};
await db.create(malformedRecord);
await db.importChanges({}, 42, [malformedRecord], { clear: true });
let parent = new LoginRecipesParent({ defaults: true });
try {
await parent.initializationPromise;
@ -146,7 +150,7 @@ add_task(async function test_malformed_recipes_in_db() {
description: "Some description here",
usernameSelector: "#username",
};
await db.create(missingHostsRecord);
await db.importChanges({}, 42, [missingHostsRecord], { clear: true });
parent = new LoginRecipesParent({ defaults: true });
try {
await parent.initializationPromise;

View File

@ -18,6 +18,7 @@ run-if = buildapp == "browser"
[test_disabled_hosts.js]
[test_displayOrigin.js]
[test_doLoginsMatch.js]
[test_findRelatedRealms.js]
[test_getFormFields.js]
[test_getPasswordFields.js]
[test_getPasswordOrigin.js]