Bug 1687852 Resolve how we handle duplicate logins when importing from CSV file r=sfoster,tgiles

Differential Revision: https://phabricator.services.mozilla.com/D102639
This commit is contained in:
Andrei Cristian Petcu 2021-02-06 01:53:56 +00:00
Родитель 5ac1094bb5
Коммит cc083bcdcf
3 изменённых файлов: 499 добавлений и 255 удалений

Просмотреть файл

@ -162,7 +162,17 @@ class LoginCSVImport {
columnName.toLocaleLowerCase()
);
if (fieldName) {
fieldsInFile.add(fieldName);
if (!fieldsInFile.has(fieldName)) {
fieldsInFile.add(fieldName);
} else {
TelemetryStopwatch.cancelKeyed(
"FX_MIGRATION_LOGINS_IMPORT_MS",
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
);
throw new ImportFailedException(
ImportFailedErrorType.CONFLICTING_VALUES_ERROR
);
}
}
}
}
@ -185,22 +195,6 @@ class LoginCSVImport {
throw new ImportFailedException(ImportFailedErrorType.FILE_FORMAT_ERROR);
}
const uniqueLoginIdentifiers = new Set();
for (const csvObject of parsedLines) {
// TODO: handle duplicates without guid column. Bug 1687852
if (csvObject.guid) {
if (uniqueLoginIdentifiers.has(csvObject.guid)) {
throw new ImportFailedException(
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
csvObject.guid
);
} else {
uniqueLoginIdentifiers.add(csvObject.guid);
}
}
}
let loginsToImport = parsedLines.map(csvObject => {
return LoginCSVImport._getVanillaLoginFromCSVObject(
csvObject,

Просмотреть файл

@ -24,6 +24,314 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/OSKeyStore.jsm"
);
/**
* A helper class to deal with CSV import rows.
*/
class ImportRowProcessor {
uniqueLoginIdentifiers = new Set();
originToRows = new Map();
summary = [];
/**
* Validates if the login data contains a GUID that was already found in a previous row in the current import.
* If this is the case, the summary will be updated with an error.
* @param {object} loginData
* An vanilla object for the login without any methods.
* @returns {boolean} True if there is an error, false otherwise.
*/
checkNonUniqueGuidError(loginData) {
if (loginData.guid) {
if (this.uniqueLoginIdentifiers.has(loginData.guid)) {
this.addLoginToSummary({ ...loginData }, "error");
return true;
}
this.uniqueLoginIdentifiers.add(loginData.guid);
}
return false;
}
/**
* Validates if the login data contains invalid fields that are mandatory like origin and password.
* If this is the case, the summary will be updated with an error.
* @param {object} loginData
* An vanilla object for the login without any methods.
* @returns {boolean} True if there is an error, false otherwise.
*/
checkMissingMandatoryFieldsError(loginData) {
loginData.origin = LoginHelper.getLoginOrigin(loginData.origin);
if (!loginData.origin) {
this.addLoginToSummary({ ...loginData }, "error_invalid_origin");
return true;
}
if (!loginData.password) {
this.addLoginToSummary({ ...loginData }, "error_invalid_password");
return true;
}
return false;
}
/**
* Validates if there is already an existing entry with similar values.
* If there are similar values but not identical, a new "modified" entry will be added to the summary.
* If there are identical values, a new "no_change" entry will be added to the summary
* If either of these is the case, it will return true.
* @param {object} loginData
* An vanilla object for the login without any methods.
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
*/
async checkExistingEntry(loginData) {
if (loginData.guid) {
// First check for `guid` matches if it's set.
// `guid` matches will allow every kind of update, including reverting
// to older passwords which can be useful if the user wants to recover
// an old password.
let existingLogins = await Services.logins.searchLoginsAsync({
guid: loginData.guid,
origin: loginData.origin, // Ignored outside of GV.
});
if (existingLogins.length) {
log.debug("maybeImportLogins: Found existing login with GUID");
// There should only be one `guid` match.
let existingLogin = existingLogins[0].QueryInterface(
Ci.nsILoginMetaInfo
);
if (
loginData.username !== existingLogin.username ||
loginData.password !== existingLogin.password ||
loginData.httpRealm !== existingLogin.httpRealm ||
loginData.formActionOrigin !== existingLogin.formActionOrigin ||
`${loginData.timeCreated}` !== `${existingLogin.timeCreated}` ||
`${loginData.timePasswordChanged}` !==
`${existingLogin.timePasswordChanged}`
) {
// Use a property bag rather than an nsILoginInfo so we don't clobber
// properties that the import source doesn't provide.
let propBag = LoginHelper.newPropertyBag(loginData);
this.addLoginToSummary({ ...existingLogin }, "modified", propBag);
return true;
}
this.addLoginToSummary({ ...existingLogin }, "no_change");
return true;
}
}
return false;
}
/**
* Validates if there is a conflict with previous rows based on the origin.
* We need to check the logins that we've already decided to add, to see if this is a duplicate.
* If this is the case, we mark this one as "no_change" in the summary and return true.
* @param {object} login
* A login object.
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
*/
checkConflictingOriginWithPreviousRows(login) {
let rowsPerOrigin = this.originToRows.get(login.origin) || [];
if (!rowsPerOrigin.length) {
this.originToRows.set(login.origin, rowsPerOrigin);
} else if (
rowsPerOrigin.some(r =>
login.matches(r.login, false /* ignorePassword */)
)
) {
this.addLoginToSummary(login, "no_change");
return true;
} else {
for (let row of rowsPerOrigin) {
let newLogin = row.login;
if (login.username == newLogin.username) {
this.addLoginToSummary(login, "no_change");
return true;
}
}
}
return false;
}
/**
* Validates if there is a conflict with existing logins based on the origin.
* If this is the case and there are some changes, we mark it as "modified" in the summary.
* If it matches an existing login without any extra modifications, we mark it as "no_change".
* For both cases we return true.
* @param {object} login
* A login object.
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
*/
checkConflictingWithExistingLogins(login) {
// While here we're passing formActionOrigin and httpRealm, they could be empty/null and get
// ignored in that case, leading to multiple logins for the same username.
let existingLogins = Services.logins.findLogins(
login.origin,
login.formActionOrigin,
login.httpRealm
);
// Check for an existing login that matches *including* the password.
// If such a login exists, we do not need to add a new login.
if (
existingLogins.some(l => login.matches(l, false /* ignorePassword */))
) {
this.addLoginToSummary(login, "no_change");
return true;
}
// Now check for a login with the same username, where it may be that we have an
// updated password.
let foundMatchingLogin = false;
for (let existingLogin of existingLogins) {
if (login.username == existingLogin.username) {
foundMatchingLogin = true;
existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
if (
(login.password != existingLogin.password) &
(login.timePasswordChanged > existingLogin.timePasswordChanged)
) {
// if a login with the same username and different password already exists and it's older
// than the current one, update its password and timestamp.
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
propBag.setProperty("password", login.password);
propBag.setProperty("timePasswordChanged", login.timePasswordChanged);
this.addLoginToSummary({ ...existingLogin }, "modified", propBag);
return true;
}
}
}
// if the new login is an update or is older than an exiting login, don't add it.
if (foundMatchingLogin) {
this.addLoginToSummary(login, "no_change");
return true;
}
return false;
}
/**
* Validates if there are any invalid values using LoginHelper.checkLoginValues.
* If this is the case we mark it as "error" and return true.
* @param {object} login
* A login object.
* @param {object} loginData
* An vanilla object for the login without any methods.
* @returns {boolean} True if there is a validation error we return true, false otherwise.
*/
checkLoginValuesError(login, loginData) {
try {
// Ensure we only send checked logins through, since the validation is optimized
// out from the bulk APIs below us.
LoginHelper.checkLoginValues(login);
} catch (e) {
this.addLoginToSummary({ ...loginData }, "error");
Cu.reportError(e);
return true;
}
return false;
}
/**
* Creates a new login from loginData.
* @param {object} loginData
* An vanilla object for the login without any methods.
* @returns {object} A login object.
*/
createNewLogin(loginData) {
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
Ci.nsILoginInfo
);
login.init(
loginData.origin,
loginData.formActionOrigin,
loginData.httpRealm,
loginData.username,
loginData.password,
loginData.usernameElement || "",
loginData.passwordElement || ""
);
login.QueryInterface(Ci.nsILoginMetaInfo);
login.timeCreated = loginData.timeCreated;
login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
login.timePasswordChanged =
loginData.timePasswordChanged || loginData.timeCreated;
login.timesUsed = loginData.timesUsed || 1;
login.guid = loginData.guid || null;
return login;
}
/**
* Cleans the action and realm field of the loginData.
* @param {object} loginData
* An vanilla object for the login without any methods.
*/
cleanupActionAndRealmFields(loginData) {
loginData.formActionOrigin =
LoginHelper.getLoginOrigin(loginData.formActionOrigin, true) ||
(typeof loginData.httpRealm == "string" ? null : "");
loginData.httpRealm =
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null;
}
/**
* Adds a login to the summary.
* @param {object} login
* A login object.
* @param {string} result
* The result type. One of "added", "modified", "error", "error_invalid_origin", "error_invalid_password" or "no_change".
* @param {object} propBag
* An optional parameter with the properties bag.
*/
addLoginToSummary(login, result, propBag) {
let rows = this.originToRows.get(login.origin) || [];
const newSummaryRow = { result, login, propBag };
rows.push(newSummaryRow);
this.summary.push(newSummaryRow);
}
/**
* Iterates over all then rows where more than two match the same origin. It mutates the internal state of the processor.
* It makes sure that if the `timePasswordChanged` field is present it will be used to decide if it's a "no_change" or "added".
* The entry with the oldest `timePasswordChanged` will be "added", the rest will be "no_change".
*/
markLastTimePasswordChangedAsModified() {
for (let rowsGroupedByOrigin of this.originToRows.values()) {
if (rowsGroupedByOrigin.length > 1) {
let lastTimePasswordChanged = rowsGroupedByOrigin[0];
for (let i = 1; i < rowsGroupedByOrigin.length; i++) {
const row = rowsGroupedByOrigin[i];
if (
(row.login.password != lastTimePasswordChanged.login.password) &
(row.login.timePasswordChanged >
lastTimePasswordChanged.login.timePasswordChanged)
) {
lastTimePasswordChanged.result = "no_change";
lastTimePasswordChanged = row;
}
}
lastTimePasswordChanged.result = "added";
}
}
}
/**
* Iterates over all then rows where more than two match the same origin. It mutates the internal state of the processor.
* It makes sure that if the `timePasswordChanged` field is present it will be used to decide if it's a "no_change" or "added".
* The entry with the oldest `timePasswordChanged` will be "added", the rest will be "no_change".
* @returns {Object[]} An entry for each processed row containing how the row was processed and the login data.
*/
async processLoginsAndBuildSummary() {
this.markLastTimePasswordChangedAsModified();
for (let summaryRow of this.summary) {
if (summaryRow.result === "added") {
summaryRow.login = await Services.logins.addLogin(summaryRow.login);
} else if (summaryRow.result === "modified") {
Services.logins.modifyLogin(summaryRow.login, summaryRow.propBag);
}
}
return this.summary;
}
}
/**
* Contains functions shared by different Login Manager components.
*/
@ -969,203 +1277,36 @@ this.LoginHelper = {
* doesn't already exist. Merge it otherwise with the similar existing ones.
*
* @param {Object[]} loginDatas - For each login, the data that needs to be added.
* @returns {nsILoginInfo[]} the newly added logins, filtered if no login was added.
* @returns {Object[]} An entry for each processed row containing how the row was processed and the login data.
*/
async maybeImportLogins(loginDatas) {
let summary = [];
let loginsToAdd = [];
let loginMap = new Map();
const processor = new ImportRowProcessor();
for (let rawLoginData of loginDatas) {
// Do some sanitization on a clone of the loginData.
let loginData = ChromeUtils.shallowClone(rawLoginData);
loginData.origin = this.getLoginOrigin(loginData.origin);
if (!loginData.origin) {
summary.push({
result: "error_invalid_origin",
login: { ...loginData },
});
if (processor.checkNonUniqueGuidError(loginData)) {
continue;
}
if (!loginData.password) {
summary.push({
result: "error_invalid_password",
login: { ...loginData },
});
if (processor.checkMissingMandatoryFieldsError(loginData)) {
continue;
}
loginData.formActionOrigin =
this.getLoginOrigin(loginData.formActionOrigin, true) ||
(typeof loginData.httpRealm == "string" ? null : "");
loginData.httpRealm =
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null;
if (loginData.guid) {
// First check for `guid` matches if it's set.
// `guid` matches will allow every kind of update, including reverting
// to older passwords which can be useful if the user wants to recover
// an old password.
let existingLogins = await Services.logins.searchLoginsAsync({
guid: loginData.guid,
origin: loginData.origin, // Ignored outside of GV.
});
if (existingLogins.length) {
log.debug("maybeImportLogins: Found existing login with GUID");
// There should only be one `guid` match.
let existingLogin = existingLogins[0].QueryInterface(
Ci.nsILoginMetaInfo
);
// Use a property bag rather than an nsILoginInfo so we don't clobber
// properties that the import source doesn't provide.
let propBag = this.newPropertyBag(loginData);
if (
loginData.username !== existingLogin.username ||
loginData.password !== existingLogin.password ||
loginData.httpRealm !== existingLogin.httpRealm ||
loginData.formActionOrigin !== existingLogin.formActionOrigin ||
`${loginData.timeCreated}` !== `${existingLogin.timeCreated}` ||
`${loginData.timePasswordChanged}` !==
`${existingLogin.timePasswordChanged}`
) {
summary.push({ result: "modified", login: { ...existingLogin } });
Services.logins.modifyLogin(existingLogin, propBag);
// Updated a login so we're done.
} else {
summary.push({ result: "no_change", login: { ...existingLogin } });
}
continue;
}
}
// create a new login
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
Ci.nsILoginInfo
);
login.init(
loginData.origin,
loginData.formActionOrigin,
loginData.httpRealm,
loginData.username,
loginData.password,
loginData.usernameElement || "",
loginData.passwordElement || ""
);
login.QueryInterface(Ci.nsILoginMetaInfo);
login.timeCreated = loginData.timeCreated;
login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
login.timePasswordChanged =
loginData.timePasswordChanged || loginData.timeCreated;
login.timesUsed = loginData.timesUsed || 1;
login.guid = loginData.guid || null;
try {
// Ensure we only send checked logins through, since the validation is optimized
// out from the bulk APIs below us.
this.checkLoginValues(login);
} catch (e) {
summary.push({
result: "error",
login: { ...loginData },
});
Cu.reportError(e);
processor.cleanupActionAndRealmFields(loginData);
if (await processor.checkExistingEntry(loginData)) {
continue;
}
// First, we need to check the logins that we've already decided to add, to
// see if this is a duplicate. This should mirror the logic below for
// existingLogins, but only for the array of logins we're adding.
let newLogins = loginMap.get(login.origin) || [];
if (!newLogins) {
loginMap.set(login.origin, newLogins);
} else {
if (newLogins.some(l => login.matches(l, false /* ignorePassword */))) {
summary.push({ result: "no_change", login });
continue;
}
let foundMatchingNewLogin = false;
for (let newLogin of newLogins) {
if (login.username == newLogin.username) {
foundMatchingNewLogin = true;
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
if (
(login.password != newLogin.password) &
(login.timePasswordChanged > newLogin.timePasswordChanged)
) {
// if a login with the same username and different password already exists and it's older
// than the current one, update its password and timestamp.
newLogin.password = login.password;
newLogin.timePasswordChanged = login.timePasswordChanged;
}
}
}
if (foundMatchingNewLogin) {
summary.push({ result: "no_change", login });
continue;
}
}
// While here we're passing formActionOrigin and httpRealm, they could be empty/null and get
// ignored in that case, leading to multiple logins for the same username.
let existingLogins = Services.logins.findLogins(
login.origin,
login.formActionOrigin,
login.httpRealm
);
// Check for an existing login that matches *including* the password.
// If such a login exists, we do not need to add a new login.
if (
existingLogins.some(l => login.matches(l, false /* ignorePassword */))
) {
summary.push({ result: "no_change", login });
let login = processor.createNewLogin(loginData);
if (processor.checkLoginValuesError(login, loginData)) {
continue;
}
// Now check for a login with the same username, where it may be that we have an
// updated password.
let foundMatchingLogin = false;
for (let existingLogin of existingLogins) {
if (login.username == existingLogin.username) {
foundMatchingLogin = true;
existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
if (
(login.password != existingLogin.password) &
(login.timePasswordChanged > existingLogin.timePasswordChanged)
) {
// if a login with the same username and different password already exists and it's older
// than the current one, update its password and timestamp.
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
propBag.setProperty("password", login.password);
propBag.setProperty(
"timePasswordChanged",
login.timePasswordChanged
);
summary.push({ result: "modified", login: { ...existingLogin } });
Services.logins.modifyLogin(existingLogin, propBag);
}
}
}
// if the new login is an update or is older than an exiting login, don't add it.
if (foundMatchingLogin) {
summary.push({ result: "no_change", login: { login } });
if (processor.checkConflictingOriginWithPreviousRows(login)) {
continue;
}
newLogins.push(login);
loginsToAdd.push(login);
if (processor.checkConflictingWithExistingLogins(login)) {
continue;
}
processor.addLoginToSummary(login, "added");
}
if (loginsToAdd.length) {
let addedLogins = await Services.logins.addLogins(loginsToAdd);
for (let addedLogin of addedLogins) {
summary.push({ result: "added", login: { ...addedLogin } });
}
}
return summary;
return processor.processLoginsAndBuildSummary();
},
/**
@ -1188,7 +1329,6 @@ this.LoginHelper = {
obj[i] = login[i];
}
}
return obj;
},
@ -1225,8 +1365,12 @@ this.LoginHelper = {
/**
* As above, but for an array of objects.
*/
vanillaObjectsToLogins(logins) {
return logins.map(this.vanillaObjectToLogin);
vanillaObjectsToLogins(vanillaObjects) {
const logins = [];
for (const vanillaObject of vanillaObjects) {
logins.push(this.vanillaObjectToLogin(vanillaObject));
}
return logins;
},
/**

Просмотреть файл

@ -164,22 +164,15 @@ add_task(async function test_import_with_duplicate_columns() {
"https://mozilla.org,https://mozilla.org,jdoe@example.com,qwerty",
]);
await LoginCSVImport.importFromCSV(csvFilePath);
await Assert.rejects(
LoginCSVImport.importFromCSV(csvFilePath),
/CONFLICTING_VALUES_ERROR/,
"Check that the errorType is file format error"
);
LoginTestUtils.checkLogins(
[
TestData.formLogin({
formActionOrigin: "",
httpRealm: null,
origin: "https://mozilla.org",
password: "qwerty",
passwordField: "",
timesUsed: 1,
username: "jdoe@example.com",
usernameField: "",
}),
],
"Check that no login was added with duplicate columns of differing values"
[],
"Check that no login was added from a file with duplicated columns"
);
});
@ -515,36 +508,90 @@ add_task(async function test_import_summary_contains_added_login() {
});
/**
* Imports login data summary contains modified logins.
* Imports login data summary contains modified logins without guid.
*/
add_task(async function test_import_summary_contains_modified_login() {
add_task(async function test_import_summary_modified_login_without_guid() {
let initialDataFile = await setupCsv([
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
"https://modifiedwithoutguid.example.com,gini@example.com,initial_password,My realm,,,1589617814635,1589710449871,1589617846802",
]);
await LoginCSVImport.importFromCSV(initialDataFile);
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
"https://modifiedwithoutguid.example.com,gini@example.com,modified_password,My realm,,,1589617814635,1589710449871,1589617846999",
]);
let [modifiedWithoutGuid] = await LoginCSVImport.importFromCSV(csvFile.path);
equal(
modifiedWithoutGuid.result,
"modified",
`Check that the login was modified when there was no guid data`
);
LoginTestUtils.checkLogins(
[
TestData.authLogin({
formActionOrigin: null,
guid: null,
httpRealm: "My realm",
origin: "https://modifiedwithoutguid.example.com",
password: "modified_password",
passwordField: "",
timeCreated: 1589617814635,
timeLastUsed: 1589710449871,
timePasswordChanged: 1589617846999,
timesUsed: 1,
username: "gini@example.com",
usernameField: "",
}),
],
"Check that logins were updated with the correct fields",
(a, e) => a.equals(e) && checkMetaInfo(a, e)
);
});
/**
* Imports login data summary contains modified logins with guid.
*/
add_task(async function test_import_summary_modified_login_with_guid() {
let initialDataFile = await setupCsv([
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
"https://modifiedwithguid.example.com,jane@example.com,initial_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0001},1589617814635,1589710449871,1589617846802",
"https://modifiedwithoutguid.example.com,jane@example.com,initial_password,My realm,,,1589617814635,1589710449871,1589617846802",
]);
await LoginCSVImport.importFromCSV(initialDataFile);
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
"https://modified.example.com,jane@example.com,modified_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0001},1589617814635,1589710449871,1589617846999",
"https://modifiedwithoutguid.example.com,jane@example.com,modified_password,My realm,,,1589617814635,1589710449871,1589617846999",
]);
let [
modifiedWithGuid,
modifiedWithoutGuid,
] = await LoginCSVImport.importFromCSV(csvFile.path);
let [modifiedWithGuid] = await LoginCSVImport.importFromCSV(csvFile.path);
equal(
modifiedWithGuid.result,
"modified",
`Check that the login was modified when it had the same guid`
);
equal(
modifiedWithoutGuid.result,
"modified",
`Check that the login was modified when there was no guid data`
LoginTestUtils.checkLogins(
[
TestData.authLogin({
formActionOrigin: null,
guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb0001}",
httpRealm: "My realm",
origin: "https://modified.example.com",
password: "modified_password",
passwordField: "",
timeCreated: 1589617814635,
timeLastUsed: 1589710449871,
timePasswordChanged: 1589617846999,
timesUsed: 1,
username: "jane@example.com",
usernameField: "",
}),
],
"Check that logins were updated with the correct fields",
(a, e) => a.equals(e) && checkMetaInfo(a, e)
);
});
@ -632,7 +679,7 @@ add_task(async function test_import_summary_with_non_csv_file() {
});
/**
* Imports login with wrong file type will have correct errorType.
* Imports login multiple url and user will import the first and skip the second.
*/
add_task(async function test_import_summary_with_url_user_multiple_values() {
let csvFilePath = await setupCsv([
@ -641,44 +688,103 @@ add_task(async function test_import_summary_with_url_user_multiple_values() {
"https://example.com,jane@example.com,password2,My realm",
]);
let errorType;
try {
await LoginCSVImport.importFromCSV(csvFilePath);
} catch (e) {
if (e instanceof ImportFailedException) {
errorType = e.errorType;
}
}
let initialLoginCount = Services.logins.getAllLogins().length;
let results = await LoginCSVImport.importFromCSV(csvFilePath);
let afterImportLoginCount = Services.logins.getAllLogins().length;
equal(results.length, 2, `Check that we got a result for each imported row`);
equal(results[0].result, "added", `Check that the first login was added`);
equal(
errorType,
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
`Check that the errorType is file format error in case of duplicate entries`
results[1].result,
"no_change",
`Check that the second login was skipped`
);
}).skip(); // TODO: Bug 1687852, resolve duplicates when importing
equal(initialLoginCount, 0, `Check that initially we had no logins`);
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
});
/**
* Imports login with wrong file type will have correct errorType.
* Imports login with duplicated guid values throws error.
*/
add_task(async function test_import_summary_with_multiple_guid_values() {
add_task(async function test_import_summary_with_duplicated_guid_values() {
let csvFilePath = await setupCsv([
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
"https://example1.com,jane1@example.com,password1,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
"https://example2.com,jane2@example.com,password2,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
]);
let initialLoginCount = Services.logins.getAllLogins().length;
let errorType;
try {
await LoginCSVImport.importFromCSV(csvFilePath);
} catch (e) {
if (e instanceof ImportFailedException) {
errorType = e.errorType;
}
}
let results = await LoginCSVImport.importFromCSV(csvFilePath);
let afterImportLoginCount = Services.logins.getAllLogins().length;
equal(
errorType,
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
`Check that the errorType is file format error in case of duplicate entries`
);
equal(results.length, 2, `Check that we got a result for each imported row`);
equal(results[0].result, "added", `Check that the first login was added`);
equal(results[1].result, "error", `Check that the second login was an error`);
equal(initialLoginCount, 0, `Check that initially we had no logins`);
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
});
/**
* Imports login with different passwords will pick up the newest one and ignore the oldest one.
*/
add_task(async function test_import_summary_with_different_time_changed() {
let csvFilePath = await setupCsv([
"url,username,password,timeCreated,timeLastUsed,timePasswordChanged",
"https://example.com,eve@example.com,old password,1589617814635,1589710449800,1589617846800",
"https://example.com,eve@example.com,new password,1589617814635,1589710449801,1589617846801",
]);
let initialLoginCount = Services.logins.getAllLogins().length;
let results = await LoginCSVImport.importFromCSV(csvFilePath);
let afterImportLoginCount = Services.logins.getAllLogins().length;
equal(results.length, 2, `Check that we got a result for each imported row`);
equal(
results[0].result,
"no_change",
`Check that the oldest password is skipped`
);
equal(
results[1].login.password,
"new password",
`Check that the newest password is imported`
);
equal(
results[1].result,
"added",
`Check that the newest password result is correct`
);
equal(initialLoginCount, 0, `Check that initially we had no logins`);
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
});
/**
* Imports duplicate logins as one without an error.
*/
add_task(async function test_import_duplicate_logins_as_one() {
let csvFilePath = await setupCsv([
"name,url,username,password",
"somesite,https://example.com/,user@example.com,asdasd123123",
"somesite,https://example.com/,user@example.com,asdasd123123",
]);
let initialLoginCount = Services.logins.getAllLogins().length;
let results = await LoginCSVImport.importFromCSV(csvFilePath);
let afterImportLoginCount = Services.logins.getAllLogins().length;
equal(results.length, 2, `Check that we got a result for each imported row`);
equal(
results[0].result,
"added",
`Check that the first login login was added`
);
equal(
results[1].result,
"no_change",
`Check that the second login was not changed`
);
equal(initialLoginCount, 0, `Check that initially we had no logins`);
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
});