From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../extensions/formautofill/test/unit/test_sync.js | 1017 ++++++++++++++++++++ 1 file changed, 1017 insertions(+) create mode 100644 browser/extensions/formautofill/test/unit/test_sync.js (limited to 'browser/extensions/formautofill/test/unit/test_sync.js') diff --git a/browser/extensions/formautofill/test/unit/test_sync.js b/browser/extensions/formautofill/test/unit/test_sync.js new file mode 100644 index 0000000000..bc9467d7c9 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_sync.js @@ -0,0 +1,1017 @@ +/** + * Tests sync functionality. + */ + +/* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */ +/* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */ +/* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */ + +"use strict"; + +const { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); +const { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( + "resource://services-sync/constants.sys.mjs" +); + +const { sanitizeStorageObject, AutofillRecord, AddressesEngine } = + ChromeUtils.importESModule("resource://autofill/FormAutofillSync.sys.mjs"); + +Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace"); +initTestLogging("Trace"); + +const TEST_STORE_FILE_NAME = "test-profile.json"; + +const TEST_PROFILE_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", + // A field this client doesn't "understand" from another client + "unknown-1": "some unknown data from another client", +}; + +const TEST_PROFILE_2 = { + "street-address": "Some Address", + country: "US", +}; + +async function expectLocalProfiles(profileStorage, expected) { + let profiles = await profileStorage.addresses.getAll({ + rawData: true, + includeDeleted: true, + }); + expected.sort((a, b) => a.guid.localeCompare(b.guid)); + profiles.sort((a, b) => a.guid.localeCompare(b.guid)); + try { + deepEqual( + profiles.map(p => p.guid), + expected.map(p => p.guid) + ); + for (let i = 0; i < expected.length; i++) { + let thisExpected = expected[i]; + let thisGot = profiles[i]; + // always check "deleted". + equal(thisExpected.deleted, thisGot.deleted); + ok(objectMatches(thisGot, thisExpected)); + } + } catch (ex) { + info("Comparing expected profiles:"); + info(JSON.stringify(expected, undefined, 2)); + info("against actual profiles:"); + info(JSON.stringify(profiles, undefined, 2)); + throw ex; + } +} + +async function setup() { + let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME); + // should always start with no profiles. + Assert.equal( + (await profileStorage.addresses.getAll({ includeDeleted: true })).length, + 0 + ); + + Services.prefs.setCharPref( + "services.sync.log.logger.engine.addresses", + "Trace" + ); + let engine = new AddressesEngine(Service); + await engine.initialize(); + // Avoid accidental automatic sync due to our own changes + Service.scheduler.syncThreshold = 10000000; + let syncID = await engine.resetLocalSyncID(); + let server = serverForUsers( + { foo: "password" }, + { + meta: { + global: { engines: { addresses: { version: engine.version, syncID } } }, + }, + addresses: {}, + } + ); + + Service.engineManager._engines.addresses = engine; + engine.enabled = true; + engine._store._storage = profileStorage.addresses; + + generateNewKeys(Service.collectionKeys); + + await SyncTestingInfrastructure(server); + + let collection = server.user("foo").collection("addresses"); + + return { profileStorage, server, collection, engine }; +} + +async function cleanup(server) { + let promiseStartOver = promiseOneObserver("weave:service:start-over:finish"); + await Service.startOver(); + await promiseStartOver; + await promiseStopServer(server); +} + +add_task(async function test_log_sanitization() { + let sanitized = sanitizeStorageObject(TEST_PROFILE_1); + // all strings have been mangled. + for (let key of Object.keys(TEST_PROFILE_1)) { + let val = TEST_PROFILE_1[key]; + if (typeof val == "string") { + notEqual(sanitized[key], val); + } + } + // And check that stringifying a sync record is sanitized. + let record = new AutofillRecord("collection", "some-id"); + record.entry = TEST_PROFILE_1; + let serialized = record.toString(); + // None of the string values should appear in the output. + for (let key of Object.keys(TEST_PROFILE_1)) { + let val = TEST_PROFILE_1[key]; + if (typeof val == "string") { + ok(!serialized.includes(val), `"${val}" shouldn't be in: ${serialized}`); + } + } +}); + +add_task(async function test_outgoing() { + let { profileStorage, server, collection, engine } = await setup(); + try { + equal(engine._tracker.score, 0); + let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); + // And a deleted item. + let deletedGUID = profileStorage.addresses._generateGUID(); + await profileStorage.addresses.add({ guid: deletedGUID, deleted: true }); + + await expectLocalProfiles(profileStorage, [ + { + guid: existingGUID, + }, + { + guid: deletedGUID, + deleted: true, + }, + ]); + + await engine._tracker.asyncObserver.promiseObserversComplete(); + // The tracker should have a score recorded for the 2 additions we had. + equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2); + + await engine.setLastSync(0); + await engine.sync(); + + Assert.equal(collection.count(), 2); + Assert.ok(collection.wbo(existingGUID)); + Assert.ok(collection.wbo(deletedGUID)); + + await expectLocalProfiles(profileStorage, [ + { + guid: existingGUID, + }, + { + guid: deletedGUID, + deleted: true, + }, + ]); + + strictEqual( + getSyncChangeCounter(profileStorage.addresses, existingGUID), + 0 + ); + strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedGUID), 0); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_new() { + let { profileStorage, server, engine } = await setup(); + try { + let profileID = Utils.makeGUID(); + let deletedID = Utils.makeGUID(); + + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + profileID, + encryptPayload({ + id: profileID, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + deletedID, + encryptPayload({ + id: deletedID, + deleted: true, + }), + getDateForSync() + ) + ); + + // The tracker should start with no score. + equal(engine._tracker.score, 0); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: profileID, + }, + { + guid: deletedID, + deleted: true, + }, + ]); + + strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); + strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedID), 0); + + // Validate incoming records with unknown fields get stored + let localRecord = await profileStorage.addresses.get(profileID); + equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); + + // The sync applied new records - ensure our tracker knew it came from + // sync and didn't bump the score. + equal(engine._tracker.score, 0); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_existing() { + let { profileStorage, server, engine } = await setup(); + try { + let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); + let guid2 = await profileStorage.addresses.add(TEST_PROFILE_2); + + // an initial sync so we don't think they are locally modified. + await engine.setLastSync(0); + await engine.sync(); + + // now server records that modify the existing items. + let modifiedEntry1 = Object.assign({}, TEST_PROFILE_1, { + version: 1, + "given-name": "NewName", + }); + + let lastSync = await engine.getLastSync(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid1, + encryptPayload({ + id: guid1, + entry: modifiedEntry1, + }), + lastSync + 10 + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid2, + encryptPayload({ + id: guid2, + deleted: true, + }), + lastSync + 10 + ) + ); + + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + Object.assign({}, modifiedEntry1, { guid: guid1 }), + { guid: guid2, deleted: true }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_tombstones() { + let { profileStorage, server, collection, engine } = await setup(); + try { + let existingGUID = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + Assert.equal(collection.count(), 1); + let payload = collection.payloads()[0]; + equal(payload.id, existingGUID); + equal(payload.deleted, undefined); + + profileStorage.addresses.remove(existingGUID); + await engine.sync(); + + // should still exist, but now be a tombstone. + Assert.equal(collection.count(), 1); + payload = collection.payloads()[0]; + equal(payload.id, existingGUID); + equal(payload.deleted, true); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_both_deleted() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Delete synced record locally. + profileStorage.addresses.remove(guid); + + // Delete same record remotely. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + await engine.sync(); + + ok( + !(await await profileStorage.addresses.get(guid)), + "Should not return record for locally deleted item" + ); + + let localRecords = await profileStorage.addresses.getAll({ + includeDeleted: true, + }); + equal(localRecords.length, 1, "Only tombstone should exist locally"); + + equal(collection.count(), 1, "Only tombstone should exist on server"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_nonexistent_tombstone() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = profileStorage.addresses._generateGUID(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + getDateForSync() + ); + + await engine.setLastSync(0); + await engine.sync(); + + ok( + !(await profileStorage.addresses.get(guid)), + "Should not return record for uknown deleted item" + ); + let localTombstone = ( + await profileStorage.addresses.getAll({ + includeDeleted: true, + }) + ).find(record => record.guid == guid); + ok(localTombstone, "Should store tombstone for unknown item"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_incoming_deleted() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Delete the record remotely. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + await engine.sync(); + + ok( + !(await profileStorage.addresses.get(guid)), + "Should delete unmodified item locally" + ); + + let localTombstone = ( + await profileStorage.addresses.getAll({ + includeDeleted: true, + }) + ).find(record => record.guid == guid); + ok(localTombstone, "Should keep local tombstone for remotely deleted item"); + strictEqual( + getSyncChangeCounter(profileStorage.addresses, guid), + 0, + "Local tombstone should be marked as syncing" + ); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_incoming_restored() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + // Removing a synced record should write a tombstone. + profileStorage.addresses.remove(guid); + + // Modify the deleted record remotely. + let collection = server.user("foo").collection("addresses"); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + serverPayload.entry["street-address"] = "I moved!"; + let lastSync = await engine.getLastSync(); + collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); + + // Sync again. + await engine.sync(); + + // We should replace our tombstone with the server's version. + let localRecord = await profileStorage.addresses.get(guid); + ok( + objectMatches(localRecord, { + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + }) + ); + + let maybeNewServerPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + deepEqual( + maybeNewServerPayload, + serverPayload, + "Should not change record on server" + ); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_applyIncoming_outgoing_restored() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + // Modify the local record. + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(guid, localCopy); + + // Replace the record with a tombstone on the server. + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + collection.insert( + guid, + encryptPayload({ + id: guid, + deleted: true, + }), + lastSync + 10 + ); + + // Sync again. + await engine.sync(); + + // We should resurrect the record on the server. + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + ok(!serverPayload.deleted, "Should resurrect record on server"); + ok( + objectMatches(serverPayload.entry, { + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + // resurrection also beings back any unknown fields we had + "unknown-1": "some unknown data from another client", + }) + ); + + let localRecord = await profileStorage.addresses.get(guid); + ok(localRecord, "Modified record should not be deleted locally"); + } finally { + await cleanup(server); + } +}); + +// Unlike most sync engines, we want "both modified" to inspect the records, +// and if materially different, create a duplicate. +add_task(async function test_reconcile_both_modified_identical() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // and an identical record on the server. + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid, + encryptPayload({ + id: guid, + entry: TEST_PROFILE_1, + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [{ guid }]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_incoming_dupes() { + let { profileStorage, server, engine } = await setup(); + try { + // Create a profile locally, then sync to upload the new profile to the + // server. + let guid1 = await profileStorage.addresses.add(TEST_PROFILE_1); + + await engine.setLastSync(0); + await engine.sync(); + + // Create another profile locally, but don't sync it yet. + await profileStorage.addresses.add(TEST_PROFILE_2); + + // Now create two records on the server with the same contents as our local + // profiles, but different GUIDs. + let lastSync = await engine.getLastSync(); + let guid1_dupe = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid1_dupe, + encryptPayload({ + id: guid1_dupe, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + lastSync + 10 + ) + ); + let guid2_dupe = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + guid2_dupe, + encryptPayload({ + id: guid2_dupe, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_2 + ), + }), + lastSync + 10 + ) + ); + + // Sync again. We should download `guid1_dupe` and `guid2_dupe`, then + // reconcile changes. + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + // We uploaded `guid1` during the first sync. Even though its contents + // are the same as `guid1_dupe`, we keep both. + Object.assign({}, TEST_PROFILE_1, { guid: guid1 }), + Object.assign({}, TEST_PROFILE_1, { guid: guid1_dupe }), + // However, we didn't upload `guid2` before downloading `guid2_dupe`, so + // we *should* dedupe `guid2` to `guid2_dupe`. + Object.assign({}, TEST_PROFILE_2, { guid: guid2_dupe }), + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_identical_unsynced() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // and an identical record on the server but different GUID. + let remoteGuid = Utils.makeGUID(); + notEqual(localGuid, remoteGuid); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + remoteGuid, + encryptPayload({ + id: remoteGuid, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + // Should have 1 item locally with GUID changed to the remote one. + // There's no tombstone as the original was unsynced. + await expectLocalProfiles(profileStorage, [ + { + guid: remoteGuid, + }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_identical_synced() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let localGuid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // sync it - it will no longer be a candidate for de-duping. + await engine.setLastSync(0); + await engine.sync(); + + // and an identical record on the server but different GUID. + let lastSync = await engine.getLastSync(); + let remoteGuid = Utils.makeGUID(); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + remoteGuid, + encryptPayload({ + id: remoteGuid, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + lastSync + 10 + ) + ); + + await engine.sync(); + + // Should have 2 items locally, since the first was synced. + await expectLocalProfiles(profileStorage, [ + { guid: localGuid }, + { guid: remoteGuid }, + ]); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_dedupe_multiple_candidates() { + let { profileStorage, server, engine } = await setup(); + try { + // It's possible to have duplicate local profiles, with the same fields but + // different GUIDs. After a node reassignment, or after disconnecting and + // reconnecting to Sync, we might dedupe a local record A to a remote record + // B, if we see B before we download and apply A. Since A and B are dupes, + // that's OK. We'll write a tombstone for A when we dedupe A to B, and + // overwrite that tombstone when we see A. + + let localRecord = { + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }; + let serverRecord = Object.assign( + { + version: 1, + }, + localRecord + ); + + // We don't pass `sourceSync` so that the records are marked as NEW. + let aGuid = await profileStorage.addresses.add(localRecord); + let bGuid = await profileStorage.addresses.add(localRecord); + + // Insert B before A. + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + bGuid, + encryptPayload({ + id: bGuid, + entry: serverRecord, + }), + getDateForSync() + ) + ); + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + aGuid, + encryptPayload({ + id: aGuid, + entry: serverRecord, + }), + getDateForSync() + ) + ); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: aGuid, + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }, + { + guid: bGuid, + "given-name": "Mark", + "family-name": "Hammond", + organization: "Mozilla", + country: "AU", + tel: "+12345678910", + }, + ]); + // Make sure these are both syncing. + strictEqual( + getSyncChangeCounter(profileStorage.addresses, aGuid), + 0, + "A should be marked as syncing" + ); + strictEqual( + getSyncChangeCounter(profileStorage.addresses, bGuid), + 0, + "B should be marked as syncing" + ); + } finally { + await cleanup(server); + } +}); + +// Unlike most sync engines, we want "both modified" to inspect the records, +// and if materially different, create a duplicate. +add_task(async function test_reconcile_both_modified_conflict() { + let { profileStorage, server, engine } = await setup(); + try { + // create a record locally. + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + // Upload the record to the server. + await engine.setLastSync(0); + await engine.sync(); + + strictEqual( + getSyncChangeCounter(profileStorage.addresses, guid), + 0, + "Original record should be marked as syncing" + ); + + // Change the same field locally and on the server. + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(guid, localCopy); + + let lastSync = await engine.getLastSync(); + let collection = server.user("foo").collection("addresses"); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(guid)).ciphertext + ); + serverPayload.entry["street-address"] = "I moved, too!"; + collection.insert(guid, encryptPayload(serverPayload), lastSync + 10); + + // Sync again. + await engine.sync(); + + // Since we wait to pull changes until we're ready to upload, both records + // should now exist on the server; we don't need a follow-up sync. + let serverPayloads = collection.payloads(); + equal(serverPayloads.length, 2, "Both records should exist on server"); + + let forkedPayload = serverPayloads.find(payload => payload.id != guid); + ok(forkedPayload, "Forked record should exist on server"); + + await expectLocalProfiles(profileStorage, [ + { + guid, + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved, too!", + }, + { + guid: forkedPayload.id, + "given-name": "Timothy", + "family-name": "Berners-Lee", + "street-address": "I moved!", + }, + ]); + + let changeCounter = getSyncChangeCounter( + profileStorage.addresses, + forkedPayload.id + ); + strictEqual(changeCounter, 0, "Forked record should be marked as syncing"); + } finally { + await cleanup(server); + } +}); + +add_task(async function test_wipe() { + let { profileStorage, server, engine } = await setup(); + try { + let guid = await profileStorage.addresses.add(TEST_PROFILE_1); + + await expectLocalProfiles(profileStorage, [{ guid }]); + + let promiseObserved = promiseOneObserver("formautofill-storage-changed"); + + await engine._wipeClient(); + + let { subject, data } = await promiseObserved; + Assert.equal( + subject.wrappedJSObject.sourceSync, + true, + "it should be noted this came from sync" + ); + Assert.equal( + subject.wrappedJSObject.collectionName, + "addresses", + "got the correct collection" + ); + Assert.equal(data, "removeAll", "a removeAll should be noted"); + + await expectLocalProfiles(profileStorage, []); + } finally { + await cleanup(server); + } +}); + +// Other clients might have data that we aren't able to process/understand yet +// We should keep that data and ensure when we sync we don't lose that data +add_task(async function test_full_roundtrip_unknown_data() { + let { profileStorage, server, engine } = await setup(); + try { + let profileID = Utils.makeGUID(); + + info("Incoming records with unknown fields are properly stored"); + // Insert a record onto the server + server.insertWBO( + "foo", + "addresses", + new ServerWBO( + profileID, + encryptPayload({ + id: profileID, + entry: Object.assign( + { + version: 1, + }, + TEST_PROFILE_1 + ), + }), + getDateForSync() + ) + ); + + // The tracker should start with no score. + equal(engine._tracker.score, 0); + + await engine.setLastSync(0); + await engine.sync(); + + await expectLocalProfiles(profileStorage, [ + { + guid: profileID, + }, + ]); + + strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0); + + // The sync applied new records - ensure our tracker knew it came from + // sync and didn't bump the score. + equal(engine._tracker.score, 0); + + // Validate incoming records with unknown fields are correctly stored + let localRecord = await profileStorage.addresses.get(profileID); + equal(localRecord["unknown-1"], TEST_PROFILE_1["unknown-1"]); + + let onChanged = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => data == "update" + ); + + // Validate we can update the records locally and not drop any unknown fields + info("Unknown fields are sent back up to the server"); + + // Modify the local copy + let localCopy = Object.assign({}, TEST_PROFILE_1); + localCopy["street-address"] = "I moved!"; + await profileStorage.addresses.update(profileID, localCopy); + await onChanged; + await profileStorage._saveImmediately(); + + let updatedCopy = await profileStorage.addresses.get(profileID); + equal(updatedCopy["street-address"], "I moved!"); + + // Sync our changes to the server + await engine.setLastSync(0); + await engine.sync(); + + let collection = server.user("foo").collection("addresses"); + + Assert.ok(collection.wbo(profileID)); + let serverPayload = JSON.parse( + JSON.parse(collection.payload(profileID)).ciphertext + ); + + // The server has the updated field as well as any unknown fields + equal( + serverPayload.entry["unknown-1"], + "some unknown data from another client" + ); + equal(serverPayload.entry["street-address"], "I moved!"); + } finally { + await cleanup(server); + } +}); -- cgit v1.2.3