diff options
Diffstat (limited to '')
-rw-r--r-- | browser/extensions/formautofill/test/unit/test_storage_syncfields.js | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/test/unit/test_storage_syncfields.js b/browser/extensions/formautofill/test/unit/test_storage_syncfields.js new file mode 100644 index 0000000000..e304aa4df0 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_storage_syncfields.js @@ -0,0 +1,498 @@ +/** + * Tests FormAutofillStorage objects support for sync related fields. + */ + +"use strict"; + +// The duplication of some of these fixtures between tests is unfortunate. +const TEST_STORE_FILE_NAME = "test-profile.json"; + +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: "+1 617 253 5702", + email: "timbl@w3.org", + "unknown-1": "an unknown field we roundtrip", +}; + +const TEST_ADDRESS_2 = { + "street-address": "Some Address", + country: "US", +}; + +const TEST_ADDRESS_3 = { + "street-address": "Other Address", + "postal-code": "12345", +}; + +// storage.get() doesn't support getting deleted items. However, this test +// wants to do that, so rather than making .get() support that just for this +// test, we use this helper. +async function findGUID(storage, guid, options) { + let all = await storage.getAll(options); + let records = all.filter(r => r.guid == guid); + equal(records.length, 1); + return records[0]; +} + +add_task(async function test_changeCounter() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + + let [address] = await profileStorage.addresses.getAll(); + // new records don't get the sync metadata. + equal(getSyncChangeCounter(profileStorage.addresses, address.guid), -1); + // But we can force one. + profileStorage.addresses.pullSyncChanges(); + equal(getSyncChangeCounter(profileStorage.addresses, address.guid), 1); +}); + +add_task(async function test_pushChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + profileStorage.addresses.pullSyncChanges(); // force sync metadata for all items + + let [, address] = await profileStorage.addresses.getAll(); + let guid = address.guid; + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + + // Pretend we're doing a sync now, and an update occured mid-sync. + let changes = { + [guid]: { + profile: address, + counter: changeCounter, + modified: address.timeLastModified, + synced: true, + }, + }; + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "update" + ); + await profileStorage.addresses.update(guid, TEST_ADDRESS_3); + await onChanged; + + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + Assert.equal(changeCounter, 2); + + profileStorage.addresses.pushSyncChanges(changes); + address = await profileStorage.addresses.get(guid); + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + + // Counter should still be 1, since our sync didn't record the mid-sync change + Assert.equal( + changeCounter, + 1, + "Counter shouldn't be zero because it didn't record update" + ); + + // now, push a new set of changes, which should make the changeCounter 0 + profileStorage.addresses.pushSyncChanges({ + [guid]: { + profile: address, + counter: changeCounter, + modified: address.timeLastModified, + synced: true, + }, + }); + + changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + Assert.equal(changeCounter, 0); +}); + +async function checkingSyncChange(action, callback) { + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == action + ); + await callback(); + let [subject] = await onChanged; + ok( + subject.wrappedJSObject.sourceSync, + "change notification should have source sync" + ); +} + +add_task(async function test_add_sourceSync() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + // Hardcode a guid so that we don't need to generate a dynamic regex + let guid = "aaaaaaaaaaaa"; + let testAddr = Object.assign({ guid, version: 1 }, TEST_ADDRESS_1); + + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, 0); + + await Assert.rejects( + profileStorage.addresses.add({ guid, deleted: true }, { sourceSync: true }), + /Record aaaaaaaaaaaa already exists/ + ); +}); + +add_task(async function test_add_tombstone_sourceSync() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + let testAddr = { guid, deleted: true }; + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let added = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(added); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + ok(added.deleted); + + // Adding same record again shouldn't throw (or change anything) + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + added = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + ok(added.deleted); +}); + +add_task(async function test_add_resurrects_tombstones() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + + // Add a tombstone. + await profileStorage.addresses.add({ guid, deleted: true }); + + // You can't re-add an item with an explicit GUID. + let resurrected = Object.assign({}, TEST_ADDRESS_1, { guid, version: 1 }); + await Assert.rejects( + profileStorage.addresses.add(resurrected), + /"(guid|version)" is not a valid field/ + ); + + // But Sync can! + let guid3 = await profileStorage.addresses.add(resurrected, { + sourceSync: true, + }); + equal(guid, guid3); + + let got = await profileStorage.addresses.get(guid); + equal(got["given-name"], TEST_ADDRESS_1["given-name"]); +}); + +add_task(async function test_remove_sourceSync_localChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + profileStorage.addresses.pullSyncChanges(); // force sync metadata + + let [{ guid }] = await profileStorage.addresses.getAll(); + + equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); + // try and remove a record stored locally with local changes + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let record = await profileStorage.addresses.get(guid); + ok(record); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 1); +}); + +add_task(async function test_remove_sourceSync_unknown() { + // remove a record not stored locally + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let tombstone = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(tombstone.deleted); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); +}); + +add_task(async function test_remove_sourceSync_unchanged() { + // Remove a local record without a change counter. + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let guid = profileStorage.addresses._generateGUID(); + let addr = Object.assign({ guid, version: 1 }, TEST_ADDRESS_1); + // add a record with sourceSync to guarantee changeCounter == 0 + await checkingSyncChange("add", async () => + profileStorage.addresses.add(addr, { sourceSync: true }) + ); + + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); + + await checkingSyncChange("remove", async () => + profileStorage.addresses.remove(guid, { sourceSync: true }) + ); + + let tombstone = await findGUID(profileStorage.addresses, guid, { + includeDeleted: true, + }); + ok(tombstone.deleted); + equal(getSyncChangeCounter(profileStorage.addresses, guid), 0); +}); + +add_task(async function test_pullSyncChanges() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let startAddresses = await profileStorage.addresses.getAll(); + equal(startAddresses.length, 2); + // All should start without sync metadata + for (let { guid } of profileStorage.addresses._store.data.addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } + profileStorage.addresses.pullSyncChanges(); // force sync metadata + + let addedDirectGUID = profileStorage.addresses._generateGUID(); + let testAddr = Object.assign( + { guid: addedDirectGUID, version: 1 }, + TEST_ADDRESS_1, + TEST_ADDRESS_3 + ); + + await checkingSyncChange("add", async () => + profileStorage.addresses.add(testAddr, { sourceSync: true }) + ); + + let tombstoneGUID = profileStorage.addresses._generateGUID(); + await checkingSyncChange("add", async () => + profileStorage.addresses.add( + { guid: tombstoneGUID, deleted: true }, + { sourceSync: true } + ) + ); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "remove" + ); + + profileStorage.addresses.remove(startAddresses[0].guid); + await onChanged; + + let addresses = await profileStorage.addresses.getAll({ + includeDeleted: true, + }); + + // Should contain changes with a change counter + let changes = profileStorage.addresses.pullSyncChanges(); + equal(Object.keys(changes).length, 2); + + ok(changes[startAddresses[0].guid].profile.deleted); + equal(changes[startAddresses[0].guid].counter, 2); + + ok(!changes[startAddresses[1].guid].profile.deleted); + equal(changes[startAddresses[1].guid].counter, 1); + + ok( + !changes[tombstoneGUID], + "Missing because it's a tombstone from sourceSync" + ); + ok(!changes[addedDirectGUID], "Missing because it was added with sourceSync"); + + for (let address of addresses) { + let change = changes[address.guid]; + if (!change) { + continue; + } + equal(change.profile.guid, address.guid); + let changeCounter = getSyncChangeCounter( + profileStorage.addresses, + change.profile.guid + ); + equal(change.counter, changeCounter); + ok(!change.synced); + } +}); + +add_task(async function test_pullPushChanges() { + // round-trip changes between pull and push + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + let psa = profileStorage.addresses; + + let guid1 = await psa.add(TEST_ADDRESS_1); + let guid2 = await psa.add(TEST_ADDRESS_2); + let guid3 = await psa.add(TEST_ADDRESS_3); + + let changes = psa.pullSyncChanges(); + + equal(getSyncChangeCounter(psa, guid1), 1); + equal(getSyncChangeCounter(psa, guid2), 1); + equal(getSyncChangeCounter(psa, guid3), 1); + + // between the pull and the push we change the second. + await psa.update(guid2, Object.assign({}, TEST_ADDRESS_2, { country: "AU" })); + equal(getSyncChangeCounter(psa, guid2), 2); + // and update the changeset to indicated we did update the first 2, but failed + // to update the 3rd for some reason. + changes[guid1].synced = true; + changes[guid2].synced = true; + + psa.pushSyncChanges(changes); + + // first was synced correctly. + equal(getSyncChangeCounter(psa, guid1), 0); + // second was synced correctly, but it had a change while syncing. + equal(getSyncChangeCounter(psa, guid2), 1); + // 3rd wasn't marked as having synced. + equal(getSyncChangeCounter(psa, guid3), 1); +}); + +add_task(async function test_changeGUID() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []); + + let newguid = () => profileStorage.addresses._generateGUID(); + + let guid_synced = await profileStorage.addresses.add(TEST_ADDRESS_1); + + // pullSyncChanges so guid_synced is flagged as syncing. + profileStorage.addresses.pullSyncChanges(); + + // and 2 items that haven't been synced. + let guid_u1 = await profileStorage.addresses.add(TEST_ADDRESS_2); + let guid_u2 = await profileStorage.addresses.add(TEST_ADDRESS_3); + + // Change a non-existing guid + Assert.throws( + () => profileStorage.addresses.changeGUID(newguid(), newguid()), + /changeGUID: no source record/ + ); + // Change to a guid that already exists. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_u1, guid_u2), + /changeGUID: record with destination id exists already/ + ); + // Try and change a guid that's already been synced. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_synced, newguid()), + /changeGUID: existing record has already been synced/ + ); + + // Change an item to itself makes no sense. + Assert.throws( + () => profileStorage.addresses.changeGUID(guid_u1, guid_u1), + /changeGUID: old and new IDs are the same/ + ); + + // and one that works. + equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 3 + ); + let targetguid = newguid(); + profileStorage.addresses.changeGUID(guid_u1, targetguid); + equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 3 + ); + + ok( + await profileStorage.addresses.get(guid_synced), + "synced item still exists." + ); + ok( + await profileStorage.addresses.get(guid_u2), + "guid we didn't touch still exists." + ); + ok(await profileStorage.addresses.get(targetguid), "target guid exists."); + ok( + !(await profileStorage.addresses.get(guid_u1)), + "old guid no longer exists." + ); +}); + +add_task(async function test_findDuplicateGUID() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + ]); + + let [record] = await profileStorage.addresses.getAll({ rawData: true }); + await Assert.rejects( + profileStorage.addresses.findDuplicateGUID(record), + /Record \w+ already exists/, + "Should throw if the GUID already exists" + ); + + // Add a malformed record, passing `sourceSync` to work around the record + // normalization logic that would prevent this. + let timeLastModified = Date.now(); + let timeCreated = timeLastModified - 60 * 1000; + + await profileStorage.addresses.add( + { + guid: profileStorage.addresses._generateGUID(), + version: 1, + timeCreated, + timeLastModified, + }, + { sourceSync: true } + ); + + strictEqual( + await profileStorage.addresses.findDuplicateGUID({ + guid: profileStorage.addresses._generateGUID(), + version: 1, + timeCreated, + timeLastModified, + }), + null, + "Should ignore internal fields and malformed records" + ); +}); + +add_task(async function test_reset() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [ + TEST_ADDRESS_1, + TEST_ADDRESS_2, + ]); + + let addresses = await profileStorage.addresses.getAll(); + // All should start without sync metadata + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } + // pullSyncChanges should create the metadata. + profileStorage.addresses.pullSyncChanges(); + addresses = await profileStorage.addresses.getAll(); + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, 1); + } + // and resetSync should wipe it. + profileStorage.addresses.resetSync(); + addresses = await profileStorage.addresses.getAll(); + for (let { guid } of addresses) { + let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid); + equal(changeCounter, -1); + } +}); |