Bug 1441666 - Add helper functions for inserting encrypted records to the test server r=kitcambridge
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 21 Feb 2018 16:35:45 -0500
changeset 461353 7bc4e7d466a3b7fd258b89b1fcf9f41ebf9ff8f2
parent 461352 affbbe1c5829beb2f6448a32a253e46224102adb
child 461354 cca108d5a695dd00aa0e527c6764ec041f7d69e1
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskitcambridge
bugs1441666
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1441666 - Add helper functions for inserting encrypted records to the test server r=kitcambridge This also converts some of our tests to use them. In particular, 1. All of the new functions should have coverage. 2. None of the existing tests should assume we can decrypt records by JSON.parsing them. MozReview-Commit-ID: y7Hw3q9Wko
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_bookmark_duping.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_bookmark_smart_bookmarks.js
services/sync/tests/unit/test_clients_engine.js
services/sync/tests/unit/test_history_engine.js
services/sync/tests/unit/test_password_engine.js
services/sync/tests/unit/test_syncengine_sync.js
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -3,16 +3,18 @@
 /* import-globals-from head_helpers.js */
 
 var Cm = Components.manager;
 
 // Shared logging for all HTTP server functions.
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 ChromeUtils.import("resource://services-common/utils.js");
 ChromeUtils.import("resource://testing-common/TestUtils.jsm");
+ChromeUtils.import("resource://testing-common/services/sync/utils.js");
+
 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
 
 // While the sync code itself uses 1.5, the tests hard-code 1.1,
 // so we're sticking with 1.1 here.
 const SYNC_API_VERSION = "1.1";
 
 // Use the same method that record.js does, which mirrors the server.
 // The server returns timestamps with 1/100 sec granularity. Note that this is
@@ -140,17 +142,37 @@ ServerWBO.prototype = {
           response.setHeader("Content-Type", "application/json");
           response.newModified = ts;
           break;
       }
       response.setHeader("X-Weave-Timestamp", "" + new_timestamp(), false);
       response.setStatusLine(request.httpVersion, statusCode, status);
       response.bodyOutputStream.write(body, body.length);
     };
-  }
+  },
+
+  /**
+   * Get the cleartext data stored in the payload.
+   *
+   * This isn't `get cleartext`, because `x.cleartext.blah = 3;` wouldn't work,
+   * which seems like a footgun.
+   */
+  getCleartext() {
+    return JSON.parse(JSON.parse(this.payload).ciphertext);
+  },
+
+  /**
+   * Setter for getCleartext(), but lets you adjust the modified timestamp too.
+   * Returns this ServerWBO object.
+   */
+  setCleartext(cleartext, modifiedTimestamp = this.modified) {
+    this.payload = JSON.stringify(encryptPayload(cleartext));
+    this.modified = modifiedTimestamp;
+    return this;
+  },
 
 };
 
 
 /**
  * Represent a collection on the server. The '_wbos' attribute is a
  * mapping of id -> ServerWBO objects.
  *
@@ -229,41 +251,77 @@ ServerCollection.prototype = {
   },
 
   /**
    * Convenience method to get an array of parsed ciphertexts.
    *
    * @return an array of the payloads of each stored WBO.
    */
   payloads() {
-    return this.wbos().map(function(wbo) {
-      return JSON.parse(JSON.parse(wbo.payload).ciphertext);
-    });
+    return this.wbos().map(wbo => wbo.getCleartext());
   },
 
   // Just for syntactic elegance.
   wbo: function wbo(id) {
     return this._wbos[id];
   },
 
   payload: function payload(id) {
     return this.wbo(id).payload;
   },
 
+  cleartext(id) {
+    return this.wbo(id).getCleartext();
+  },
+
   /**
    * Insert the provided WBO under its ID.
    *
    * @return the provided WBO.
    */
   insertWBO: function insertWBO(wbo) {
     this.timestamp = Math.max(this.timestamp, wbo.modified);
     return this._wbos[wbo.id] = wbo;
   },
 
   /**
+   * Update an existing WBO's cleartext using a callback function that modifies
+   * the record in place, or returns a new record.
+   */
+  updateRecord(id, updateCallback, optTimestamp) {
+    let wbo = this.wbo(id);
+    if (!wbo) {
+      throw new Error("No record with provided ID");
+    }
+    let curCleartext = wbo.getCleartext();
+    // Allow update callback to either return a new cleartext, or modify in place.
+    let newCleartext = updateCallback(curCleartext) || curCleartext;
+    wbo.setCleartext(newCleartext, optTimestamp);
+    // It is already inserted, but we might need to update our timestamp based
+    // on it's `modified` value, if `optTimestamp` was provided.
+    return this.insertWBO(wbo);
+  },
+
+  /**
+   * Insert a record, which may either an object with a cleartext property, or
+   * the cleartext property itself.
+   */
+  insertRecord(record, timestamp = Date.now() / 1000) {
+    if (typeof timestamp != "number") {
+      throw new TypeError("insertRecord: Timestamp is not a number.");
+    }
+    if (!record.id) {
+      throw new Error("Attempt to insert record with no id");
+    }
+    // Allow providing either the cleartext directly, or the CryptoWrapper-like.
+    let cleartext = record.cleartext || record;
+    return this.insert(record.id, encryptPayload(cleartext), timestamp);
+  },
+
+  /**
    * Insert the provided payload as part of a new ServerWBO with the provided
    * ID.
    *
    * @param id
    *        The GUID for the WBO.
    * @param payload
    *        The payload, as provided to the ServerWBO constructor.
    * @param modified
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -61,19 +61,17 @@ async function createFolder(parentId, ti
 async function createBookmark(parentId, url, title, index = bms.DEFAULT_INDEX) {
   let parentGuid = await PlacesUtils.promiseItemGuid(parentId);
   let bookmark = await bms.insert({ parentGuid, url, index, title });
   let id = await PlacesUtils.promiseItemId(bookmark.guid);
   return { id, guid: bookmark.guid };
 }
 
 function getServerRecord(collection, id) {
-  let wbo = collection.get({ full: true, ids: [id] });
-  // Whew - lots of json strings inside strings.
-  return JSON.parse(JSON.parse(JSON.parse(JSON.parse(wbo)[0]).payload).ciphertext);
+  return collection.cleartext(id);
 }
 
 async function promiseNoLocalItem(guid) {
   // Check there's no item with the specified guid.
   let got = await bms.fetch({ guid });
   ok(!got, `No record remains with GUID ${guid}`);
   // and while we are here ensure the places cache doesn't still have it.
   await Assert.rejects(PlacesUtils.promiseItemId(guid));
@@ -161,23 +159,23 @@ add_task(async function test_dupe_bookma
     PlacesUtils.bookmarks.addObserver(obs, false);
 
     _("Syncing so new dupe record is processed");
     engine.lastSync = engine.lastSync - 5;
     await engine.sync();
 
     // We should have logically deleted the dupe record.
     equal(collection.count(), 7);
-    ok(getServerRecord(collection, bmk1_guid).deleted);
+    ok(collection.cleartext(bmk1_guid).deleted);
     // and physically removed from the local store.
     await promiseNoLocalItem(bmk1_guid);
     // Parent should still only have 1 item.
     equal((await getFolderChildrenIDs(folder1_id)).length, 1);
     // The parent record on the server should now reference the new GUID and not the old.
-    let serverRecord = getServerRecord(collection, folder1_guid);
+    let serverRecord = collection.cleartext(folder1_guid);
     ok(!serverRecord.children.includes(bmk1_guid));
     ok(serverRecord.children.includes(newGUID));
 
     ok(onItemChangedObserved);
 
     // and a final sanity check - use the validator
     await validate(collection);
     PlacesUtils.bookmarks.removeObserver(obs);
@@ -222,31 +220,31 @@ add_task(async function test_dupe_repare
     collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 500);
 
     _("Syncing so new dupe record is processed");
     engine.lastSync = engine.lastSync - 5;
     await engine.sync();
 
     // We should have logically deleted the dupe record.
     equal(collection.count(), 8);
-    ok(getServerRecord(collection, bmk1_guid).deleted);
+    ok(collection.cleartext(bmk1_guid).deleted);
     // and physically removed from the local store.
     await promiseNoLocalItem(bmk1_guid);
     // The original folder no longer has the item
     equal((await getFolderChildrenIDs(folder1_id)).length, 0);
     // But the second dupe folder does.
     equal((await getFolderChildrenIDs(folder2_id)).length, 1);
 
     // The record for folder1 on the server should reference neither old or new GUIDs.
-    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    let serverRecord1 = collection.cleartext(folder1_guid);
     ok(!serverRecord1.children.includes(bmk1_guid));
     ok(!serverRecord1.children.includes(newGUID));
 
     // The record for folder2 on the server should only reference the new new GUID.
-    let serverRecord2 = getServerRecord(collection, folder2_guid);
+    let serverRecord2 = collection.cleartext(folder2_guid);
     ok(!serverRecord2.children.includes(bmk1_guid));
     ok(serverRecord2.children.includes(newGUID));
 
     // and a final sanity check - use the validator
     await validate(collection);
   } finally {
     await cleanup(engine, server);
   }
@@ -303,31 +301,31 @@ add_task(async function test_dupe_repare
     // We need to take care to only sync the one new record - if we also see
     // our local item as incoming the test fails - bug 1368608.
     engine.lastSync = newWBO.modified - 0.000001;
     engine.lastModified = null;
     await engine.sync();
 
     // We should have logically deleted the dupe record.
     equal(collection.count(), 8);
-    ok(getServerRecord(collection, bmk1_guid).deleted);
+    ok(collection.cleartext(bmk1_guid).deleted);
     // and physically removed from the local store.
     await promiseNoLocalItem(bmk1_guid);
     // The original folder still has the item
     equal((await getFolderChildrenIDs(folder1_id)).length, 1);
     // The second folder does not.
     equal((await getFolderChildrenIDs(folder2_id)).length, 0);
 
     // The record for folder1 on the server should reference only the GUID.
-    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    let serverRecord1 = collection.cleartext(folder1_guid);
     ok(!serverRecord1.children.includes(bmk1_guid));
     ok(serverRecord1.children.includes(newGUID));
 
     // The record for folder2 on the server should reference nothing.
-    let serverRecord2 = getServerRecord(collection, folder2_guid);
+    let serverRecord2 = collection.cleartext(folder2_guid);
     ok(!serverRecord2.children.includes(bmk1_guid));
     ok(!serverRecord2.children.includes(newGUID));
 
     // and a final sanity check - use the validator
     await validate(collection);
   } finally {
     await cleanup(engine, server);
   }
@@ -526,24 +524,24 @@ add_task(async function test_dupe_repare
     }), Date.now() / 1000 + 500);
 
     _("Syncing so new dupe record is processed");
     engine.lastSync = engine.lastSync - 5;
     await engine.sync();
 
     // We should have logically deleted the dupe record.
     equal(collection.count(), 8);
-    ok(getServerRecord(collection, bmk1_guid).deleted);
+    ok(collection.cleartext(bmk1_guid).deleted);
     // and physically removed from the local store.
     await promiseNoLocalItem(bmk1_guid);
     // The intended parent doesn't exist, so it remains in the original folder
     equal((await getFolderChildrenIDs(folder1_id)).length, 1);
 
     // The record for folder1 on the server should reference the new GUID.
-    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    let serverRecord1 = collection.cleartext(folder1_guid);
     ok(!serverRecord1.children.includes(bmk1_guid));
     ok(serverRecord1.children.includes(newGUID));
 
     // As the incoming parent is missing the item should have been annotated
     // with that missing parent.
     equal(PlacesUtils.annotations.getItemAnnotation((await store.idForGUID(newGUID)),
       PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO), newParentGUID);
 
@@ -644,15 +642,15 @@ add_task(async function test_dupe_empty_
     engine.lastSync = engine.lastSync - 5;
     await engine.sync();
 
     await validate(collection);
 
     // Collection now has one additional record - the logically deleted dupe.
     equal(collection.count(), 6);
     // original folder should be logically deleted.
-    ok(getServerRecord(collection, folder1_guid).deleted);
+    ok(collection.cleartext(folder1_guid).deleted);
     await promiseNoLocalItem(folder1_guid);
   } finally {
     await cleanup(engine, server);
   }
 });
 // XXX - TODO - folders with children. Bug 1293163
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -792,17 +792,17 @@ add_bookmark_test(async function test_sy
     // Next sync of the engine doesn't hit info/collections, so lastModified
     // remains stale. Setting it to null side-steps that.
     engine.lastModified = null;
     await sync_engine_and_validate_telem(engine, false);
 
     let newRecord2 = await store.createRecord(item2GUID);
     equal(newRecord2.dateAdded, item2.dateAdded, "dateAdded update should work for earlier date");
 
-    let bzWBO = JSON.parse(JSON.parse(collection._wbos[bz.guid].payload).ciphertext);
+    let bzWBO = collection.cleartext(bz.guid);
     ok(bzWBO.dateAdded, "Locally added dateAdded lost");
 
     let localRecord = await store.createRecord(bz.guid);
     equal(bzWBO.dateAdded, localRecord.dateAdded, "dateAdded should not change during upload");
 
     item2.dateAdded += 10000;
     collection.insert(item2GUID, encryptPayload(item2.cleartext), now / 1000 - 10);
 
@@ -862,18 +862,17 @@ add_task(async function test_sync_imap_U
     await sync_engine_and_validate_telem(engine, false);
 
     let aInfo = await PlacesUtils.bookmarks.fetch("bookmarkAAAA");
     equal(aInfo.url.href, "imap://vs@eleven.vs.solnicky.cz:993/" +
       "fetch%3EUID%3E/INBOX%3E56291?part=1.2&type=image/jpeg&filename=" +
       "invalidazPrahy.jpg",
       "Remote bookmark A with IMAP URL should exist locally");
 
-    let bPayload = JSON.parse(JSON.parse(
-      collection.payload("bookmarkBBBB")).ciphertext);
+    let bPayload = collection.cleartext("bookmarkBBBB");
     equal(bPayload.bmkUri, "imap://eleven.vs.solnicky.cz:993/" +
       "fetch%3EUID%3E/CURRENT%3E2433?part=1.2&type=text/html&filename=" +
       "TomEdwards.html",
       "Local bookmark B with IMAP URL should exist remotely");
   } finally {
     await cleanup(engine, server);
   }
 });
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -102,17 +102,17 @@ add_task(async function test_annotation_
                });
     Assert.equal(wbos.length, 1);
 
     _("Verify that the server WBO has the annotation.");
     let serverGUID = wbos[0];
     Assert.equal(serverGUID, guid);
     let serverWBO = collection.wbo(serverGUID);
     Assert.ok(!!serverWBO);
-    let body = JSON.parse(JSON.parse(serverWBO.payload).ciphertext);
+    let body = serverWBO.getCleartext();
     Assert.equal(body.queryId, "MostVisited");
 
     _("We still have the right count.");
     Assert.equal(smartBookmarkCount(), startCount + 1);
 
     _("Clear local records; now we can't find it.");
 
     // "Clear" by changing attributes: if we delete it, apparently it sticks
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -13,17 +13,17 @@ const LESS_THAN_CLIENTS_TTL_REFRESH = 86
 
 let engine;
 
 /**
  * Unpack the record with this ID, and verify that it has the same version that
  * we should be putting into records.
  */
 async function check_record_version(user, id) {
-    let payload = JSON.parse(user.collection("clients").wbo(id).payload);
+    let payload = user.collection("clients").wbo(id).data;
 
     let rec = new CryptoWrapper();
     rec.id = id;
     rec.collection = "clients";
     rec.ciphertext = payload.ciphertext;
     rec.hmac = payload.hmac;
     rec.IV = payload.IV;
 
@@ -219,34 +219,34 @@ add_task(async function test_full_sync()
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
   let activeID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: activeID,
     name: "Active client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   let deletedID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: deletedID,
     name: "Client to delete",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     let store = engine._store;
 
     _("First sync. 2 records downloaded; our record uploaded.");
     strictEqual(engine.lastRecordUpload, 0);
     await syncClientsEngine(server);
     ok(engine.lastRecordUpload > 0);
@@ -368,24 +368,24 @@ add_task(async function test_last_modifi
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
   let activeID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: activeID,
     name: "Active client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     let collection = user.collection("clients");
 
     _("Sync to download the record");
     await syncClientsEngine(server);
 
     equal(engine._store._remoteClients[activeID].serverLastModified, now - 10,
@@ -398,17 +398,17 @@ add_task(async function test_last_modifi
     // The sync above also did a POST, so adjust our lastModified.
     engine.lastModified = server.getCollection("foo", "clients").timestamp;
     await engine._uploadOutgoing();
 
     _("Local record should have updated timestamp");
     ok(engine._store._remoteClients[activeID].serverLastModified >= now);
 
     _("Record on the server should have new name but not serverLastModified");
-    let payload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext);
+    let payload = collection.cleartext(activeID);
     equal(payload.name, "New name");
     equal(payload.serverLastModified, undefined);
 
   } finally {
     await cleanup();
     server.deleteCollections("foo");
     await promiseStopServer(server);
   }
@@ -604,46 +604,46 @@ add_task(async function test_filter_dupl
   let server = await serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
   // Synced recently.
   let recentID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(recentID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: recentID,
     name: "My Phone",
     type: "mobile",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   // Dupe of our client, synced more than 1 week ago.
   let dupeID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: dupeID,
     name: engine.localName,
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 604810));
+  }, now - 604820);
 
   // Synced more than 1 week ago, but not a dupe.
   let oldID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(oldID, encryptPayload({
+  user.collection("clients").insertRecord({
     id: oldID,
     name: "My old desktop",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 604820));
+  }, now - 604820);
 
   try {
     let store = engine._store;
 
     _("First sync");
     strictEqual(engine.lastRecordUpload, 0);
     await syncClientsEngine(server);
     ok(engine.lastRecordUpload > 0);
@@ -687,37 +687,37 @@ add_task(async function test_filter_dupl
     equal(counts.failed, 0);
     equal(counts.newFailed, 0);
 
     _("Broadcast logout to all clients");
     await engine.sendCommand("logout", []);
     await syncClientsEngine(server);
 
     let collection = server.getCollection("foo", "clients");
-    let recentPayload = JSON.parse(JSON.parse(collection.payload(recentID)).ciphertext);
+    let recentPayload = collection.cleartext(recentID);
     compareCommands(recentPayload.commands, [{ command: "logout", args: [] }],
                     "Should send commands to the recent client");
 
-    let oldPayload = JSON.parse(JSON.parse(collection.payload(oldID)).ciphertext);
+    let oldPayload = collection.cleartext(oldID);
     compareCommands(oldPayload.commands, [{ command: "logout", args: [] }],
                     "Should send commands to the week-old client");
 
-    let dupePayload = JSON.parse(JSON.parse(collection.payload(dupeID)).ciphertext);
+    let dupePayload = collection.cleartext(dupeID);
     deepEqual(dupePayload.commands, [],
               "Should not send commands to the dupe client");
 
     _("Update the dupe client's modified time");
-    server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
+    collection.insertRecord({
       id: dupeID,
       name: engine.localName,
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"],
-    }), now - 10));
+    }, now - 10);
 
     _("Second sync.");
     await syncClientsEngine(server);
 
     ids = await store.getAllIDs();
     deepEqual(Object.keys(ids).sort(),
               [recentID, oldID, dupeID, engine.localID].sort(),
               "Stale client synced, so it should no longer be marked as a dupe");
@@ -760,24 +760,24 @@ add_task(async function test_command_syn
   let user     = server.user("foo");
   let remoteId = Utils.makeGUID();
 
   function clientWBO(id) {
     return user.collection("clients").wbo(id);
   }
 
   _("Create remote client record");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+  user.collection("clients").insertRecord({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), Date.now() / 1000));
+  });
 
   try {
     _("Syncing.");
     await syncClientsEngine(server);
 
     _("Checking remote record was downloaded.");
     let clientRecord = engine._store._remoteClients[remoteId];
     notEqual(clientRecord, undefined);
@@ -825,36 +825,38 @@ add_task(async function test_clients_not
   await engine._store.wipe();
   await generateNewKeys(Service.collectionKeys);
 
   let server   = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let remoteId = Utils.makeGUID();
   let remoteId2 = Utils.makeGUID();
+  let collection = server.getCollection("foo", "clients");
 
   _("Create remote client records");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+  collection.insertRecord({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteId,
     protocols: ["1.5"],
-  }), Date.now() / 1000));
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
+  });
+
+  collection.insertRecord({
     id: remoteId2,
     name: "Remote client 2",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteId2,
     protocols: ["1.5"],
-  }), Date.now() / 1000));
+  });
 
   let fxAccounts = engine.fxAccounts;
   engine.fxAccounts = {
     notifyDevices() { return Promise.resolve(true); },
     getDeviceId() { return fxAccounts.getDeviceId(); },
     getDeviceList() { return Promise.resolve([{ id: remoteId }]); }
   };
 
@@ -865,17 +867,16 @@ add_task(async function test_clients_not
     ok(!engine._store._remoteClients[remoteId].stale);
     ok(engine._store._remoteClients[remoteId2].stale);
 
   } finally {
     engine.fxAccounts = fxAccounts;
     await cleanup();
 
     try {
-      let collection = server.getCollection("foo", "clients");
       collection.remove(remoteId);
     } finally {
       await promiseStopServer(server);
     }
   }
 });
 
 
@@ -887,35 +888,37 @@ add_task(async function test_dupe_device
 
   let server   = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let remoteId = Utils.makeGUID();
   let remoteId2 = Utils.makeGUID();
   let remoteDeviceId = Utils.makeGUID();
 
+  let collection = server.getCollection("foo", "clients");
+
   _("Create remote client records");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+  collection.insertRecord({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteDeviceId,
     protocols: ["1.5"],
-  }), Date.now() / 1000 - 30000));
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
+  }, Date.now() / 1000 - 30000);
+  collection.insertRecord({
     id: remoteId2,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteDeviceId,
     protocols: ["1.5"],
-  }), Date.now() / 1000));
+  });
 
   let fxAccounts = engine.fxAccounts;
   engine.fxAccounts = {
     notifyDevices() { return Promise.resolve(true); },
     getDeviceId() { return fxAccounts.getDeviceId(); },
     getDeviceList() { return Promise.resolve([{ id: remoteDeviceId }]); }
   };
 
@@ -926,17 +929,16 @@ add_task(async function test_dupe_device
     ok(engine._store._remoteClients[remoteId].stale);
     ok(!engine._store._remoteClients[remoteId2].stale);
 
   } finally {
     engine.fxAccounts = fxAccounts;
     await cleanup();
 
     try {
-      let collection = server.getCollection("foo", "clients");
       collection.remove(remoteId);
     } finally {
       await promiseStopServer(server);
     }
   }
 });
 
 
@@ -1059,68 +1061,68 @@ add_task(async function test_optional_cl
   await cleanup();
 });
 
 add_task(async function test_merge_commands() {
   _("Verifies local commands for remote clients are merged with the server's");
 
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
-
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
+  let collection = server.getCollection("foo", "clients");
+
   let desktopID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
+  collection.insertRecord({
     id: desktopID,
     name: "Desktop client",
     type: "desktop",
     commands: [{
       command: "displayURI",
       args: ["https://example.com", engine.localID, "Yak Herders Anonymous"],
       flowID: Utils.makeGUID(),
     }],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   let mobileID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(mobileID, encryptPayload({
+  collection.insertRecord({
     id: mobileID,
     name: "Mobile client",
     type: "mobile",
     commands: [{
       command: "logout",
       args: [],
       flowID: Utils.makeGUID(),
     }],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     _("First sync. 2 records downloaded.");
     strictEqual(engine.lastRecordUpload, 0);
     await syncClientsEngine(server);
 
     _("Broadcast logout to all clients");
     await engine.sendCommand("logout", []);
     await syncClientsEngine(server);
 
-    let collection = server.getCollection("foo", "clients");
-    let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
+    let desktopPayload = collection.cleartext(desktopID);
     compareCommands(desktopPayload.commands, [{
       command: "displayURI",
       args: ["https://example.com", engine.localID, "Yak Herders Anonymous"],
     }, {
       command: "logout",
       args: [],
     }], "Should send the logout command to the desktop client");
 
-    let mobilePayload = JSON.parse(JSON.parse(collection.payload(mobileID)).ciphertext);
+    let mobilePayload = collection.cleartext(mobileID);
     compareCommands(mobilePayload.commands, [{ command: "logout", args: [] }],
                     "Should not send a duplicate logout to the mobile client");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1133,51 +1135,52 @@ add_task(async function test_duplicate_r
   _("Verifies local commands for remote clients are sent only once (bug 1289287)");
 
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
+  let collection = server.getCollection("foo", "clients");
+
   let desktopID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
+  collection.insertRecord({
     id: desktopID,
     name: "Desktop client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     _("First sync. 1 record downloaded.");
     strictEqual(engine.lastRecordUpload, 0);
     await syncClientsEngine(server);
 
     _("Send tab to client");
     await engine.sendCommand("displayURI", ["https://example.com", engine.localID, "Yak Herders Anonymous"]);
     await syncClientsEngine(server);
 
     _("Simulate the desktop client consuming the command and syncing to the server");
-    server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
+    collection.insertRecord({
       id: desktopID,
       name: "Desktop client",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"],
-    }), now - 10));
+    }, now - 10);
 
     _("Send another tab to the desktop client");
     await engine.sendCommand("displayURI", ["https://foobar.com", engine.localID, "Foo bar!"], desktopID);
     await syncClientsEngine(server);
 
-    let collection = server.getCollection("foo", "clients");
-    let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
+    let desktopPayload = collection.cleartext(desktopID);
     compareCommands(desktopPayload.commands, [{
       command: "displayURI",
       args: ["https://foobar.com", engine.localID, "Foo bar!"],
     }], "Should only send the second command to the desktop client");
   } finally {
     await cleanup();
 
     try {
@@ -1192,75 +1195,76 @@ add_task(async function test_upload_afte
   _("Multiple downloads, reboot, then upload (bug 1289287)");
 
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
+  let collection = server.getCollection("foo", "clients");
+
   let deviceBID = Utils.makeGUID();
   let deviceCID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
+  collection.insertRecord({
     id: deviceBID,
     name: "Device B",
     type: "desktop",
     commands: [{
       command: "displayURI",
       args: ["https://deviceclink.com", deviceCID, "Device C link"],
       flowID: Utils.makeGUID(),
     }],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
-  server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({
+  }, now - 10);
+  collection.insertRecord({
     id: deviceCID,
     name: "Device C",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     _("First sync. 2 records downloaded.");
     strictEqual(engine.lastRecordUpload, 0);
     await syncClientsEngine(server);
 
     _("Send tab to client");
     await engine.sendCommand("displayURI", ["https://example.com", engine.localID, "Yak Herders Anonymous"], deviceBID);
 
     const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
     SyncEngine.prototype._uploadOutgoing = async () => engine._onRecordsWritten([], [deviceBID]);
     await syncClientsEngine(server);
 
-    let collection = server.getCollection("foo", "clients");
-    let deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+    let deviceBPayload = collection.cleartext(deviceBID);
     compareCommands(deviceBPayload.commands, [{
       command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"]
     }], "Should be the same because the upload failed");
 
     _("Simulate the client B consuming the command and syncing to the server");
-    server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
+    collection.insertRecord({
       id: deviceBID,
       name: "Device B",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"],
-    }), now - 10));
+    }, now - 10);
 
     // Simulate reboot
     SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
     engine = Service.clientsEngine = new ClientEngine(Service);
     await engine.initialize();
 
     await syncClientsEngine(server);
 
-    deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+    deviceBPayload = collection.cleartext(deviceBID);
     compareCommands(deviceBPayload.commands, [{
       command: "displayURI",
       args: ["https://example.com", engine.localID, "Yak Herders Anonymous"],
     }], "Should only had written our outgoing command");
   } finally {
     await cleanup();
 
     try {
@@ -1275,76 +1279,77 @@ add_task(async function test_keep_cleare
   _("Download commands, fail upload, reboot, then apply new commands (bug 1289287)");
 
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
+  let collection = server.getCollection("foo", "clients");
+
   let deviceBID = Utils.makeGUID();
   let deviceCID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({
+  collection.insertRecord({
     id: engine.localID,
     name: "Device A",
     type: "desktop",
     commands: [{
       command: "displayURI",
       args: ["https://deviceblink.com", deviceBID, "Device B link"],
       flowID: Utils.makeGUID(),
     },
     {
       command: "displayURI",
       args: ["https://deviceclink.com", deviceCID, "Device C link"],
       flowID: Utils.makeGUID(),
     }],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
-  server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
+  }, now - 10);
+  collection.insertRecord({
     id: deviceBID,
     name: "Device B",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
-  server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({
+  }, now - 10);
+  collection.insertRecord({
     id: deviceCID,
     name: "Device C",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     _("First sync. Download remote and our record.");
     strictEqual(engine.lastRecordUpload, 0);
 
-    let collection = server.getCollection("foo", "clients");
     const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
     SyncEngine.prototype._uploadOutgoing = async () => engine._onRecordsWritten([], [deviceBID]);
     let commandsProcessed = 0;
     engine._handleDisplayURIs = (uris) => { commandsProcessed = uris.length; };
 
     await syncClientsEngine(server);
     await engine.processIncomingCommands(); // Not called by the engine.sync(), gotta call it ourselves
     equal(commandsProcessed, 2, "We processed 2 commands");
 
-    let localRemoteRecord = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
+    let localRemoteRecord = collection.cleartext(engine.localID);
     compareCommands(localRemoteRecord.commands, [{
       command: "displayURI", args: ["https://deviceblink.com", deviceBID, "Device B link"]
     },
     {
       command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"]
     }], "Should be the same because the upload failed");
 
     // Another client sends another link
-    server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({
+    collection.insertRecord({
       id: engine.localID,
       name: "Device A",
       type: "desktop",
       commands: [{
         command: "displayURI",
         args: ["https://deviceblink.com", deviceBID, "Device B link"],
         flowID: Utils.makeGUID(),
       },
@@ -1355,30 +1360,30 @@ add_task(async function test_keep_cleare
       },
       {
         command: "displayURI",
         args: ["https://deviceclink2.com", deviceCID, "Device C link 2"],
         flowID: Utils.makeGUID(),
       }],
       version: "48",
       protocols: ["1.5"],
-    }), now - 5));
+    }, now - 5);
 
     // Simulate reboot
     SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
     engine = Service.clientsEngine = new ClientEngine(Service);
     await engine.initialize();
 
     commandsProcessed = 0;
     engine._handleDisplayURIs = (uris) => { commandsProcessed = uris.length; };
     await syncClientsEngine(server);
     await engine.processIncomingCommands();
     equal(commandsProcessed, 1, "We processed one command (the other were cleared)");
 
-    localRemoteRecord = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+    localRemoteRecord = collection.cleartext(deviceBID);
     deepEqual(localRemoteRecord.commands, [], "Should be empty");
   } finally {
     await cleanup();
 
     // Reset service (remove mocks)
     engine = Service.clientsEngine = new ClientEngine(Service);
     await engine.initialize();
     engine._resetClient();
@@ -1395,52 +1400,53 @@ add_task(async function test_deleted_com
   _("Verifies commands for a deleted client are discarded");
 
   let now = Date.now() / 1000;
   let server = await serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   await generateNewKeys(Service.collectionKeys);
 
+  let collection = server.getCollection("foo", "clients");
+
   let activeID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
+  collection.insertRecord({
     id: activeID,
     name: "Active client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   let deletedID = Utils.makeGUID();
-  server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
+  collection.insertRecord({
     id: deletedID,
     name: "Client to delete",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }), now - 10));
+  }, now - 10);
 
   try {
     _("First sync. 2 records downloaded.");
     await syncClientsEngine(server);
 
     _("Delete a record on the server.");
-    let collection = server.getCollection("foo", "clients");
     collection.remove(deletedID);
 
     _("Broadcast a command to all clients");
     await engine.sendCommand("logout", []);
     await syncClientsEngine(server);
 
     deepEqual(collection.keys().sort(), [activeID, engine.localID].sort(),
       "Should not reupload deleted clients");
 
-    let activePayload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext);
+    let activePayload = collection.cleartext(activeID);
     compareCommands(activePayload.commands, [{ command: "logout", args: [] }],
       "Should send the command to the active client");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1459,43 +1465,43 @@ add_task(async function test_send_uri_ac
   await generateNewKeys(Service.collectionKeys);
 
   try {
     let fakeSenderID = Utils.makeGUID();
 
     _("Initial sync for empty clients collection");
     await syncClientsEngine(server);
     let collection = server.getCollection("foo", "clients");
-    let ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
-    ok(ourPayload, "Should upload our client record");
 
-    _("Send a URL to the device on the server");
-    ourPayload.commands = [{
-      command: "displayURI",
-      args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
-      flowID: Utils.makeGUID(),
-    }];
-    server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload(ourPayload), now));
+    collection.updateRecord(engine.localID, payload => {
+      _("Send a URL to the device on the server");
+      payload.commands = [{
+        command: "displayURI",
+        args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
+        flowID: Utils.makeGUID(),
+      }];
+    }, now - 10);
 
     _("Sync again");
     await syncClientsEngine(server);
+
     compareCommands(engine.localCommands, [{
       command: "displayURI",
       args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
     }], "Should receive incoming URI");
     ok((await engine.processIncomingCommands()), "Should process incoming commands");
     const clearedCommands = (await engine._readCommands())[engine.localID];
     compareCommands(clearedCommands, [{
       command: "displayURI",
       args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
     }], "Should mark the commands as cleared after processing");
 
     _("Check that the command was removed on the server");
     await syncClientsEngine(server);
-    ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
+    let ourPayload = collection.cleartext(engine.localID);
     ok(ourPayload, "Should upload the synced client record");
     deepEqual(ourPayload.commands, [], "Should not reupload cleared commands");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1513,34 +1519,34 @@ add_task(async function test_command_syn
   let server    = await serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.getCollection("foo", "clients");
   let remoteId   = Utils.makeGUID();
   let remoteId2  = Utils.makeGUID();
 
   _("Create remote client record 1");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+  collection.insertRecord({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"]
-  }), Date.now() / 1000));
+  });
 
   _("Create remote client record 2");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
+  collection.insertRecord({
     id: remoteId2,
     name: "Remote client 2",
     type: "mobile",
     commands: [],
     version: "48",
     protocols: ["1.5"]
-  }), Date.now() / 1000));
+  });
 
   try {
     equal(collection.count(), 2, "2 remote records written");
     await syncClientsEngine(server);
     equal(collection.count(), 3, "3 remote records written (+1 for the synced local record)");
 
     await engine.sendCommand("wipeAll", []);
     await engine._tracker.addChangedID(engine.localID);
@@ -1573,39 +1579,40 @@ add_task(async function ensureSameFlowID
     events.push({ object, method, value, extra });
   };
 
   let server = await serverForFoo(engine);
   try {
     // Setup 2 clients, send them a command, and ensure we get to events
     // written, both with the same flowID.
     await SyncTestingInfrastructure(server);
+    let collection = server.getCollection("foo", "clients");
 
     let remoteId   = Utils.makeGUID();
     let remoteId2  = Utils.makeGUID();
 
     _("Create remote client record 1");
-    server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+    collection.insertRecord({
       id: remoteId,
       name: "Remote client",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    }), Date.now() / 1000));
+    });
 
     _("Create remote client record 2");
-    server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
+    collection.insertRecord({
       id: remoteId2,
       name: "Remote client 2",
       type: "mobile",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    }), Date.now() / 1000));
+    });
 
     await syncClientsEngine(server);
     await engine.sendCommand("wipeAll", []);
     await syncClientsEngine(server);
     equal(events.length, 2);
     // we don't know what the flowID is, but do know it should be the same.
     equal(events[0].extra.flowID, events[1].extra.flowID);
     // Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
@@ -1665,39 +1672,40 @@ add_task(async function test_duplicate_c
   let origRecordTelemetryEvent = Service.recordTelemetryEvent;
   Service.recordTelemetryEvent = (object, method, value, extra) => {
     events.push({ object, method, value, extra });
   };
 
   let server = await serverForFoo(engine);
   try {
     await SyncTestingInfrastructure(server);
+    let collection = server.getCollection("foo", "clients");
 
     let remoteId   = Utils.makeGUID();
     let remoteId2  = Utils.makeGUID();
 
     _("Create remote client record 1");
-    server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+    collection.insertRecord({
       id: remoteId,
       name: "Remote client",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    }), Date.now() / 1000));
+    });
 
     _("Create remote client record 2");
-    server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
+    collection.insertRecord({
       id: remoteId2,
       name: "Remote client 2",
       type: "mobile",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    }), Date.now() / 1000));
+    });
 
     await syncClientsEngine(server);
     // Make sure deduping works before syncing
     await engine.sendURIToClientForDisplay("https://example.com", remoteId, "Example");
     await engine.sendURIToClientForDisplay("https://example.com", remoteId, "Example");
     equal(events.length, 1);
     await syncClientsEngine(server);
     // And after syncing.
@@ -1831,61 +1839,60 @@ add_task(async function test_create_reco
 
   let maxSizeStub = sinon.stub(Service,
     "getMemcacheMaxRecordPayloadSize", () => fakeLimit);
 
   let user = server.user("foo");
   let remoteId = Utils.makeGUID();
 
   _("Create remote client record");
-  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
+  user.collection("clients").insertRecord({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "57",
     protocols: ["1.5"],
-  }), Date.now() / 1000));
+  });
 
   try {
     _("Initial sync.");
     await syncClientsEngine(server);
 
     _("Send a fairly sane number of commands.");
 
     for (let i = 0; i < 5; ++i) {
       await engine.sendURIToClientForDisplay(
         `https://www.example.com/1/${i}`, remoteId, `Page 1.${i}`);
     }
 
     await syncClientsEngine(server);
 
     _("Make sure they all fit and weren't dropped.");
-    let parsedServerRecord = JSON.parse(JSON.parse(
-      user.collection("clients").payload(remoteId)).ciphertext);
+    let parsedServerRecord = user.collection("clients").cleartext(remoteId);
 
     equal(parsedServerRecord.commands.length, 5);
 
     await engine.sendCommand("wipeEngine", ["history"], remoteId);
 
     _("Send a not-sane number of commands.");
     // Much higher than the maximum number of commands we could actually fit.
     for (let i = 0; i < 500; ++i) {
       await engine.sendURIToClientForDisplay(
         `https://www.example.com/2/${i}`, remoteId, `Page 2.${i}`);
     }
 
     await syncClientsEngine(server);
 
     _("Ensure we didn't overflow the server limit.");
-    let payload = user.collection("clients").payload(remoteId);
-    less(payload.length, fakeLimit);
+    let wbo = user.collection("clients").wbo(remoteId);
+    less(wbo.payload.length, fakeLimit);
 
     _("And that the data we uploaded is both sane json and containing some commands.");
-    let remoteCommands = JSON.parse(JSON.parse(payload).ciphertext).commands;
+    let remoteCommands = wbo.getCleartext().commands;
     greater(remoteCommands.length, 2);
     let firstCommand = remoteCommands[0];
     _("The first command should still be present, since it had a high priority");
     equal(firstCommand.command, "wipeEngine");
     _("And the last command in the list should be the last command we sent.");
     let lastCommand = remoteCommands[remoteCommands.length - 1];
     equal(lastCommand.command, "displayURI");
     deepEqual(lastCommand.args, ["https://www.example.com/2/499", engine.localID, "Page 2.499"]);
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -156,30 +156,29 @@ add_task(async function test_history_vis
   equal(visits.length, 1);
   equal(visits[0].date, time);
 
   let collection = server.user("foo").collection("history");
 
   // Sync the visit up to the server.
   await sync_engine_and_validate_telem(engine, false);
 
-  let wbo = collection.wbo(id);
-  let data = JSON.parse(JSON.parse(wbo.payload).ciphertext);
-  // Double-check that we didn't round the visit's timestamp to the nearest
-  // millisecond when uploading.
-  equal(data.visits[0].date, time);
-
-  // Add a remote visit so that we get past the deepEquals check in reconcile
-  // (otherwise the history engine will skip applying this record). The contents
-  // of this visit don't matter, beyond the fact that it needs to exist.
-  data.visits.push({
-    date: (Date.now() - oneHourMS / 2) * 1000,
-    type: PlacesUtils.history.TRANSITIONS.LINK
-  });
-  collection.insertWBO(new ServerWBO(id, encryptPayload(data), Date.now() / 1000 + 10));
+  collection.updateRecord(id, cleartext => {
+    // Double-check that we didn't round the visit's timestamp to the nearest
+    // millisecond when uploading.
+    equal(cleartext.visits[0].date, time);
+    // Add a remote visit so that we get past the deepEquals check in reconcile
+    // (otherwise the history engine will skip applying this record). The
+    // contents of this visit don't matter, beyond the fact that it needs to
+    // exist.
+    cleartext.visits.push({
+      date: (Date.now() - oneHourMS / 2) * 1000,
+      type: PlacesUtils.history.TRANSITIONS.LINK
+    });
+  }, Date.now() / 1000 + 10);
 
   // Force a remote sync.
   engine.lastSync = Date.now() / 1000 - 30;
   await sync_engine_and_validate_telem(engine, false);
 
   // Make sure that we didn't duplicate the visit when inserting. (Prior to bug
   // 1423395, we would insert a duplicate visit, where the timestamp was
   // effectively `Math.round(microsecondTimestamp / 1000) * 1000`.)
@@ -215,39 +214,37 @@ add_task(async function test_history_vis
     includeVisits: true
   });
   equal(allVisits.length, 26);
 
   let collection = server.user("foo").collection("history");
 
   await sync_engine_and_validate_telem(engine, false);
 
-  let wbo = collection.wbo(guid);
-  let data = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+  collection.updateRecord(guid, data => {
+    data.visits.push(
+      // Add a couple remote visit equivalent to some old visits we have already
+      {
+        date: Date.UTC(2017, 10, 1) * 1000, // Nov 1, 2017
+        type: PlacesUtils.history.TRANSITIONS.LINK
+      }, {
+        date: Date.UTC(2017, 10, 2) * 1000, // Nov 2, 2017
+        type: PlacesUtils.history.TRANSITIONS.LINK
+      },
+      // Add a couple new visits to make sure we are still applying them.
+      {
+        date: Date.UTC(2017, 11, 4) * 1000, // Dec 4, 2017
+        type: PlacesUtils.history.TRANSITIONS.LINK
+      }, {
+        date: Date.UTC(2017, 11, 5) * 1000, // Dec 5, 2017
+        type: PlacesUtils.history.TRANSITIONS.LINK
+      }
+    );
+  }, Date.now() / 1000 + 10);
 
-  data.visits.push(
-    // Add a couple remote visit equivalent to some old visits we have already
-    {
-      date: Date.UTC(2017, 10, 1) * 1000, // Nov 1, 2017
-      type: PlacesUtils.history.TRANSITIONS.LINK
-    }, {
-      date: Date.UTC(2017, 10, 2) * 1000, // Nov 2, 2017
-      type: PlacesUtils.history.TRANSITIONS.LINK
-    },
-    // Add a couple new visits to make sure we are still applying them.
-    {
-      date: Date.UTC(2017, 11, 4) * 1000, // Dec 4, 2017
-      type: PlacesUtils.history.TRANSITIONS.LINK
-    }, {
-      date: Date.UTC(2017, 11, 5) * 1000, // Dec 5, 2017
-      type: PlacesUtils.history.TRANSITIONS.LINK
-    }
-  );
-
-  collection.insertWBO(new ServerWBO(guid, encryptPayload(data), Date.now() / 1000 + 10));
   engine.lastSync = Date.now() / 1000 - 30;
   await sync_engine_and_validate_telem(engine, false);
 
   allVisits = (await PlacesUtils.history.fetch("https://www.example.com", {
     includeVisits: true
   })).visits;
 
   equal(allVisits.length, 28);
--- a/services/sync/tests/unit/test_password_engine.js
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -155,18 +155,17 @@ add_task(async function test_password_en
     collection.insert(oldLogin.guid, encryptPayload(rec.cleartext));
   }
 
   await engine._tracker.stop();
 
   try {
     await sync_engine_and_validate_telem(engine, false);
 
-    let newRec = JSON.parse(JSON.parse(
-      collection.payload(newLogin.guid)).ciphertext);
+    let newRec = collection.cleartext(newLogin.guid);
     equal(newRec.password, "password",
       "Should update remote password for newer login");
 
     let logins = Services.logins.findLogins({}, "https://mozilla.com", "", "");
     equal(logins[0].password, "n3wpa55",
       "Should update local password for older login");
   } finally {
     await cleanup(engine, server);
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -474,17 +474,17 @@ add_task(async function test_processInco
 
   // After the sync, the server's payload for the original ID should be marked
   // as deleted.
   do_check_empty(engine._store.items);
   let collection = server.getCollection(user, "rotary");
   Assert.equal(1, collection.count());
   wbo = collection.wbo("DUPE_INCOMING");
   Assert.notEqual(null, wbo);
-  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+  let payload = wbo.getCleartext();
   Assert.ok(payload.deleted);
 
   await cleanAndGo(engine, server);
 });
 
 add_task(async function test_processIncoming_reconcile_locally_deleted_dupe_old() {
   _("Ensure locally deleted duplicate record older than incoming is restored.");
 
@@ -513,17 +513,17 @@ add_task(async function test_processInco
   // Since the remote change is newer, the incoming item should exist locally.
   do_check_attribute_count(engine._store.items, 1);
   Assert.ok("DUPE_INCOMING" in engine._store.items);
   Assert.equal("incoming", engine._store.items.DUPE_INCOMING);
 
   let collection = server.getCollection(user, "rotary");
   Assert.equal(1, collection.count());
   wbo = collection.wbo("DUPE_INCOMING");
-  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+  let payload = wbo.getCleartext();
   Assert.equal("incoming", payload.denomination);
 
   await cleanAndGo(engine, server);
 });
 
 add_task(async function test_processIncoming_reconcile_changed_dupe() {
   _("Ensure that locally changed duplicate record is handled properly.");
 
@@ -551,17 +551,17 @@ add_task(async function test_processInco
   Assert.ok("DUPE_INCOMING" in engine._store.items);
 
   // On the server, the local ID should be deleted and the incoming ID should
   // have its payload set to what was in the local record.
   let collection = server.getCollection(user, "rotary");
   Assert.equal(1, collection.count());
   wbo = collection.wbo("DUPE_INCOMING");
   Assert.notEqual(undefined, wbo);
-  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+  let payload = wbo.getCleartext();
   Assert.equal("local", payload.denomination);
 
   await cleanAndGo(engine, server);
 });
 
 add_task(async function test_processIncoming_reconcile_changed_dupe_new() {
   _("Ensure locally changed duplicate record older than incoming is ignored.");
 
@@ -590,17 +590,17 @@ add_task(async function test_processInco
   Assert.ok("DUPE_INCOMING" in engine._store.items);
 
   // On the server, the local ID should be deleted and the incoming ID should
   // have its payload retained.
   let collection = server.getCollection(user, "rotary");
   Assert.equal(1, collection.count());
   wbo = collection.wbo("DUPE_INCOMING");
   Assert.notEqual(undefined, wbo);
-  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+  let payload = wbo.getCleartext();
   Assert.equal("incoming", payload.denomination);
   await cleanAndGo(engine, server);
 });
 
 add_task(async function test_processIncoming_resume_toFetch() {
   _("toFetch and previousFailed items left over from previous syncs are fetched on the next sync, along with new items.");
 
   const LASTSYNC = Date.now() / 1000;
@@ -1079,18 +1079,17 @@ add_task(async function test_uploadOutgo
 
     // Local timestamp has been set.
     Assert.ok(engine.lastSyncLocal > 0);
 
     // Ensure the marked record ('scotsman') has been uploaded and is
     // no longer marked.
     Assert.equal(collection.payload("flying"), undefined);
     Assert.ok(!!collection.payload("scotsman"));
-    Assert.equal(JSON.parse(collection.wbo("scotsman").data.ciphertext).id,
-                 "scotsman");
+    Assert.equal(collection.cleartext("scotsman").id, "scotsman");
     const changes = await engine._tracker.getChangedIDs();
     Assert.equal(changes.scotsman, undefined);
 
     // The 'flying' record wasn't marked so it wasn't uploaded
     Assert.equal(collection.payload("flying"), undefined);
 
   } finally {
     await cleanAndGo(engine, server);