Bug 1351678: Handle resolved conflicts correctly, r?kmag draft
authorEthan Glasser-Camp <eglassercamp@mozilla.com>
Thu, 20 Apr 2017 13:44:46 -0400
changeset 566837 2508975134c0613f9d8c3f2d989fc34caf927d37
parent 565030 c0ea5ed7f91a6be996a4a3c5ab25e2cdf6b4377e
child 570940 a0fd38b52a5e5ce646cc6d94be6a3d99727ad741
push id55358
push usereglassercamp@mozilla.com
push dateMon, 24 Apr 2017 02:22:45 +0000
reviewerskmag
bugs1351678
milestone55.0a1
Bug 1351678: Handle resolved conflicts correctly, r?kmag The elements of the SyncResultObject#conflicts field are {local, remote} pairs. However, the elements of the SyncResultObject#resolved field are just the resolutions, and trying to access "local" and "remote" fields of those resolutions causes a crash. Add a test that demonstrates the error, and then fix it. MozReview-Commit-ID: LF0NBw5VKBC
toolkit/components/extensions/ExtensionStorageSync.jsm
toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -803,23 +803,28 @@ class ExtensionStorageSync {
       };
     }
     for (const record of syncResults.deleted) {
       changes[record.key] = {
         oldValue: record.data,
       };
     }
     for (const conflict of syncResults.resolved) {
-      // FIXME: Should we even send a notification? If so, what
-      // best values for "old" and "new"? This might violate
-      // client code's assumptions, since from their perspective,
-      // we were in state L, but this diff is from R -> L.
-      changes[conflict.remote.key] = {
-        oldValue: conflict.local.data,
-        newValue: conflict.remote.data,
+      // FIXME: We can't send a "changed" notification because
+      // kinto.js only provides the newly-resolved value. But should
+      // we even send a notification? We use CLIENT_WINS so nothing
+      // has really "changed" on this end. (The change will come on
+      // the other end when it pulls down the update, which is handled
+      // by the "updated" case above.) If we are going to send a
+      // notification, what best values for "old" and "new"?  This
+      // might violate client code's assumptions, since from their
+      // perspective, we were in state L, but this diff is from R ->
+      // L.
+      changes[conflict.key] = {
+        newValue: conflict.data,
       };
     }
     if (Object.keys(changes).length > 0) {
       this.notifyListeners(extension, changes);
     }
   }
 
   /**
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -1109,16 +1109,85 @@ add_task(function* test_storage_sync_pus
       ok(!updateEncrypted.data,
          "pushing an updated value should not have any plaintext visible");
       equal(updateEncrypted.id, hashedId,
             "pushing an updated value should maintain the same ID");
     });
   });
 });
 
+add_task(function* test_storage_sync_pulls_conflicts() {
+  const extensionId = uuid();
+  const extension = {id: extensionId};
+  yield* withContextAndServer(function* (context, server) {
+    yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+      const cryptoCollection = new CryptoCollection(fxaService);
+      let transformer = new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extensionId);
+      server.installCollection("storage-sync-crypto");
+
+      yield extensionStorageSync.ensureCanSync([extensionId]);
+      const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
+      yield server.encryptAndAddRecord(transformer, {
+        collectionId,
+        data: {
+          "id": "key-remote_2D_key",
+          "key": "remote-key",
+          "data": 6,
+        },
+        predicate: appearsAt(850),
+      });
+      server.etag = 900;
+
+      yield extensionStorageSync.set(extension, {"remote-key": 8}, context);
+
+      let calls = [];
+      yield extensionStorageSync.addOnChangedListener(extension, function() {
+        calls.push(arguments);
+      }, context);
+
+      yield extensionStorageSync.syncAll();
+      const remoteValue = (yield extensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+      equal(remoteValue, 8,
+            "locally set value overrides remote value");
+
+      equal(calls.length, 1,
+            "conflicts manifest in on-changed listener");
+      deepEqual(calls[0][0], {"remote-key": {newValue: 8}});
+      calls = [];
+
+      // Syncing again doesn't do anything
+      yield extensionStorageSync.syncAll();
+
+      equal(calls.length, 0,
+            "syncing again shouldn't call on-changed listener");
+
+      // Updating the server causes us to pull down the new value
+      server.etag = 1000;
+      yield server.encryptAndAddRecord(transformer, {
+        collectionId,
+        data: {
+          "id": "key-remote_2D_key",
+          "key": "remote-key",
+          "data": 7,
+        },
+        predicate: appearsAt(950),
+      });
+
+      yield extensionStorageSync.syncAll();
+      const remoteValue2 = (yield extensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+      equal(remoteValue2, 7,
+            "conflicts do not prevent retrieval of new values");
+
+      equal(calls.length, 1,
+            "syncing calls on-changed listener on update");
+      deepEqual(calls[0][0], {"remote-key": {oldValue: 8, newValue: 7}});
+    });
+  });
+});
+
 add_task(function* test_storage_sync_pulls_deletes() {
   const extension = defaultExtension;
   yield* withContextAndServer(function* (context, server) {
     yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
       const cryptoCollection = new CryptoCollection(fxaService);
       const collectionId = yield cryptoCollection.extensionIdToCollectionId(defaultExtensionId);
       server.installCollection(collectionId);
       server.installCollection("storage-sync-crypto");