summaryrefslogtreecommitdiff
path: root/services/sync/tests/unit/test_clients_engine.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/test_clients_engine.js')
-rw-r--r--services/sync/tests/unit/test_clients_engine.js1439
1 files changed, 1439 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_clients_engine.js b/services/sync/tests/unit/test_clients_engine.js
new file mode 100644
index 0000000000..d2123f80a0
--- /dev/null
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -0,0 +1,1439 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
+const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
+
+var engine = Service.clientsEngine;
+
+/**
+ * Unpack the record with this ID, and verify that it has the same version that
+ * we should be putting into records.
+ */
+function check_record_version(user, id) {
+ 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;
+
+ let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients"));
+
+ _("Payload is " + JSON.stringify(cleartext));
+ equal(Services.appinfo.version, cleartext.version);
+ equal(2, cleartext.protocols.length);
+ equal("1.1", cleartext.protocols[0]);
+ equal("1.5", cleartext.protocols[1]);
+}
+
+add_test(function test_bad_hmac() {
+ _("Ensure that Clients engine deletes corrupt records.");
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let deletedCollections = [];
+ let deletedItems = [];
+ let callback = {
+ __proto__: SyncServerCallback,
+ onItemDeleted: function (username, coll, wboID) {
+ deletedItems.push(coll + "/" + wboID);
+ },
+ onCollectionDeleted: function (username, coll) {
+ deletedCollections.push(coll);
+ }
+ }
+ let server = serverForUsers({"foo": "password"}, contents, callback);
+ let user = server.user("foo");
+
+ function check_clients_count(expectedCount) {
+ let stack = Components.stack.caller;
+ let coll = user.collection("clients");
+
+ // Treat a non-existent collection as empty.
+ equal(expectedCount, coll ? coll.count() : 0, stack);
+ }
+
+ function check_client_deleted(id) {
+ let coll = user.collection("clients");
+ let wbo = coll.wbo(id);
+ return !wbo || !wbo.payload;
+ }
+
+ function uploadNewKeys() {
+ generateNewKeys(Service.collectionKeys);
+ let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+ serverKeys.encrypt(Service.identity.syncKeyBundle);
+ ok(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
+ }
+
+ try {
+ ensureLegacyIdentityManager();
+ let passphrase = "abcdeabcdeabcdeabcdeabcdea";
+ Service.serverURL = server.baseURI;
+ Service.login("foo", "ilovejane", passphrase);
+
+ generateNewKeys(Service.collectionKeys);
+
+ _("First sync, client record is uploaded");
+ equal(engine.lastRecordUpload, 0);
+ check_clients_count(0);
+ engine._sync();
+ check_clients_count(1);
+ ok(engine.lastRecordUpload > 0);
+
+ // Our uploaded record has a version.
+ check_record_version(user, engine.localID);
+
+ // Initial setup can wipe the server, so clean up.
+ deletedCollections = [];
+ deletedItems = [];
+
+ _("Change our keys and our client ID, reupload keys.");
+ let oldLocalID = engine.localID; // Preserve to test for deletion!
+ engine.localID = Utils.makeGUID();
+ engine.resetClient();
+ generateNewKeys(Service.collectionKeys);
+ let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+ serverKeys.encrypt(Service.identity.syncKeyBundle);
+ ok(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
+
+ _("Sync.");
+ engine._sync();
+
+ _("Old record " + oldLocalID + " was deleted, new one uploaded.");
+ check_clients_count(1);
+ check_client_deleted(oldLocalID);
+
+ _("Now change our keys but don't upload them. " +
+ "That means we get an HMAC error but redownload keys.");
+ Service.lastHMACEvent = 0;
+ engine.localID = Utils.makeGUID();
+ engine.resetClient();
+ generateNewKeys(Service.collectionKeys);
+ deletedCollections = [];
+ deletedItems = [];
+ check_clients_count(1);
+ engine._sync();
+
+ _("Old record was not deleted, new one uploaded.");
+ equal(deletedCollections.length, 0);
+ equal(deletedItems.length, 0);
+ check_clients_count(2);
+
+ _("Now try the scenario where our keys are wrong *and* there's a bad record.");
+ // Clean up and start fresh.
+ user.collection("clients")._wbos = {};
+ Service.lastHMACEvent = 0;
+ engine.localID = Utils.makeGUID();
+ engine.resetClient();
+ deletedCollections = [];
+ deletedItems = [];
+ check_clients_count(0);
+
+ uploadNewKeys();
+
+ // Sync once to upload a record.
+ engine._sync();
+ check_clients_count(1);
+
+ // Generate and upload new keys, so the old client record is wrong.
+ uploadNewKeys();
+
+ // Create a new client record and new keys. Now our keys are wrong, as well
+ // as the object on the server. We'll download the new keys and also delete
+ // the bad client record.
+ oldLocalID = engine.localID; // Preserve to test for deletion!
+ engine.localID = Utils.makeGUID();
+ engine.resetClient();
+ generateNewKeys(Service.collectionKeys);
+ let oldKey = Service.collectionKeys.keyForCollection();
+
+ equal(deletedCollections.length, 0);
+ equal(deletedItems.length, 0);
+ engine._sync();
+ equal(deletedItems.length, 1);
+ check_client_deleted(oldLocalID);
+ check_clients_count(1);
+ let newKey = Service.collectionKeys.keyForCollection();
+ ok(!oldKey.equals(newKey));
+
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ server.stop(run_next_test);
+ }
+});
+
+add_test(function test_properties() {
+ _("Test lastRecordUpload property");
+ try {
+ equal(Svc.Prefs.get("clients.lastRecordUpload"), undefined);
+ equal(engine.lastRecordUpload, 0);
+
+ let now = Date.now();
+ engine.lastRecordUpload = now / 1000;
+ equal(engine.lastRecordUpload, Math.floor(now / 1000));
+ } finally {
+ Svc.Prefs.resetBranch("");
+ run_next_test();
+ }
+});
+
+add_test(function test_full_sync() {
+ _("Ensure that Clients engine fetches all records for each sync.");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let activeID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
+ id: activeID,
+ name: "Active client",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ let deletedID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
+ id: deletedID,
+ name: "Client to delete",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ try {
+ let store = engine._store;
+
+ _("First sync. 2 records downloaded; our record uploaded.");
+ strictEqual(engine.lastRecordUpload, 0);
+ engine._sync();
+ ok(engine.lastRecordUpload > 0);
+ deepEqual(user.collection("clients").keys().sort(),
+ [activeID, deletedID, engine.localID].sort(),
+ "Our record should be uploaded on first sync");
+ deepEqual(Object.keys(store.getAllIDs()).sort(),
+ [activeID, deletedID, engine.localID].sort(),
+ "Other clients should be downloaded on first sync");
+
+ _("Delete a record, then sync again");
+ let collection = server.getCollection("foo", "clients");
+ collection.remove(deletedID);
+ // Simulate a timestamp update in info/collections.
+ engine.lastModified = now;
+ engine._sync();
+
+ _("Record should be updated");
+ deepEqual(Object.keys(store.getAllIDs()).sort(),
+ [activeID, engine.localID].sort(),
+ "Deleted client should be removed on next sync");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_sync() {
+ _("Ensure that Clients engine uploads a new client record once a week.");
+
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ function clientWBO() {
+ return user.collection("clients").wbo(engine.localID);
+ }
+
+ try {
+
+ _("First sync. Client record is uploaded.");
+ equal(clientWBO(), undefined);
+ equal(engine.lastRecordUpload, 0);
+ engine._sync();
+ ok(!!clientWBO().payload);
+ ok(engine.lastRecordUpload > 0);
+
+ _("Let's time travel more than a week back, new record should've been uploaded.");
+ engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
+ let lastweek = engine.lastRecordUpload;
+ clientWBO().payload = undefined;
+ engine._sync();
+ ok(!!clientWBO().payload);
+ ok(engine.lastRecordUpload > lastweek);
+
+ _("Remove client record.");
+ engine.removeClientData();
+ equal(clientWBO().payload, undefined);
+
+ _("Time travel one day back, no record uploaded.");
+ engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
+ let yesterday = engine.lastRecordUpload;
+ engine._sync();
+ equal(clientWBO().payload, undefined);
+ equal(engine.lastRecordUpload, yesterday);
+
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ server.stop(run_next_test);
+ }
+});
+
+add_test(function test_client_name_change() {
+ _("Ensure client name change incurs a client record update.");
+
+ let tracker = engine._tracker;
+
+ let localID = engine.localID;
+ let initialName = engine.localName;
+
+ Svc.Obs.notify("weave:engine:start-tracking");
+ _("initial name: " + initialName);
+
+ // Tracker already has data, so clear it.
+ tracker.clearChangedIDs();
+
+ let initialScore = tracker.score;
+
+ equal(Object.keys(tracker.changedIDs).length, 0);
+
+ Svc.Prefs.set("client.name", "new name");
+
+ _("new name: " + engine.localName);
+ notEqual(initialName, engine.localName);
+ equal(Object.keys(tracker.changedIDs).length, 1);
+ ok(engine.localID in tracker.changedIDs);
+ ok(tracker.score > initialScore);
+ ok(tracker.score >= SCORE_INCREMENT_XLARGE);
+
+ Svc.Obs.notify("weave:engine:stop-tracking");
+
+ run_next_test();
+});
+
+add_test(function test_send_command() {
+ _("Verifies _sendCommandToClient puts commands in the outbound queue.");
+
+ let store = engine._store;
+ let tracker = engine._tracker;
+ let remoteId = Utils.makeGUID();
+ let rec = new ClientsRec("clients", remoteId);
+
+ store.create(rec);
+ let remoteRecord = store.createRecord(remoteId, "clients");
+
+ let action = "testCommand";
+ let args = ["foo", "bar"];
+
+ engine._sendCommandToClient(action, args, remoteId);
+
+ let newRecord = store._remoteClients[remoteId];
+ let clientCommands = engine._readCommands()[remoteId];
+ notEqual(newRecord, undefined);
+ equal(clientCommands.length, 1);
+
+ let command = clientCommands[0];
+ equal(command.command, action);
+ equal(command.args.length, 2);
+ deepEqual(command.args, args);
+
+ notEqual(tracker.changedIDs[remoteId], undefined);
+
+ run_next_test();
+});
+
+add_test(function test_command_validation() {
+ _("Verifies that command validation works properly.");
+
+ let store = engine._store;
+
+ let testCommands = [
+ ["resetAll", [], true ],
+ ["resetAll", ["foo"], false],
+ ["resetEngine", ["tabs"], true ],
+ ["resetEngine", [], false],
+ ["wipeAll", [], true ],
+ ["wipeAll", ["foo"], false],
+ ["wipeEngine", ["tabs"], true ],
+ ["wipeEngine", [], false],
+ ["logout", [], true ],
+ ["logout", ["foo"], false],
+ ["__UNKNOWN__", [], false]
+ ];
+
+ for (let [action, args, expectedResult] of testCommands) {
+ let remoteId = Utils.makeGUID();
+ let rec = new ClientsRec("clients", remoteId);
+
+ store.create(rec);
+ store.createRecord(remoteId, "clients");
+
+ engine.sendCommand(action, args, remoteId);
+
+ let newRecord = store._remoteClients[remoteId];
+ notEqual(newRecord, undefined);
+
+ let clientCommands = engine._readCommands()[remoteId];
+
+ if (expectedResult) {
+ _("Ensuring command is sent: " + action);
+ equal(clientCommands.length, 1);
+
+ let command = clientCommands[0];
+ equal(command.command, action);
+ deepEqual(command.args, args);
+
+ notEqual(engine._tracker, undefined);
+ notEqual(engine._tracker.changedIDs[remoteId], undefined);
+ } else {
+ _("Ensuring command is scrubbed: " + action);
+ equal(clientCommands, undefined);
+
+ if (store._tracker) {
+ equal(engine._tracker[remoteId], undefined);
+ }
+ }
+
+ }
+ run_next_test();
+});
+
+add_test(function test_command_duplication() {
+ _("Ensures duplicate commands are detected and not added");
+
+ let store = engine._store;
+ let remoteId = Utils.makeGUID();
+ let rec = new ClientsRec("clients", remoteId);
+ store.create(rec);
+ store.createRecord(remoteId, "clients");
+
+ let action = "resetAll";
+ let args = [];
+
+ engine.sendCommand(action, args, remoteId);
+ engine.sendCommand(action, args, remoteId);
+
+ let newRecord = store._remoteClients[remoteId];
+ let clientCommands = engine._readCommands()[remoteId];
+ equal(clientCommands.length, 1);
+
+ _("Check variant args length");
+ engine._saveCommands({});
+
+ action = "resetEngine";
+ engine.sendCommand(action, [{ x: "foo" }], remoteId);
+ engine.sendCommand(action, [{ x: "bar" }], remoteId);
+
+ _("Make sure we spot a real dupe argument.");
+ engine.sendCommand(action, [{ x: "bar" }], remoteId);
+
+ clientCommands = engine._readCommands()[remoteId];
+ equal(clientCommands.length, 2);
+
+ run_next_test();
+});
+
+add_test(function test_command_invalid_client() {
+ _("Ensures invalid client IDs are caught");
+
+ let id = Utils.makeGUID();
+ let error;
+
+ try {
+ engine.sendCommand("wipeAll", [], id);
+ } catch (ex) {
+ error = ex;
+ }
+
+ equal(error.message.indexOf("Unknown remote client ID: "), 0);
+
+ run_next_test();
+});
+
+add_test(function test_process_incoming_commands() {
+ _("Ensures local commands are executed");
+
+ engine.localCommands = [{ command: "logout", args: [] }];
+
+ let ev = "weave:service:logout:finish";
+
+ var handler = function() {
+ Svc.Obs.remove(ev, handler);
+
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ run_next_test();
+ };
+
+ Svc.Obs.add(ev, handler);
+
+ // logout command causes processIncomingCommands to return explicit false.
+ ok(!engine.processIncomingCommands());
+});
+
+add_test(function test_filter_duplicate_names() {
+ _("Ensure that we exclude clients with identical names that haven't synced in a week.");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ // Synced recently.
+ let recentID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(recentID, encryptPayload({
+ id: recentID,
+ name: "My Phone",
+ type: "mobile",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ // Dupe of our client, synced more than 1 week ago.
+ let dupeID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
+ id: dupeID,
+ name: engine.localName,
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 604810));
+
+ // Synced more than 1 week ago, but not a dupe.
+ let oldID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(oldID, encryptPayload({
+ id: oldID,
+ name: "My old desktop",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 604820));
+
+ try {
+ let store = engine._store;
+
+ _("First sync");
+ strictEqual(engine.lastRecordUpload, 0);
+ engine._sync();
+ ok(engine.lastRecordUpload > 0);
+ deepEqual(user.collection("clients").keys().sort(),
+ [recentID, dupeID, oldID, engine.localID].sort(),
+ "Our record should be uploaded on first sync");
+
+ deepEqual(Object.keys(store.getAllIDs()).sort(),
+ [recentID, dupeID, oldID, engine.localID].sort(),
+ "Duplicate ID should remain in getAllIDs");
+ ok(engine._store.itemExists(dupeID), "Dupe ID should be considered as existing for Sync methods.");
+ ok(!engine.remoteClientExists(dupeID), "Dupe ID should not be considered as existing for external methods.");
+
+ // dupe desktop should not appear in .deviceTypes.
+ equal(engine.deviceTypes.get("desktop"), 2);
+ equal(engine.deviceTypes.get("mobile"), 1);
+
+ // dupe desktop should not appear in stats
+ deepEqual(engine.stats, {
+ hasMobile: 1,
+ names: [engine.localName, "My Phone", "My old desktop"],
+ numClients: 3,
+ });
+
+ ok(engine.remoteClientExists(oldID), "non-dupe ID should exist.");
+ ok(!engine.remoteClientExists(dupeID), "dupe ID should not exist");
+ equal(engine.remoteClients.length, 2, "dupe should not be in remoteClients");
+
+ // Check that a subsequent Sync doesn't report anything as being processed.
+ let counts;
+ Svc.Obs.add("weave:engine:sync:applied", function observe(subject, data) {
+ Svc.Obs.remove("weave:engine:sync:applied", observe);
+ counts = subject;
+ });
+
+ engine._sync();
+ equal(counts.applied, 0); // We didn't report applying any records.
+ equal(counts.reconciled, 4); // We reported reconcilliation for all records
+ equal(counts.succeeded, 0);
+ equal(counts.failed, 0);
+ equal(counts.newFailed, 0);
+
+ _("Broadcast logout to all clients");
+ engine.sendCommand("logout", []);
+ engine._sync();
+
+ let collection = server.getCollection("foo", "clients");
+ let recentPayload = JSON.parse(JSON.parse(collection.payload(recentID)).ciphertext);
+ deepEqual(recentPayload.commands, [{ command: "logout", args: [] }],
+ "Should send commands to the recent client");
+
+ let oldPayload = JSON.parse(JSON.parse(collection.payload(oldID)).ciphertext);
+ deepEqual(oldPayload.commands, [{ command: "logout", args: [] }],
+ "Should send commands to the week-old client");
+
+ 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");
+ server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({
+ id: dupeID,
+ name: engine.localName,
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ _("Second sync.");
+ engine._sync();
+
+ deepEqual(Object.keys(store.getAllIDs()).sort(),
+ [recentID, oldID, dupeID, engine.localID].sort(),
+ "Stale client synced, so it should no longer be marked as a dupe");
+
+ ok(engine.remoteClientExists(dupeID), "Dupe ID should appear as it synced.");
+
+ // Recently synced dupe desktop should appear in .deviceTypes.
+ equal(engine.deviceTypes.get("desktop"), 3);
+
+ // Recently synced dupe desktop should now appear in stats
+ deepEqual(engine.stats, {
+ hasMobile: 1,
+ names: [engine.localName, "My Phone", engine.localName, "My old desktop"],
+ numClients: 4,
+ });
+
+ ok(engine.remoteClientExists(dupeID), "recently synced dupe ID should now exist");
+ equal(engine.remoteClients.length, 3, "recently synced dupe should now be in remoteClients");
+
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_command_sync() {
+ _("Ensure that commands are synced across clients.");
+
+ engine._store.wipe();
+ generateNewKeys(Service.collectionKeys);
+
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ new SyncTestingInfrastructure(server.server);
+
+ 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({
+ id: remoteId,
+ name: "Remote client",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), Date.now() / 1000));
+
+ try {
+ _("Syncing.");
+ engine._sync();
+
+ _("Checking remote record was downloaded.");
+ let clientRecord = engine._store._remoteClients[remoteId];
+ notEqual(clientRecord, undefined);
+ equal(clientRecord.commands.length, 0);
+
+ _("Send a command to the remote client.");
+ engine.sendCommand("wipeAll", []);
+ let clientCommands = engine._readCommands()[remoteId];
+ equal(clientCommands.length, 1);
+ engine._sync();
+
+ _("Checking record was uploaded.");
+ notEqual(clientWBO(engine.localID).payload, undefined);
+ ok(engine.lastRecordUpload > 0);
+
+ notEqual(clientWBO(remoteId).payload, undefined);
+
+ Svc.Prefs.set("client.GUID", remoteId);
+ engine._resetClient();
+ equal(engine.localID, remoteId);
+ _("Performing sync on resetted client.");
+ engine._sync();
+ notEqual(engine.localCommands, undefined);
+ equal(engine.localCommands.length, 1);
+
+ let command = engine.localCommands[0];
+ equal(command.command, "wipeAll");
+ equal(command.args.length, 0);
+
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+
+ try {
+ let collection = server.getCollection("foo", "clients");
+ collection.remove(remoteId);
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_send_uri_to_client_for_display() {
+ _("Ensure sendURIToClientForDisplay() sends command properly.");
+
+ let tracker = engine._tracker;
+ let store = engine._store;
+
+ let remoteId = Utils.makeGUID();
+ let rec = new ClientsRec("clients", remoteId);
+ rec.name = "remote";
+ store.create(rec);
+ let remoteRecord = store.createRecord(remoteId, "clients");
+
+ tracker.clearChangedIDs();
+ let initialScore = tracker.score;
+
+ let uri = "http://www.mozilla.org/";
+ let title = "Title of the Page";
+ engine.sendURIToClientForDisplay(uri, remoteId, title);
+
+ let newRecord = store._remoteClients[remoteId];
+
+ notEqual(newRecord, undefined);
+ let clientCommands = engine._readCommands()[remoteId];
+ equal(clientCommands.length, 1);
+
+ let command = clientCommands[0];
+ equal(command.command, "displayURI");
+ equal(command.args.length, 3);
+ equal(command.args[0], uri);
+ equal(command.args[1], engine.localID);
+ equal(command.args[2], title);
+
+ ok(tracker.score > initialScore);
+ ok(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
+
+ _("Ensure unknown client IDs result in exception.");
+ let unknownId = Utils.makeGUID();
+ let error;
+
+ try {
+ engine.sendURIToClientForDisplay(uri, unknownId);
+ } catch (ex) {
+ error = ex;
+ }
+
+ equal(error.message.indexOf("Unknown remote client ID: "), 0);
+
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ run_next_test();
+});
+
+add_test(function test_receive_display_uri() {
+ _("Ensure processing of received 'displayURI' commands works.");
+
+ // We don't set up WBOs and perform syncing because other tests verify
+ // the command API works as advertised. This saves us a little work.
+
+ let uri = "http://www.mozilla.org/";
+ let remoteId = Utils.makeGUID();
+ let title = "Page Title!";
+
+ let command = {
+ command: "displayURI",
+ args: [uri, remoteId, title],
+ };
+
+ engine.localCommands = [command];
+
+ // Received 'displayURI' command should result in the topic defined below
+ // being called.
+ let ev = "weave:engine:clients:display-uris";
+
+ let handler = function(subject, data) {
+ Svc.Obs.remove(ev, handler);
+
+ equal(subject[0].uri, uri);
+ equal(subject[0].clientId, remoteId);
+ equal(subject[0].title, title);
+ equal(data, null);
+
+ run_next_test();
+ };
+
+ Svc.Obs.add(ev, handler);
+
+ ok(engine.processIncomingCommands());
+
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+});
+
+add_test(function test_optional_client_fields() {
+ _("Ensure that we produce records with the fields added in Bug 1097222.");
+
+ const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
+ let local = engine._store.createRecord(engine.localID, "clients");
+ equal(local.name, engine.localName);
+ equal(local.type, engine.localType);
+ equal(local.version, Services.appinfo.version);
+ deepEqual(local.protocols, SUPPORTED_PROTOCOL_VERSIONS);
+
+ // Optional fields.
+ // Make sure they're what they ought to be...
+ equal(local.os, Services.appinfo.OS);
+ equal(local.appPackage, Services.appinfo.ID);
+
+ // ... and also that they're non-empty.
+ ok(!!local.os);
+ ok(!!local.appPackage);
+ ok(!!local.application);
+
+ // We don't currently populate device or formfactor.
+ // See Bug 1100722, Bug 1100723.
+
+ engine._resetClient();
+ run_next_test();
+});
+
+add_test(function test_merge_commands() {
+ _("Verifies local commands for remote clients are merged with the server's");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let desktopID = Utils.makeGUID();
+ 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"],
+ }],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ let mobileID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(mobileID, encryptPayload({
+ id: mobileID,
+ name: "Mobile client",
+ type: "mobile",
+ commands: [{
+ command: "logout",
+ args: [],
+ }],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ try {
+ let store = engine._store;
+
+ _("First sync. 2 records downloaded.");
+ strictEqual(engine.lastRecordUpload, 0);
+ engine._sync();
+
+ _("Broadcast logout to all clients");
+ engine.sendCommand("logout", []);
+ engine._sync();
+
+ let collection = server.getCollection("foo", "clients");
+ let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
+ deepEqual(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);
+ deepEqual(mobilePayload.commands, [{ command: "logout", args: [] }],
+ "Should not send a duplicate logout to the mobile client");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_duplicate_remote_commands() {
+ _("Verifies local commands for remote clients are sent only once (bug 1289287)");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let desktopID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
+ id: desktopID,
+ name: "Desktop client",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ try {
+ let store = engine._store;
+
+ _("First sync. 1 record downloaded.");
+ strictEqual(engine.lastRecordUpload, 0);
+ engine._sync();
+
+ _("Send tab to client");
+ engine.sendCommand("displayURI", ["https://example.com", engine.localID, "Yak Herders Anonymous"]);
+ engine._sync();
+
+ _("Simulate the desktop client consuming the command and syncing to the server");
+ server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
+ id: desktopID,
+ name: "Desktop client",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ _("Send another tab to the desktop client");
+ engine.sendCommand("displayURI", ["https://foobar.com", engine.localID, "Foo bar!"], desktopID);
+ engine._sync();
+
+ let collection = server.getCollection("foo", "clients");
+ let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext);
+ deepEqual(desktopPayload.commands, [{
+ command: "displayURI",
+ args: ["https://foobar.com", engine.localID, "Foo bar!"],
+ }], "Should only send the second command to the desktop client");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_upload_after_reboot() {
+ _("Multiple downloads, reboot, then upload (bug 1289287)");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let deviceBID = Utils.makeGUID();
+ let deviceCID = Utils.makeGUID();
+ 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"]
+ }],
+ version: "48",
+ protocols: ["1.5"],
+ }), 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));
+
+ try {
+ let store = engine._store;
+
+ _("First sync. 2 records downloaded.");
+ strictEqual(engine.lastRecordUpload, 0);
+ engine._sync();
+
+ _("Send tab to client");
+ engine.sendCommand("displayURI", ["https://example.com", engine.localID, "Yak Herders Anonymous"], deviceBID);
+
+ const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
+ SyncEngine.prototype._uploadOutgoing = () => engine._onRecordsWritten.call(engine, [], [deviceBID]);
+ engine._sync();
+
+ let collection = server.getCollection("foo", "clients");
+ let deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+ deepEqual(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({
+ id: deviceBID,
+ name: "Device B",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ // Simulate reboot
+ SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
+ engine = Service.clientsEngine = new ClientEngine(Service);
+
+ engine._sync();
+
+ deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+ deepEqual(deviceBPayload.commands, [{
+ command: "displayURI",
+ args: ["https://example.com", engine.localID, "Yak Herders Anonymous"],
+ }], "Should only had written our outgoing command");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_keep_cleared_commands_after_reboot() {
+ _("Download commands, fail upload, reboot, then apply new commands (bug 1289287)");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let deviceBID = Utils.makeGUID();
+ let deviceCID = Utils.makeGUID();
+ 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"]
+ },
+ {
+ command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"]
+ }],
+ version: "48",
+ protocols: ["1.5"],
+ }), 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));
+ server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({
+ id: deviceCID,
+ name: "Device C",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ try {
+ let store = engine._store;
+
+ _("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 = () => engine._onRecordsWritten.call(engine, [], [deviceBID]);
+ let commandsProcessed = 0;
+ engine._handleDisplayURIs = (uris) => { commandsProcessed = uris.length };
+
+ engine._sync();
+ 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);
+ deepEqual(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({
+ id: engine.localID,
+ name: "Device A",
+ type: "desktop",
+ commands: [{
+ command: "displayURI", args: ["https://deviceblink.com", deviceBID, "Device B link"]
+ },
+ {
+ command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"]
+ },
+ {
+ command: "displayURI", args: ["https://deviceclink2.com", deviceCID, "Device C link 2"]
+ }],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ // Simulate reboot
+ SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
+ engine = Service.clientsEngine = new ClientEngine(Service);
+
+ commandsProcessed = 0;
+ engine._handleDisplayURIs = (uris) => { commandsProcessed = uris.length };
+ engine._sync();
+ engine.processIncomingCommands();
+ equal(commandsProcessed, 1, "We processed one command (the other were cleared)");
+
+ localRemoteRecord = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext);
+ deepEqual(localRemoteRecord.commands, [], "Should be empty");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+
+ // Reset service (remove mocks)
+ engine = Service.clientsEngine = new ClientEngine(Service);
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_deleted_commands() {
+ _("Verifies commands for a deleted client are discarded");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ let activeID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
+ id: activeID,
+ name: "Active client",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ let deletedID = Utils.makeGUID();
+ server.insertWBO("foo", "clients", new ServerWBO(deletedID, encryptPayload({
+ id: deletedID,
+ name: "Client to delete",
+ type: "desktop",
+ commands: [],
+ version: "48",
+ protocols: ["1.5"],
+ }), now - 10));
+
+ try {
+ let store = engine._store;
+
+ _("First sync. 2 records downloaded.");
+ engine._sync();
+
+ _("Delete a record on the server.");
+ let collection = server.getCollection("foo", "clients");
+ collection.remove(deletedID);
+
+ _("Broadcast a command to all clients");
+ engine.sendCommand("logout", []);
+ engine._sync();
+
+ deepEqual(collection.keys().sort(), [activeID, engine.localID].sort(),
+ "Should not reupload deleted clients");
+
+ let activePayload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext);
+ deepEqual(activePayload.commands, [{ command: "logout", args: [] }],
+ "Should send the command to the active client");
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_send_uri_ack() {
+ _("Ensure a sent URI is deleted when the client syncs");
+
+ let now = Date.now() / 1000;
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ let user = server.user("foo");
+
+ new SyncTestingInfrastructure(server.server);
+ generateNewKeys(Service.collectionKeys);
+
+ try {
+ let fakeSenderID = Utils.makeGUID();
+
+ _("Initial sync for empty clients collection");
+ engine._sync();
+ 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"],
+ }];
+ server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload(ourPayload), now));
+
+ _("Sync again");
+ engine._sync();
+ deepEqual(engine.localCommands, [{
+ command: "displayURI",
+ args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"],
+ }], "Should receive incoming URI");
+ ok(engine.processIncomingCommands(), "Should process incoming commands");
+ const clearedCommands = engine._readCommands()[engine.localID];
+ deepEqual(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");
+ engine._sync();
+ 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 {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ engine._resetClient();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+add_test(function test_command_sync() {
+ _("Notify other clients when writing their record.");
+
+ engine._store.wipe();
+ generateNewKeys(Service.collectionKeys);
+
+ let contents = {
+ meta: {global: {engines: {clients: {version: engine.version,
+ syncID: engine.syncID}}}},
+ clients: {},
+ crypto: {}
+ };
+ let server = serverForUsers({"foo": "password"}, contents);
+ new SyncTestingInfrastructure(server.server);
+
+ let user = server.user("foo");
+ let collection = server.getCollection("foo", "clients");
+ let remoteId = Utils.makeGUID();
+ let remoteId2 = Utils.makeGUID();
+
+ function clientWBO(id) {
+ return user.collection("clients").wbo(id);
+ }
+
+ _("Create remote client record 1");
+ 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");
+ 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");
+ engine._sync();
+ equal(collection.count(), 3, "3 remote records written (+1 for the synced local record)");
+
+ let notifiedIds;
+ engine.sendCommand("wipeAll", []);
+ engine._tracker.addChangedID(engine.localID);
+ engine.getClientFxaDeviceId = (id) => "fxa-" + id;
+ engine._notifyCollectionChanged = (ids) => (notifiedIds = ids);
+ _("Syncing.");
+ engine._sync();
+ deepEqual(notifiedIds, ["fxa-fake-guid-00","fxa-fake-guid-01"]);
+ ok(!notifiedIds.includes(engine.getClientFxaDeviceId(engine.localID)),
+ "We never notify the local device");
+
+ } finally {
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+
+ try {
+ server.deleteCollections("foo");
+ } finally {
+ server.stop(run_next_test);
+ }
+ }
+});
+
+function run_test() {
+ initTestLogging("Trace");
+ Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
+ run_next_test();
+}