/** * Tests FormAutofillStorage object with addresses records. */ "use strict"; const TEST_STORE_FILE_NAME = "test-profile.json"; const COLLECTION_NAME = "addresses"; const TEST_ADDRESS_1 = { "given-name": "Timothy", "additional-name": "John", "family-name": "Berners-Lee", organization: "World Wide Web Consortium", "street-address": "32 Vassar Street\nMIT Room 32-G524", "address-level2": "Cambridge", "address-level1": "MA", "postal-code": "02139", country: "US", tel: "+16172535702", email: "timbl@w3.org", "unknown-1": "an unknown field from another client", }; const TEST_ADDRESS_2 = { "street-address": "Some Address", country: "US", }; const TEST_ADDRESS_3 = { "given-name": "Timothy", "family-name": "Berners-Lee", "street-address": "Other Address", "postal-code": "12345", }; const TEST_ADDRESS_4 = { "given-name": "Timothy", "additional-name": "John", "family-name": "Berners-Lee", organization: "World Wide Web Consortium", }; const TEST_ADDRESS_WITH_EMPTY_FIELD = { name: "Tim Berners", "street-address": "", }; const TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD = { name: "", "address-line1": "", "address-line2": "", "address-line3": "", "country-name": "", "tel-country-code": "", "tel-national": "", "tel-area-code": "", "tel-local": "", "tel-local-prefix": "", "tel-local-suffix": "", email: "timbl@w3.org", }; const TEST_ADDRESS_WITH_INVALID_FIELD = { "street-address": "Another Address", email: { email: "invalidemail" }, }; const TEST_ADDRESS_EMPTY_AFTER_NORMALIZE = { country: "XXXXXX", }; const TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2 = { "street-address": "", country: "XXXXXX", }; const MERGE_TESTCASES = [ { description: "Merge a superset", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", "unknown-1": "an unknown field from another client", }, addressToMerge: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", "unknown-1": "an unknown field from another client", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", "unknown-1": "an unknown field from another client", }, }, { description: "Loose merge a subset", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, addressToMerge: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, noNeedToUpdate: true, }, { description: "Strict merge a subset without empty string", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, addressToMerge: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, strict: true, noNeedToUpdate: true, }, { description: "Merge an address with partial overlaps", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with multi-line street-address in storage and single-line incoming one", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn Avenue Line2", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with 3-line street-address in storage and 2-line incoming one", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn Avenue\nLine2 Line3", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with single-line street-address in storage and multi-line incoming one", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue Line2", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn Avenue\nLine2", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with 2-line street-address in storage and 3-line incoming one", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2 Line3", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with the same amount of lines", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E. Evelyn\nAvenue Line2\nLine3", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with superfluous external and internal whitespace in the street-address", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", }, addressToMerge: { "street-address": " 331 E. Evelyn\n Avenue Line2\n Line3 ", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue\nLine2\nLine3", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with collapsed whitespace", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", }, addressToMerge: { "street-address": "331 E.Evelyn Avenue", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with punctuation and mIxEd-cAsE", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", }, addressToMerge: { "street-address": "331.e.EVELYN AVENUE", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Avenue", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with accent characters", addressInStorage: { "given-name": "Timothy", "street-address": "331 E. Evelyn Straße", tel: "+16509030800", }, addressToMerge: { "street-address": "331.e.EVELYN Strasse", country: "US", }, expectedAddress: { "given-name": "Timothy", "street-address": "331 E. Evelyn Straße", tel: "+16509030800", country: "US", }, }, { description: "Merge an address with a mIxEd-cAsE name", addressInStorage: { "given-name": "Timothy", tel: "+16509030800", }, addressToMerge: { "given-name": "TIMOTHY", tel: "+16509030800", country: "US", }, expectedAddress: { "given-name": "Timothy", tel: "+16509030800", country: "US", }, }, ]; ChromeUtils.defineESModuleGetters(this, { Preferences: "resource://gre/modules/Preferences.sys.mjs", }); let do_check_record_matches = (recordWithMeta, record) => { for (let key in record) { Assert.equal(recordWithMeta[key], record[key]); } }; add_task(async function test_initialize() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); Assert.equal(profileStorage._store.data.version, 1); Assert.equal(profileStorage._store.data.addresses.length, 0); let data = profileStorage._store.data; Assert.deepEqual(data.addresses, []); await profileStorage._saveImmediately(); profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); Assert.deepEqual(profileStorage._store.data, data); for (let { _sync } of profileStorage._store.data.addresses) { Assert.ok(_sync); Assert.equal(_sync.changeCounter, 1); } }); add_task(async function test_getAll() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); Assert.equal(addresses.length, 2); do_check_record_matches(addresses[0], TEST_ADDRESS_1); do_check_record_matches(addresses[1], TEST_ADDRESS_2); // Check computed fields. Assert.equal(addresses[0].name, "Timothy John Berners-Lee"); Assert.equal(addresses[0]["address-line1"], "32 Vassar Street"); Assert.equal(addresses[0]["address-line2"], "MIT Room 32-G524"); // Test with rawData set. addresses = await profileStorage.addresses.getAll({ rawData: true }); Assert.equal(addresses[0].name, undefined); Assert.equal(addresses[0]["address-line1"], undefined); Assert.equal(addresses[0]["address-line2"], undefined); // Modifying output shouldn't affect the storage. addresses[0].organization = "test"; do_check_record_matches( (await profileStorage.addresses.getAll())[0], TEST_ADDRESS_1 ); }); add_task(async function test_get() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[0].guid; let address = await profileStorage.addresses.get(guid); do_check_record_matches(address, TEST_ADDRESS_1); // Test with rawData set. address = await profileStorage.addresses.get(guid, { rawData: true }); Assert.equal(address.name, undefined); Assert.equal(address["address-line1"], undefined); Assert.equal(address["address-line2"], undefined); // Modifying output shouldn't affect the storage. address.organization = "test"; do_check_record_matches( await profileStorage.addresses.get(guid), TEST_ADDRESS_1 ); Assert.equal(await profileStorage.addresses.get("INVALID_GUID"), null); }); add_task(async function test_add() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); Assert.equal(addresses.length, 2); do_check_record_matches(addresses[0], TEST_ADDRESS_1); do_check_record_matches(addresses[1], TEST_ADDRESS_2); Assert.notEqual(addresses[0].guid, undefined); Assert.equal(addresses[0].version, 1); Assert.notEqual(addresses[0].timeCreated, undefined); Assert.equal(addresses[0].timeLastModified, addresses[0].timeCreated); Assert.equal(addresses[0].timeLastUsed, 0); Assert.equal(addresses[0].timesUsed, 0); // Empty string should be deleted before saving. await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_FIELD); let address = profileStorage.addresses._data[2]; Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name); Assert.equal(address["street-address"], undefined); // Empty computed fields shouldn't cause any problem. await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD); address = profileStorage.addresses._data[3]; Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); await Assert.rejects( profileStorage.addresses.add(TEST_ADDRESS_WITH_INVALID_FIELD), /"email" contains invalid data type: object/ ); await Assert.rejects( profileStorage.addresses.add({}), /Record contains no valid field\./ ); await Assert.rejects( profileStorage.addresses.add(TEST_ADDRESS_EMPTY_AFTER_NORMALIZE), /Record contains no valid field\./ ); }); add_task(async function test_update() { // Test assumes that when an entry is saved a second time, it's last modified date will // be different from the first. With high values of precision reduction, we execute too // fast for that to be true. let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); Preferences.set("privacy.reduceTimerPrecision", false); registerCleanupFunction(function () { Preferences.set("privacy.reduceTimerPrecision", timerPrecision); }); let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[1].guid; // We need to cheat a little due to race conditions of Date.now() when // we're running these tests, so we subtract one and test accordingly // in the times Date.now() returns the same timestamp let timeLastModified = addresses[1].timeLastModified - 1; let onChanged = TestUtils.topicObserved( "formautofill-storage-changed", (subject, data) => data == "update" && subject.wrappedJSObject.guid == guid && subject.wrappedJSObject.collectionName == COLLECTION_NAME ); Assert.notEqual(addresses[1].country, undefined); await profileStorage.addresses.update(guid, TEST_ADDRESS_3); await onChanged; await profileStorage._saveImmediately(); profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. let address = await profileStorage.addresses.get(guid, { rawData: true }); Assert.equal(address.country, undefined); Assert.ok(address.timeLastModified > timeLastModified); do_check_record_matches(address, TEST_ADDRESS_3); Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); // Test preserveOldProperties parameter and field with empty string. await profileStorage.addresses.update( guid, TEST_ADDRESS_WITH_EMPTY_FIELD, true ); await onChanged; await profileStorage._saveImmediately(); profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. address = await profileStorage.addresses.get(guid, { rawData: true }); Assert.equal(address["given-name"], "Tim"); Assert.equal(address["family-name"], "Berners"); Assert.equal(address["street-address"], undefined); Assert.equal(address["postal-code"], "12345"); Assert.notEqual(address.timeLastModified, timeLastModified); Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2); // Empty string should be deleted while updating. await profileStorage.addresses.update( profileStorage.addresses._data[0].guid, TEST_ADDRESS_WITH_EMPTY_FIELD ); address = profileStorage.addresses._data[0]; Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name); Assert.equal(address["street-address"], undefined); Assert.equal(address[("unknown-1", "an unknown field from another client")]); // Empty computed fields shouldn't cause any problem. await profileStorage.addresses.update( profileStorage.addresses._data[0].guid, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, false ); address = profileStorage.addresses._data[0]; Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); await profileStorage.addresses.update( profileStorage.addresses._data[1].guid, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, true ); address = profileStorage.addresses._data[1]; Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email); await Assert.rejects( profileStorage.addresses.update("INVALID_GUID", TEST_ADDRESS_3), /No matching record\./ ); await Assert.rejects( profileStorage.addresses.update(guid, TEST_ADDRESS_WITH_INVALID_FIELD), /"email" contains invalid data type: object/ ); await Assert.rejects( profileStorage.addresses.update(guid, {}), /Record contains no valid field\./ ); await Assert.rejects( profileStorage.addresses.update(guid, TEST_ADDRESS_EMPTY_AFTER_NORMALIZE), /Record contains no valid field\./ ); profileStorage.addresses.update(guid, TEST_ADDRESS_2); await Assert.rejects( profileStorage.addresses.update( guid, TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2 ), /Record contains no valid field\./ ); }); add_task(async function test_notifyUsed() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[1].guid; let timeLastUsed = addresses[1].timeLastUsed; let timesUsed = addresses[1].timesUsed; profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below. let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); let onChanged = TestUtils.topicObserved( "formautofill-storage-changed", (subject, data) => data == "notifyUsed" && subject.wrappedJSObject.guid == guid && subject.wrappedJSObject.collectionName == COLLECTION_NAME ); profileStorage.addresses.notifyUsed(guid); await onChanged; let address = await profileStorage.addresses.get(guid); Assert.equal(address.timesUsed, timesUsed + 1); Assert.notEqual(address.timeLastUsed, timeLastUsed); // Using a record should not bump its change counter. Assert.equal( getSyncChangeCounter(profileStorage.addresses, guid), changeCounter ); Assert.throws( () => profileStorage.addresses.notifyUsed("INVALID_GUID"), /No matching record\./ ); }); add_task(async function test_remove() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[1].guid; let onChanged = TestUtils.topicObserved( "formautofill-storage-changed", (subject, data) => data == "remove" && subject.wrappedJSObject.guid == guid && subject.wrappedJSObject.collectionName == COLLECTION_NAME ); Assert.equal(addresses.length, 2); profileStorage.addresses.remove(guid); await onChanged; addresses = await profileStorage.addresses.getAll(); Assert.equal(addresses.length, 1); Assert.equal(await profileStorage.addresses.get(guid), null); }); MERGE_TESTCASES.forEach(testcase => { add_task(async function test_merge() { info("Starting testcase: " + testcase.description); let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ testcase.addressInStorage, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[0].guid; // We need to cheat a little due to race conditions of Date.now() when // we're running these tests, so we subtract one and test accordingly // in the times Date.now() returns the same timestamp let timeLastModified = addresses[0].timeLastModified - 1; // Merge address and verify the guid in notifyObservers subject let onMerged = TestUtils.topicObserved( "formautofill-storage-changed", (subject, data) => data == "update" && subject.wrappedJSObject.guid == guid && subject.wrappedJSObject.collectionName == COLLECTION_NAME ); // Force to create sync metadata. profileStorage.addresses.pullSyncChanges(); Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); Assert.ok( profileStorage.addresses.mergeIfPossible( guid, testcase.addressToMerge, testcase.strict ) ); if (!testcase.noNeedToUpdate) { await onMerged; } addresses = await profileStorage.addresses.getAll(); Assert.equal(addresses.length, 1); do_check_record_matches(addresses[0], testcase.expectedAddress); if (testcase.noNeedToUpdate) { // see timeLastModified for why we check -1 Assert.equal(addresses[0].timeLastModified - 1, timeLastModified); // No need to bump the change counter if the data is unchanged. Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); } else { Assert.ok(addresses[0].timeLastModified > timeLastModified); // Record merging should bump the change counter. Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2); } }); }); add_task(async function test_merge_same_address() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[0].guid; let timeLastModified = addresses[0].timeLastModified; // Force to create sync metadata. profileStorage.addresses.pullSyncChanges(); Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); // Merge same address will still return true but it won't update timeLastModified. Assert.ok(profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_1)); Assert.equal(addresses[0].timeLastModified, timeLastModified); // ... and won't bump the change counter, either. Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); }); add_task(async function test_merge_unable_merge() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); let addresses = await profileStorage.addresses.getAll(); let guid = addresses[1].guid; // Force to create sync metadata. profileStorage.addresses.pullSyncChanges(); Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); // Unable to merge because of conflict Assert.equal( await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_3), false ); // Unable to merge because no overlap Assert.equal( await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_4), false ); // Unable to strict merge because subset with empty string let subset = Object.assign({}, TEST_ADDRESS_1); subset.organization = ""; Assert.equal( await profileStorage.addresses.mergeIfPossible(guid, subset, true), false ); // Shouldn't bump the change counter Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); }); add_task(async function test_mergeToStorage() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); // Merge an address to storage let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_2); await profileStorage.addresses.add(anotherAddress); anotherAddress.email = "timbl@w3.org"; Assert.equal( (await profileStorage.addresses.mergeToStorage(anotherAddress)).length, 2 ); Assert.equal( (await profileStorage.addresses.getAll())[1].email, anotherAddress.email ); Assert.equal( (await profileStorage.addresses.getAll())[2].email, anotherAddress.email ); // Empty computed fields shouldn't cause any problem. Assert.equal( ( await profileStorage.addresses.mergeToStorage( TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD ) ).length, 3 ); }); add_task(async function test_mergeToStorage_strict() { let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ TEST_ADDRESS_1, TEST_ADDRESS_2, ]); // Try to merge a subset with empty string let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_1); anotherAddress.email = ""; Assert.equal( (await profileStorage.addresses.mergeToStorage(anotherAddress, true)) .length, 0 ); Assert.equal( (await profileStorage.addresses.getAll())[0].email, TEST_ADDRESS_1.email ); // Empty computed fields shouldn't cause any problem. Assert.equal( ( await profileStorage.addresses.mergeToStorage( TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD, true ) ).length, 1 ); });