/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests the LoginStore object. */ "use strict"; // Globals ChromeUtils.defineESModuleGetters(this, { LoginStore: "resource://gre/modules/LoginStore.sys.mjs", }); const TEST_STORE_FILE_NAME = "test-logins.json"; const MAX_DATE_MS = 8640000000000000; // Tests /** * Saves login data to a file, then reloads it. */ add_task(async function test_save_reload() { let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); // The "load" method must be called before preparing the data to be saved. await storeForSave.load(); let rawLoginData = { id: storeForSave.data.nextId++, hostname: "http://www.example.com", httpRealm: null, formSubmitURL: "http://www.example.com", usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345), passwordField: "field_" + String.fromCharCode(421, 259, 349, 537), encryptedUsername: "(test)", encryptedPassword: "(test)", guid: "(test)", encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR, timeCreated: Date.now(), timeLastUsed: Date.now(), timePasswordChanged: Date.now(), timesUsed: 1, }; storeForSave.data.logins.push(rawLoginData); await storeForSave._save(); // Test the asynchronous initialization path. let storeForLoad = new LoginStore(storeForSave.path); await storeForLoad.load(); Assert.equal(storeForLoad.data.logins.length, 1); Assert.deepEqual(storeForLoad.data.logins[0], rawLoginData); // Test the synchronous initialization path. storeForLoad = new LoginStore(storeForSave.path); storeForLoad.ensureDataReady(); Assert.equal(storeForLoad.data.logins.length, 1); Assert.deepEqual(storeForLoad.data.logins[0], rawLoginData); }); /** * Checks that loading from a missing file results in empty arrays. */ add_task(async function test_load_empty() { let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); Assert.equal(false, await IOUtils.exists(store.path)); await store.load(); Assert.equal(false, await IOUtils.exists(store.path)); Assert.equal(store.data.logins.length, 0); }); /** * Checks that saving empty data still overwrites any existing file. */ add_task(async function test_save_empty() { const store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); await store.load(); await IOUtils.writeUTF8(store.path, "", { writeMode: "create" }); await store._save(); Assert.ok(await IOUtils.exists(store.path)); }); /** * Loads data from a string in a predefined format. The purpose of this test is * to verify that the JSON format used in previous versions can be loaded. */ add_task(async function test_load_string_predefined() { let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); let string = '{"logins":[{' + '"id":1,' + '"hostname":"http://www.example.com",' + '"httpRealm":null,' + '"formSubmitURL":"http://www.example.com",' + '"usernameField":"usernameField",' + '"passwordField":"passwordField",' + '"encryptedUsername":"(test)",' + '"encryptedPassword":"(test)",' + '"guid":"(test)",' + '"encType":1,' + '"timeCreated":1262304000000,' + '"timeLastUsed":1262390400000,' + '"timePasswordChanged":1262476800000,' + '"timesUsed":1}],"disabledHosts":[' + '"http://www.example.org"]}'; await IOUtils.writeUTF8(store.path, string, { tmpPath: store.path + ".tmp", }); await store.load(); Assert.equal(store.data.logins.length, 1); Assert.deepEqual(store.data.logins[0], { id: 1, hostname: "http://www.example.com", httpRealm: null, formSubmitURL: "http://www.example.com", usernameField: "usernameField", passwordField: "passwordField", encryptedUsername: "(test)", encryptedPassword: "(test)", guid: "(test)", encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR, timeCreated: 1262304000000, timeLastUsed: 1262390400000, timePasswordChanged: 1262476800000, timesUsed: 1, }); }); /** * Loads login data from a malformed JSON string. */ add_task(async function test_load_string_malformed() { let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); let string = '{"logins":[{"hostname":"http://www.example.com","id":1,'; await IOUtils.writeUTF8(store.path, string, { tmpPath: store.path + ".tmp", }); await store.load(); // A backup file should have been created. Assert.ok(await IOUtils.exists(store.path + ".corrupt")); await IOUtils.remove(store.path + ".corrupt"); // The store should be ready to accept new data. Assert.equal(store.data.logins.length, 0); }); /** * Loads login data from a malformed JSON string, using the synchronous * initialization path. */ add_task(async function test_load_string_malformed_sync() { let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); let string = '{"logins":[{"hostname":"http://www.example.com","id":1,'; await IOUtils.writeUTF8(store.path, string, { tmpPath: store.path + ".tmp", }); store.ensureDataReady(); // A backup file should have been created. Assert.ok(await IOUtils.exists(store.path + ".corrupt")); await IOUtils.remove(store.path + ".corrupt"); // The store should be ready to accept new data. Assert.equal(store.data.logins.length, 0); }); /** * Fix bad dates when loading login data */ add_task(async function test_load_bad_dates() { let rawLoginData = { encType: 1, encryptedPassword: "(test)", encryptedUsername: "(test)", formSubmitURL: "https://www.example.com", guid: "{2a97313f-873b-4048-9a3d-4f442b46c1e5}", hostname: "https://www.example.com", httpRealm: null, id: 1, passwordField: "pass", timesUsed: 1, usernameField: "email", }; let rawStoreData = { dismissedBreachAlertsByLoginGUID: {}, logins: [], nextId: 2, potentiallyVulnerablePasswords: [], version: 2, }; /** * test that: * - bogus (0 or out-of-range) date values in any of the date fields are replaced with the * earliest time marked by the other date fields * - bogus bogus (0 or out-of-range) date values in all date fields are replaced with the time of import */ let tests = [ { name: "Out-of-range time values", savedProps: { timePasswordChanged: MAX_DATE_MS + 1, timeLastUsed: MAX_DATE_MS + 1, timeCreated: MAX_DATE_MS + 1, }, expectedProps: { timePasswordChanged: "now", timeLastUsed: "now", timeCreated: "now", }, }, { name: "All zero time values", savedProps: { timePasswordChanged: 0, timeLastUsed: 0, timeCreated: 0, }, expectedProps: { timePasswordChanged: "now", timeLastUsed: "now", timeCreated: "now", }, }, { name: "Only timeCreated has value", savedProps: { timePasswordChanged: 0, timeLastUsed: 0, timeCreated: 946713600000, }, expectedProps: { timePasswordChanged: 946713600000, timeLastUsed: 946713600000, timeCreated: 946713600000, }, }, { name: "timeCreated has 0 value", savedProps: { timePasswordChanged: 946713600000, timeLastUsed: 946713600000, timeCreated: 0, }, expectedProps: { timePasswordChanged: 946713600000, timeLastUsed: 946713600000, timeCreated: 946713600000, }, }, { name: "timeCreated has out-of-range value", savedProps: { timePasswordChanged: 946713600000, timeLastUsed: 946713600000, timeCreated: MAX_DATE_MS + 1, }, expectedProps: { timePasswordChanged: 946713600000, timeLastUsed: 946713600000, timeCreated: 946713600000, }, }, { name: "Use earliest time for missing value", savedProps: { timePasswordChanged: 0, timeLastUsed: 946713600000, timeCreated: 946540800000, }, expectedProps: { timePasswordChanged: 946540800000, timeLastUsed: 946713600000, timeCreated: 946540800000, }, }, ]; for (let testData of tests) { let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path); let string = JSON.stringify( Object.assign({}, rawStoreData, { logins: [Object.assign({}, rawLoginData, testData.savedProps)], }) ); await IOUtils.writeUTF8(store.path, string, { tmpPath: store.path + ".tmp", }); let now = Date.now(); await store.load(); Assert.equal( store.data.logins.length, 1, `${testData.name}: Expected a single login` ); let login = store.data.logins[0]; for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) { if (testData.expectedProps[pname] === "now") { Assert.ok( login[pname] >= now, `${testData.name}: Check ${pname} is at/near now` ); } else { Assert.equal( login[pname], testData.expectedProps[pname], `${testData.name}: Check expected ${pname}` ); } } Assert.equal(store.data.version, 3, "Check version was bumped"); } });