summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js')
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js870
1 files changed, 870 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js
new file mode 100644
index 0000000000..12e040eb60
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginCSVImport.js
@@ -0,0 +1,870 @@
+/* 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/. */
+
+/**
+ * Tests the LoginCSVImport module.
+ */
+
+"use strict";
+
+const { LoginCSVImport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginCSVImport.sys.mjs"
+);
+const { LoginExport } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginExport.sys.mjs"
+);
+const { TelemetryTestUtils: TTU } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+const CATEGORICAL_HISTOGRAM = "PWMGR_IMPORT_LOGINS_FROM_FILE_CATEGORICAL";
+const IMPORT_TIMER_HISTOGRAM = "PWMGR_IMPORT_LOGINS_FROM_FILE_MS";
+const IMPORT_JANK_HISTOGRAM = "PWMGR_IMPORT_LOGINS_FROM_FILE_JANK_MS";
+/**
+ * Given an array of strings it creates a temporary CSV file that has them as content.
+ *
+ * @param {string[]} csvLines
+ * The lines that make up the CSV file.
+ * @param {string} extension
+ * Optional parameter. Either 'csv' or 'tsv'. Default is 'csv'.
+ * @returns {string} The path to the CSV file that was created.
+ */
+async function setupCsv(csvLines, extension) {
+ // Cleanup state.
+ TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+ TTU.getAndClearHistogram(IMPORT_TIMER_HISTOGRAM);
+ TTU.getAndClearHistogram(IMPORT_JANK_HISTOGRAM);
+ Services.logins.removeAllUserFacingLogins();
+ let tmpFile = await LoginTestUtils.file.setupCsvFileWithLines(
+ csvLines,
+ extension
+ );
+ return tmpFile.path;
+}
+
+function checkMetaInfo(
+ actual,
+ expected,
+ props = ["timesUsed", "timeCreated", "timePasswordChanged", "timeLastUsed"]
+) {
+ for (let prop of props) {
+ // This will throw if not equal.
+ equal(actual[prop], expected[prop], `Check ${prop}`);
+ }
+ return true;
+}
+
+function checkLoginNewlyCreated(login) {
+ // These will throw if not equal.
+ LoginTestUtils.assertTimeIsAboutNow(login.timeCreated);
+ LoginTestUtils.assertTimeIsAboutNow(login.timePasswordChanged);
+ LoginTestUtils.assertTimeIsAboutNow(login.timeLastUsed);
+ return true;
+}
+
+/**
+ * Asserts histogram telemetry for the categories of logins
+ *
+ * @param {Object} histogram Histogram object returned from `TelemetryTestUtils.getAndClearHistogram()`
+ * @param {Number} index Index representing one of the following values in order: ["added", "modified", "error", "no_change"]. See `toolkit/components/telemetry/Histogram.json` for more information
+ * @param {Number} expected The expected number of entries in the histogram at the passed index
+ */
+function assertHistogramTelemetry(histogram, index, expected) {
+ TTU.assertHistogram(histogram, index, expected);
+}
+
+/**
+ * Ensure that an import works with TSV.
+ */
+add_task(async function test_import_tsv() {
+ let histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+ let tsvFilePath = await setupCsv(
+ [
+ "url\tusername\tpassword\thttpRealm\tformActionOrigin\tguid\ttimeCreated\ttimeLastUsed\ttimePasswordChanged",
+ `https://example.com:8080\tjoe@example.com\tqwerty\tMy realm\t""\t{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}\t1589617814635\t1589710449871\t1589617846802`,
+ ],
+ "tsv"
+ );
+
+ await LoginCSVImport.importFromCSV(tsvFilePath);
+ assertHistogramTelemetry(histogram, 0, 1);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.authLogin({
+ formActionOrigin: null,
+ guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}",
+ httpRealm: "My realm",
+ origin: "https://example.com:8080",
+ password: "qwerty",
+ passwordField: "",
+ timeCreated: 1589617814635,
+ timeLastUsed: 1589710449871,
+ timePasswordChanged: 1589617846802,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Ensure that an import fails if there is no username column in a TSV file.
+ */
+add_task(async function test_import_tsv_with_missing_columns() {
+ let csvFilePath = await setupCsv(
+ [
+ "url\tusernameTypo\tpassword\thttpRealm\tformActionOrigin\tguid\ttimeCreated\ttimeLastUsed\ttimePasswordChanged",
+ "https://example.com\tkramer@example.com\tqwerty\tMy realm\t\t{5ec0d12f-e194-4279-ae1b-d7d281bb46f7}\t1589617814635\t1589710449871\t1589617846802",
+ ],
+ "tsv"
+ );
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /FILE_FORMAT_ERROR/,
+ "Ensure missing username throws"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added without finding columns"
+ );
+});
+
+/**
+ * Ensure that an import fails if there is no username column. We don't want
+ * to accidentally import duplicates due to a name mismatch for the username column.
+ */
+add_task(async function test_import_lacking_username_column() {
+ let csvFilePath = await setupCsv([
+ "url,usernameTypo,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example.com,joe@example.com,qwerty,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802`,
+ ]);
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /FILE_FORMAT_ERROR/,
+ "Ensure missing username throws"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added without finding a username column"
+ );
+});
+
+/**
+ * Ensure that an import fails if there are two columns that map to one login field.
+ */
+add_task(async function test_import_with_duplicate_fields() {
+ // Two origin columns (url & login_uri).
+ // One row has different values and the other has the same.
+ let csvFilePath = await setupCsv([
+ "url,login_uri,username,login_password",
+ "https://example.com/path,https://example.com,john@example.com,azerty",
+ "https://mozilla.org,https://mozilla.org,jdoe@example.com,qwerty",
+ ]);
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /CONFLICTING_VALUES_ERROR/,
+ "Check that the errorType is file format error"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added from a file with duplicated columns"
+ );
+});
+
+/**
+ * Ensure that an import fails if there are two identical columns.
+ */
+add_task(async function test_import_with_duplicate_columns() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,password",
+ "https://example.com/path,john@example.com,azerty,12345",
+ ]);
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /CONFLICTING_VALUES_ERROR/,
+ "Check that the errorType is file format error"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added from a file with duplicated columns"
+ );
+});
+
+/**
+ * Ensure that import is allowed with only origin, username, password and that
+ * one can mix and match column naming between conventions from different
+ * password managers (so that we better support new/unknown password managers).
+ */
+add_task(async function test_import_minimal_with_mixed_naming() {
+ let csvFilePath = await setupCsv([
+ "url,username,login_password",
+ "ftp://example.com,john@example.com,azerty",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "ftp://example.com",
+ password: "azerty",
+ passwordField: "",
+ timesUsed: 1,
+ username: "john@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) =>
+ a.equals(e) &&
+ checkMetaInfo(a, e, ["timesUsed"]) &&
+ checkLoginNewlyCreated(a)
+ );
+});
+
+/**
+ * Imports login data from the latest Firefox CSV file for various logins from
+ * LoginTestUtils.testData.loginList().
+ */
+add_task(async function test_import_from_firefox_various_latest() {
+ await setupCsv([]);
+ info("Populate the login list for export");
+ let logins = LoginTestUtils.testData.loginList();
+ await Services.logins.addLogins(logins);
+
+ let tmpFilePath = FileTestUtils.getTempFile("logins.csv").path;
+ await LoginExport.exportAsCSV(tmpFilePath);
+
+ await LoginCSVImport.importFromCSV(tmpFilePath);
+
+ LoginTestUtils.checkLogins(
+ logins,
+ "Check that all of LoginTestUtils.testData.loginList can be re-imported"
+ );
+});
+
+/**
+ * Imports login data from a Firefox CSV file without quotes.
+ */
+add_task(async function test_import_from_firefox_auth() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example.com:8080,joe@example.com,qwerty,My realm,"",{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802`,
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.authLogin({
+ formActionOrigin: null,
+ guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}",
+ httpRealm: "My realm",
+ origin: "https://example.com:8080",
+ password: "qwerty",
+ passwordField: "",
+ timeCreated: 1589617814635,
+ timeLastUsed: 1589710449871,
+ timePasswordChanged: 1589617846802,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Imports login data from a Firefox CSV file with quotes.
+ */
+add_task(async function test_import_from_firefox_auth_with_quotes() {
+ let csvFilePath = await setupCsv([
+ '"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"',
+ '"https://example.com","joe@example.com","qwerty2","My realm",,"{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}","1589617814635","1589710449871","1589617846802"',
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.authLogin({
+ formActionOrigin: null,
+ httpRealm: "My realm",
+ origin: "https://example.com",
+ password: "qwerty2",
+ passwordField: "",
+ timeCreated: 1589617814635,
+ timeLastUsed: 1589710449871,
+ timePasswordChanged: 1589617846802,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Imports login data from a Firefox CSV file where only cells containing a comma are quoted.
+ */
+add_task(async function test_import_from_firefox_auth_some_quoted_fields() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ 'https://example.com,joe@example.com,"one,two,tree","My realm",,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802',
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.authLogin({
+ formActionOrigin: null,
+ httpRealm: "My realm",
+ origin: "https://example.com",
+ password: "one,two,tree",
+ passwordField: "",
+ timeCreated: 1589617814635,
+ timePasswordChanged: 1589617846802,
+ timeLastUsed: 1589710449871,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Imports login data from a Firefox CSV file with an empty formActionOrigin and null httpRealm
+ */
+add_task(async function test_import_from_firefox_form_empty_formActionOrigin() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "https://example.com,joe@example.com,s3cret1,,,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814636,1589710449872,1589617846803",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "https://example.com",
+ password: "s3cret1",
+ passwordField: "",
+ timeCreated: 1589617814636,
+ timePasswordChanged: 1589617846803,
+ timeLastUsed: 1589710449872,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Imports login data from a Firefox CSV file with a non-empty formActionOrigin and null httpRealm.
+ */
+add_task(async function test_import_from_firefox_form_with_formActionOrigin() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "http://example.com,joe@example.com,s3cret1,,https://other.example.com,{5ec0d12f-e194-4279-ae1b-d7d281bb46f1},1589617814635,1589710449871,1589617846802",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "https://other.example.com",
+ httpRealm: null,
+ origin: "http://example.com",
+ password: "s3cret1",
+ passwordField: "",
+ timeCreated: 1589617814635,
+ timePasswordChanged: 1589617846802,
+ timeLastUsed: 1589710449871,
+ timesUsed: 1,
+ username: "joe@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new login was added with the correct fields",
+ (a, e) => a.equals(e) && checkMetaInfo(a, e)
+ );
+});
+
+/**
+ * Imports login data from a Bitwarden CSV file.
+ * `name` is ignored until bug 1433770.
+ */
+add_task(async function test_import_from_bitwarden_csv() {
+ let csvFilePath = await setupCsv([
+ "folder,favorite,type,name,notes,fields,login_uri,login_username,login_password,login_totp",
+ `,,note,jane's note,"secret note, ignore me!",,,,,`,
+ ",,login,example.com,,,https://example.com/login,jane@example.com,secret_password",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "https://example.com",
+ password: "secret_password",
+ passwordField: "",
+ timesUsed: 1,
+ username: "jane@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new Bitwarden login was added with the correct fields",
+ (a, e) =>
+ a.equals(e) &&
+ checkMetaInfo(a, e, ["timesUsed"]) &&
+ checkLoginNewlyCreated(a)
+ );
+});
+
+/**
+ * Imports login data from a Chrome CSV file.
+ * `name` is ignored until bug 1433770.
+ */
+add_task(async function test_import_from_chrome_csv() {
+ let csvFilePath = await setupCsv([
+ "name,url,username,password",
+ "example.com,https://example.com/login,jane@example.com,secret_chrome_password",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "https://example.com",
+ password: "secret_chrome_password",
+ passwordField: "",
+ timesUsed: 1,
+ username: "jane@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new Chrome login was added with the correct fields",
+ (a, e) =>
+ a.equals(e) &&
+ checkMetaInfo(a, e, ["timesUsed"]) &&
+ checkLoginNewlyCreated(a)
+ );
+});
+
+/**
+ * Imports login data with an item without the username.
+ */
+add_task(async function test_import_login_without_username() {
+ let csvFilePath = await setupCsv([
+ "url,username,password",
+ "https://example.com/login,,secret_password",
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "https://example.com",
+ password: "secret_password",
+ passwordField: "",
+ timesUsed: 1,
+ username: "",
+ usernameField: "",
+ }),
+ ],
+ "Check that a Login is added without an username",
+ (a, e) =>
+ a.equals(e) &&
+ checkMetaInfo(a, e, ["timesUsed"]) &&
+ checkLoginNewlyCreated(a)
+ );
+});
+
+/**
+ * Imports login data from a KeepassXC CSV file.
+ * `Title` is ignored until bug 1433770.
+ */
+add_task(async function test_import_from_keepassxc_csv() {
+ let csvFilePath = await setupCsv([
+ `"Group","Title","Username","Password","URL","Notes"`,
+ `"NewDatabase/Internet","Amazing","test@example.com","<password>","https://example.org",""`,
+ ]);
+
+ await LoginCSVImport.importFromCSV(csvFilePath);
+
+ LoginTestUtils.checkLogins(
+ [
+ TestData.formLogin({
+ formActionOrigin: "",
+ httpRealm: null,
+ origin: "https://example.org",
+ password: "<password>",
+ passwordField: "",
+ timesUsed: 1,
+ username: "test@example.com",
+ usernameField: "",
+ }),
+ ],
+ "Check that a new KeepassXC login was added with the correct fields",
+ (a, e) =>
+ a.equals(e) &&
+ checkMetaInfo(a, e, ["timesUsed"]) &&
+ checkLoginNewlyCreated(a)
+ );
+});
+
+/**
+ * Imports login data summary contains added logins.
+ */
+add_task(async function test_import_summary_contains_added_login() {
+ let csvFilePath = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "https://added.example.com,jane@example.com,added_passwordd,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0003},1589617814635,1589710449871,1589617846802",
+ ]);
+
+ let [added] = await LoginCSVImport.importFromCSV(csvFilePath);
+
+ equal(added.result, "added", `Check that the login was added`);
+});
+
+/**
+ * Imports login data summary contains modified logins without guid.
+ */
+add_task(async function test_import_summary_modified_login_without_guid() {
+ let histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+ 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);
+ assertHistogramTelemetry(histogram, 0, 1);
+ histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+
+ 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);
+ assertHistogramTelemetry(histogram, 1, 1);
+ 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",
+ ]);
+ 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",
+ ]);
+
+ let [modifiedWithGuid] = await LoginCSVImport.importFromCSV(csvFile.path);
+
+ equal(
+ modifiedWithGuid.result,
+ "modified",
+ `Check that the login was modified when it had the same guid`
+ );
+ 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)
+ );
+});
+
+/**
+ * Imports login data summary contains unchanged logins.
+ */
+add_task(async function test_import_summary_contains_unchanged_login() {
+ let histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+ let initialDataFile = await setupCsv([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "https://nochange.example.com,jane@example.com,nochange_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0002},1589617814635,1589710449871,1589617846802",
+ ]);
+ await LoginCSVImport.importFromCSV(initialDataFile);
+ assertHistogramTelemetry(histogram, 0, 1);
+ histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+
+ let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "https://nochange.example.com,jane@example.com,nochange_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0002},1589617814635,1589710449871,1589617846802",
+ ]);
+
+ let [noChange] = await LoginCSVImport.importFromCSV(csvFile.path);
+ assertHistogramTelemetry(histogram, 3, 1);
+ equal(noChange.result, "no_change", `Check that the login was not changed`);
+});
+
+/**
+ * Imports login data summary contains logins with errors in case of missing fields.
+ */
+add_task(async function test_import_summary_contains_missing_fields_errors() {
+ let histogram = TTU.getAndClearHistogram(CATEGORICAL_HISTOGRAM);
+ const missingFieldsToCheck = ["url", "password"];
+ const sourceObject = {
+ url: "https://invalid.password.example.com",
+ username: "jane@example.com",
+ password: "qwerty",
+ };
+ for (const missingField of missingFieldsToCheck) {
+ const clonedUser = { ...sourceObject };
+ clonedUser[missingField] = "";
+ let csvFilePath = await setupCsv([
+ "url,username,password",
+ `${clonedUser.url},${clonedUser.username},${clonedUser.password}`,
+ ]);
+
+ let [importLogin] = await LoginCSVImport.importFromCSV(csvFilePath);
+
+ equal(
+ importLogin.result,
+ "error_missing_field",
+ `Check that the missing field error is reported for ${missingField}`
+ );
+ equal(
+ importLogin.field_name,
+ missingField,
+ `Check that the invalid field name is correctly reported for the ${missingField}`
+ );
+ }
+ assertHistogramTelemetry(histogram, 2, 1);
+});
+
+/**
+ * Imports login with wrong file format will have correct errorType.
+ */
+add_task(async function test_import_summary_with_bad_format() {
+ let csvFilePath = await setupCsv(["password", "123qwe!@#QWE"]);
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /FILE_FORMAT_ERROR/,
+ "Check that the errorType is file format error"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added with bad format"
+ );
+});
+
+/**
+ * Imports login with wrong file type will have correct errorType.
+ */
+add_task(async function test_import_summary_with_non_csv_file() {
+ let csvFilePath = await setupCsv([
+ "<body>this is totally not a csv file</body>",
+ ]);
+
+ await Assert.rejects(
+ LoginCSVImport.importFromCSV(csvFilePath),
+ /FILE_FORMAT_ERROR/,
+ "Check that the errorType is file format error"
+ );
+
+ LoginTestUtils.checkLogins(
+ [],
+ "Check that no login was added with file of different format"
+ );
+});
+
+/**
+ * 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([
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ "https://example.com,jane@example.com,password1,My realm",
+ "https://example.com,jane@example.com,password2,My realm",
+ ]);
+
+ 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(
+ results[1].result,
+ "no_change",
+ `Check that the second login was skipped`
+ );
+ equal(initialLoginCount, 0, `Check that initially we had no logins`);
+ equal(afterImportLoginCount, 1, `Check that we imported only one login`);
+});
+
+/**
+ * Imports login with duplicated guid values throws error.
+ */
+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 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(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`);
+});