Backed out changeset 885ab31f399b (bug 1441666) for xpcshell failure on browser/extensions/formautofill/test/unit/test_sync.js. CLOSED TREE
authorDorel Luca <dluca@mozilla.com>
Fri, 02 Mar 2018 00:37:49 +0200
changeset 461197 a197d5e1bce62e3bcb232975ad2afe6ed9be1dc1
parent 461196 a1330e4089ad73ec22d89b3162bf50bfa534feb8
child 461198 d1a509b4cdfbec084aa5bf57f91038813f0cc645
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)
bugs1441666
milestone60.0a1
backs out885ab31f399bc91eeb70d4721e2c6c90e56508fc
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
Backed out changeset 885ab31f399b (bug 1441666) for xpcshell failure on browser/extensions/formautofill/test/unit/test_sync.js. CLOSED TREE
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_addons_engine.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_engine_changes_during_sync.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,18 +3,16 @@
 /* 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
@@ -142,37 +140,17 @@ 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.
  *
@@ -250,78 +228,42 @@ ServerCollection.prototype = {
     return os;
   },
 
   /**
    * Convenience method to get an array of parsed ciphertexts.
    *
    * @return an array of the payloads of each stored WBO.
    */
-  cleartextPayloads() {
-    return this.wbos().map(wbo => wbo.getCleartext());
+  payloads() {
+    return this.wbos().map(function(wbo) {
+      return JSON.parse(JSON.parse(wbo.payload).ciphertext);
+    });
   },
 
   // 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_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -229,17 +229,17 @@ add_task(async function test_disabled_in
   let collection = server.getCollection(USER, "addons");
   engine.lastModified = collection.timestamp;
   await engine._sync();
 
   // The client should not upload a new record. The old record should be
   // retained and unmodified.
   Assert.equal(1, collection.count());
 
-  let payload = collection.cleartextPayloads()[0];
+  let payload = collection.payloads()[0];
   Assert.notEqual(null, collection.wbo(id));
   Assert.equal(ADDON_ID, payload.addonID);
   Assert.ok(!payload.enabled);
 
   await promiseStopServer(server);
 });
 
 add_test(function cleanup() {
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -61,30 +61,32 @@ 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) {
-  return collection.cleartext(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);
 }
 
 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));
 }
 
 async function validate(collection, expectedFailures = []) {
   let validator = new BookmarkValidator();
-  let records = collection.cleartextPayloads();
+  let records = collection.payloads();
 
   let { problemData: problems } = await validator.inspectServerRecords(records);
   // all non-zero problems.
   let summary = problems.getSummary().filter(prob => prob.count != 0);
 
   // split into 2 arrays - expected and unexpected.
   let isInExpectedFailures = elt => {
     for (let i = 0; i < expectedFailures.length; i++) {
@@ -101,17 +103,17 @@ async function validate(collection, expe
   }
   if (unexpected.length || expected.length != expectedFailures.length) {
     info("Validation failed:");
     info(JSON.stringify(summary));
     // print the entire validator output as it has IDs etc.
     info(JSON.stringify(problems, undefined, 2));
     info("Expected: " + JSON.stringify(expectedFailures, undefined, 2));
     // All server records and the entire bookmark tree.
-    info("Server records:\n" + JSON.stringify(collection.cleartextPayloads(), undefined, 2));
+    info("Server records:\n" + JSON.stringify(collection.payloads(), undefined, 2));
     let tree = await PlacesUtils.promiseBookmarksTree("", { includeItemIds: true });
     info("Local bookmark tree:\n" + JSON.stringify(tree, undefined, 2));
     ok(false);
   }
 }
 
 add_task(async function test_dupe_bookmark() {
   _("Ensure that a bookmark we consider a dupe is handled correctly.");
@@ -159,23 +161,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(collection.cleartext(bmk1_guid).deleted);
+    ok(getServerRecord(collection, 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 = collection.cleartext(folder1_guid);
+    let serverRecord = getServerRecord(collection, 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);
@@ -220,31 +222,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(collection.cleartext(bmk1_guid).deleted);
+    ok(getServerRecord(collection, 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 = collection.cleartext(folder1_guid);
+    let serverRecord1 = getServerRecord(collection, 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 = collection.cleartext(folder2_guid);
+    let serverRecord2 = getServerRecord(collection, 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);
   }
@@ -301,31 +303,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(collection.cleartext(bmk1_guid).deleted);
+    ok(getServerRecord(collection, 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 = collection.cleartext(folder1_guid);
+    let serverRecord1 = getServerRecord(collection, 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 = collection.cleartext(folder2_guid);
+    let serverRecord2 = getServerRecord(collection, 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);
   }
@@ -524,24 +526,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(collection.cleartext(bmk1_guid).deleted);
+    ok(getServerRecord(collection, 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 = collection.cleartext(folder1_guid);
+    let serverRecord1 = getServerRecord(collection, 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);
 
@@ -642,15 +644,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(collection.cleartext(folder1_guid).deleted);
+    ok(getServerRecord(collection, 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
@@ -354,17 +354,17 @@ async function test_restoreOrImport(engi
     } catch (ex) {
       error = ex;
       _("Got error: " + Log.exceptionStr(ex));
     }
     Assert.ok(!error);
 
     _("Verify that there's the right bookmarks on the server.");
     // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
-    let payloads     = server.user("foo").collection("bookmarks").cleartextPayloads();
+    let payloads     = server.user("foo").collection("bookmarks").payloads();
     let bookmarkWBOs = payloads.filter(function(wbo) {
                          return wbo.type == "bookmark";
                        });
 
     let folderWBOs   = payloads.filter(function(wbo) {
                          return ((wbo.type == "folder") &&
                                  (wbo.id != "menu") &&
                                  (wbo.id != "toolbar") &&
@@ -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 = collection.cleartext(bz.guid);
+    let bzWBO = JSON.parse(JSON.parse(collection._wbos[bz.guid].payload).ciphertext);
     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,17 +862,18 @@ 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 = collection.cleartext("bookmarkBBBB");
+    let bPayload = JSON.parse(JSON.parse(
+      collection.payload("bookmarkBBBB")).ciphertext);
     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 = serverWBO.getCleartext();
+    let body = JSON.parse(JSON.parse(serverWBO.payload).ciphertext);
     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 = user.collection("clients").wbo(id).data;
+    let payload = JSON.parse(user.collection("clients").wbo(id).payload);
 
     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();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     id: activeID,
     name: "Active client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }, now - 10);
+  }), now - 10));
 
   let deletedID = Utils.makeGUID();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
     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();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     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 = collection.cleartext(activeID);
+    let payload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext);
     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();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(recentID, encryptPayload({
     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();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
     id: dupeID,
     name: engine.localName,
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }, now - 604820);
+  }), now - 604810));
 
   // Synced more than 1 week ago, but not a dupe.
   let oldID = Utils.makeGUID();
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(oldID, encryptPayload({
     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 = collection.cleartext(recentID);
+    let recentPayload = JSON.parse(JSON.parse(collection.payload(recentID)).ciphertext);
     compareCommands(recentPayload.commands, [{ command: "logout", args: [] }],
                     "Should send commands to the recent client");
 
-    let oldPayload = collection.cleartext(oldID);
+    let oldPayload = JSON.parse(JSON.parse(collection.payload(oldID)).ciphertext);
     compareCommands(oldPayload.commands, [{ command: "logout", args: [] }],
                     "Should send commands to the week-old client");
 
-    let dupePayload = collection.cleartext(dupeID);
+    let dupePayload = JSON.parse(JSON.parse(collection.payload(dupeID)).ciphertext);
     deepEqual(dupePayload.commands, [],
               "Should not send commands to the dupe client");
 
     _("Update the dupe client's modified time");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
       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");
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
     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,38 +825,36 @@ 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");
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteId,
     protocols: ["1.5"],
-  });
-
-  collection.insertRecord({
+  }), Date.now() / 1000));
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
     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 }]); }
   };
 
@@ -867,16 +865,17 @@ 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);
     }
   }
 });
 
 
@@ -888,37 +887,35 @@ 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");
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     fxaDeviceId: remoteDeviceId,
     protocols: ["1.5"],
-  }, Date.now() / 1000 - 30000);
-  collection.insertRecord({
+  }), Date.now() / 1000 - 30000));
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
     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 }]); }
   };
 
@@ -929,16 +926,17 @@ 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);
     }
   }
 });
 
 
@@ -1061,68 +1059,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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
     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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(mobileID, encryptPayload({
     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 desktopPayload = collection.cleartext(desktopID);
+    let collection = server.getCollection("foo", "clients");
+    let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
     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 = collection.cleartext(mobileID);
+    let mobilePayload = JSON.parse(JSON.parse(collection.payload(mobileID)).ciphertext);
     compareCommands(mobilePayload.commands, [{ command: "logout", args: [] }],
                     "Should not send a duplicate logout to the mobile client");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1135,52 +1133,51 @@ 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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
     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");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
       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 desktopPayload = collection.cleartext(desktopID);
+    let collection = server.getCollection("foo", "clients");
+    let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
     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 {
@@ -1195,76 +1192,75 @@ 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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
     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);
-  collection.insertRecord({
+  }), now - 10));
+  server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({
     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 deviceBPayload = collection.cleartext(deviceBID);
+    let collection = server.getCollection("foo", "clients");
+    let deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
     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");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
       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 = collection.cleartext(deviceBID);
+    deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
     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 {
@@ -1279,77 +1275,76 @@ 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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({
     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);
-  collection.insertRecord({
+  }), now - 10));
+  server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
     id: deviceBID,
     name: "Device B",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }, now - 10);
-  collection.insertRecord({
+  }), now - 10));
+  server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({
     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 = collection.cleartext(engine.localID);
+    let localRemoteRecord = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
     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
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({
       id: engine.localID,
       name: "Device A",
       type: "desktop",
       commands: [{
         command: "displayURI",
         args: ["https://deviceblink.com", deviceBID, "Device B link"],
         flowID: Utils.makeGUID(),
       },
@@ -1360,30 +1355,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 = collection.cleartext(deviceBID);
+    localRemoteRecord = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
     deepEqual(localRemoteRecord.commands, [], "Should be empty");
   } finally {
     await cleanup();
 
     // Reset service (remove mocks)
     engine = Service.clientsEngine = new ClientEngine(Service);
     await engine.initialize();
     engine._resetClient();
@@ -1400,53 +1395,52 @@ 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();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     id: activeID,
     name: "Active client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"],
-  }, now - 10);
+  }), now - 10));
 
   let deletedID = Utils.makeGUID();
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
     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 = collection.cleartext(activeID);
+    let activePayload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext);
     compareCommands(activePayload.commands, [{ command: "logout", args: [] }],
       "Should send the command to the active client");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1465,43 +1459,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");
 
-    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);
+    _("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));
 
     _("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);
-    let ourPayload = collection.cleartext(engine.localID);
+    ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext);
     ok(ourPayload, "Should upload the synced client record");
     deepEqual(ourPayload.commands, [], "Should not reupload cleared commands");
   } finally {
     await cleanup();
 
     try {
       server.deleteCollections("foo");
     } finally {
@@ -1519,34 +1513,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");
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
     id: remoteId,
     name: "Remote client",
     type: "desktop",
     commands: [],
     version: "48",
     protocols: ["1.5"]
-  });
+  }), Date.now() / 1000));
 
   _("Create remote client record 2");
-  collection.insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
     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);
@@ -1579,40 +1573,39 @@ 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");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
       id: remoteId,
       name: "Remote client",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    });
+    }), Date.now() / 1000));
 
     _("Create remote client record 2");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
       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.
@@ -1672,40 +1665,39 @@ 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");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
       id: remoteId,
       name: "Remote client",
       type: "desktop",
       commands: [],
       version: "48",
       protocols: ["1.5"]
-    });
+    }), Date.now() / 1000));
 
     _("Create remote client record 2");
-    collection.insertRecord({
+    server.insertWBO("foo", "clients", new ServerWBO(remoteId2, encryptPayload({
       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.
@@ -1839,60 +1831,61 @@ 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");
-  user.collection("clients").insertRecord({
+  server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
     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 = user.collection("clients").cleartext(remoteId);
+    let parsedServerRecord = JSON.parse(JSON.parse(
+      user.collection("clients").payload(remoteId)).ciphertext);
 
     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 wbo = user.collection("clients").wbo(remoteId);
-    less(wbo.payload.length, fakeLimit);
+    let payload = user.collection("clients").payload(remoteId);
+    less(payload.length, fakeLimit);
 
     _("And that the data we uploaded is both sane json and containing some commands.");
-    let remoteCommands = wbo.getCleartext().commands;
+    let remoteCommands = JSON.parse(JSON.parse(payload).ciphertext).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_engine_changes_during_sync.js
+++ b/services/sync/tests/unit/test_engine_changes_during_sync.js
@@ -179,26 +179,26 @@ add_task(async function test_prefs_chang
     remoteRec.value = {
       [TEST_PREF]: "world",
     };
     collection.insert(remoteRec.id, encryptPayload(remoteRec.cleartext));
 
     await sync_engine_and_validate_telem(engine, true);
     strictEqual(Service.scheduler.globalScore, 0,
       "Should not bump global score for prefs added during first sync");
-    let payloads = collection.cleartextPayloads();
+    let payloads = collection.payloads();
     equal(payloads.length, 1,
       "Should not upload multiple prefs records after first sync");
     equal(payloads[0].value[TEST_PREF], "world",
       "Should not upload pref value changed during first sync");
 
     await sync_engine_and_validate_telem(engine, true);
     strictEqual(Service.scheduler.globalScore, 0,
       "Should not bump global score during second prefs sync");
-    payloads = collection.cleartextPayloads();
+    payloads = collection.payloads();
     equal(payloads.length, 1,
       "Should not upload multiple prefs records after second sync");
     equal(payloads[0].value[TEST_PREF], "hello",
       "Should upload changed pref value during second sync");
   } finally {
     engine._uploadOutgoing = uploadOutgoing;
     await cleanup(engine, server);
     Services.prefs.clearUserPref(TEST_PREF);
--- a/services/sync/tests/unit/test_history_engine.js
+++ b/services/sync/tests/unit/test_history_engine.js
@@ -156,29 +156,30 @@ 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);
 
-  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);
+  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));
 
   // 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`.)
@@ -214,37 +215,39 @@ 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);
 
-  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);
+  let wbo = collection.wbo(guid);
+  let data = JSON.parse(JSON.parse(wbo.payload).ciphertext);
 
+  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,17 +155,18 @@ 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 = collection.cleartext(newLogin.guid);
+    let newRec = JSON.parse(JSON.parse(
+      collection.payload(newLogin.guid)).ciphertext);
     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 = wbo.getCleartext();
+  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   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 = wbo.getCleartext();
+  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   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 = wbo.getCleartext();
+  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   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 = wbo.getCleartext();
+  let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
   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,17 +1079,18 @@ 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(collection.cleartext("scotsman").id, "scotsman");
+    Assert.equal(JSON.parse(collection.wbo("scotsman").data.ciphertext).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);