/** * 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); } });