Files
UXP-Fixed/services/sync/tests/unit/test_clients_engine.js
T
2018-02-02 04:16:08 -05:00

1440 lines
43 KiB
JavaScript

/* 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();
}