gecko-dev/services/sync/tests/unit/test_clients_engine.js

538 lines
17 KiB
JavaScript

Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://services-sync/service.js");
const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
add_test(function test_bad_hmac() {
_("Ensure that Clients engine deletes corrupt records.");
let global = new ServerWBO('global',
{engines: {clients: {version: Clients.version,
syncID: Clients.syncID}}});
let clientsColl = new ServerCollection({}, true);
let keysWBO = new ServerWBO("keys");
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let collections = collectionsHelper.collections;
// Watch for deletions in the given collection.
let deleted = false;
function trackDeletedHandler(coll, handler) {
let u = upd(coll, handler);
return function(request, response) {
if (request.method == "DELETE")
deleted = true;
return u(request, response);
};
}
let handlers = {
"/1.1/foo/info/collections": collectionsHelper.handler,
"/1.1/foo/storage/meta/global": upd("meta", global.handler()),
"/1.1/foo/storage/crypto/keys": upd("crypto", keysWBO.handler()),
"/1.1/foo/storage/clients": trackDeletedHandler("clients", clientsColl.handler())
};
let server = httpd_setup(handlers);
try {
let passphrase = "abcdeabcdeabcdeabcdeabcdea";
Service.serverURL = "http://localhost:8080/";
Service.clusterURL = "http://localhost:8080/";
Service.login("foo", "ilovejane", passphrase);
generateNewKeys();
_("First sync, client record is uploaded");
do_check_eq(0, clientsColl.count());
do_check_eq(Clients.lastRecordUpload, 0);
Clients.sync();
do_check_eq(1, clientsColl.count());
do_check_true(Clients.lastRecordUpload > 0);
deleted = false; // Initial setup can wipe the server, so clean up.
_("Records now: " + clientsColl.get({}));
_("Change our keys and our client ID, reupload keys.");
Clients.localID = Utils.makeGUID();
Clients.resetClient();
generateNewKeys();
let serverKeys = CollectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Weave.Service.syncKeyBundle);
do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
_("Sync.");
do_check_true(!deleted);
Clients.sync();
_("Old record was deleted, new one uploaded.");
do_check_true(deleted);
do_check_eq(1, clientsColl.count());
_("Records now: " + clientsColl.get({}));
_("Now change our keys but don't upload them. " +
"That means we get an HMAC error but redownload keys.");
Service.lastHMACEvent = 0;
Clients.localID = Utils.makeGUID();
Clients.resetClient();
generateNewKeys();
deleted = false;
do_check_eq(1, clientsColl.count());
Clients.sync();
_("Old record was not deleted, new one uploaded.");
do_check_false(deleted);
do_check_eq(2, clientsColl.count());
_("Records now: " + clientsColl.get({}));
_("Now try the scenario where our keys are wrong *and* there's a bad record.");
// Clean up and start fresh.
clientsColl.wbos = {};
Service.lastHMACEvent = 0;
Clients.localID = Utils.makeGUID();
Clients.resetClient();
deleted = false;
do_check_eq(0, clientsColl.count());
// Create and upload keys.
generateNewKeys();
serverKeys = CollectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Weave.Service.syncKeyBundle);
do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
// Sync once to upload a record.
Clients.sync();
do_check_eq(1, clientsColl.count());
// Generate and upload new keys, so the old client record is wrong.
generateNewKeys();
serverKeys = CollectionKeys.asWBO("crypto", "keys");
serverKeys.encrypt(Weave.Service.syncKeyBundle);
do_check_true(serverKeys.upload(Weave.Service.cryptoKeysURL).success);
// 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.
Clients.localID = Utils.makeGUID();
Clients.resetClient();
generateNewKeys();
let oldKey = CollectionKeys.keyForCollection();
do_check_false(deleted);
Clients.sync();
do_check_true(deleted);
do_check_eq(1, clientsColl.count());
let newKey = CollectionKeys.keyForCollection();
do_check_false(oldKey.equals(newKey));
} finally {
Svc.Prefs.resetBranch("");
Records.clearCache();
server.stop(run_next_test);
}
});
add_test(function test_properties() {
_("Test lastRecordUpload property");
try {
do_check_eq(Svc.Prefs.get("clients.lastRecordUpload"), undefined);
do_check_eq(Clients.lastRecordUpload, 0);
let now = Date.now();
Clients.lastRecordUpload = now / 1000;
do_check_eq(Clients.lastRecordUpload, Math.floor(now / 1000));
} finally {
Svc.Prefs.resetBranch("");
run_next_test();
}
});
add_test(function test_sync() {
_("Ensure that Clients engine uploads a new client record once a week.");
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
Svc.Prefs.set("username", "foo");
generateNewKeys();
let global = new ServerWBO('global',
{engines: {clients: {version: Clients.version,
syncID: Clients.syncID}}});
let coll = new ServerCollection();
let clientwbo = coll.wbos[Clients.localID] = new ServerWBO(Clients.localID);
let server = httpd_setup({
"/1.1/foo/storage/meta/global": global.handler(),
"/1.1/foo/storage/clients": coll.handler()
});
server.registerPathHandler(
"/1.1/foo/storage/clients/" + Clients.localID, clientwbo.handler());
try {
_("First sync, client record is uploaded");
do_check_eq(clientwbo.payload, undefined);
do_check_eq(Clients.lastRecordUpload, 0);
Clients.sync();
do_check_true(!!clientwbo.payload);
do_check_true(Clients.lastRecordUpload > 0);
_("Let's time travel more than a week back, new record should've been uploaded.");
Clients.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
let lastweek = Clients.lastRecordUpload;
clientwbo.payload = undefined;
Clients.sync();
do_check_true(!!clientwbo.payload);
do_check_true(Clients.lastRecordUpload > lastweek);
_("Remove client record.");
Clients.removeClientData();
do_check_eq(clientwbo.payload, undefined);
_("Time travel one day back, no record uploaded.");
Clients.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
let yesterday = Clients.lastRecordUpload;
Clients.sync();
do_check_eq(clientwbo.payload, undefined);
do_check_eq(Clients.lastRecordUpload, yesterday);
} finally {
Svc.Prefs.resetBranch("");
Records.clearCache();
server.stop(run_next_test);
}
});
add_test(function test_client_name_change() {
_("Ensure client name change incurs a client record update.");
let tracker = Clients._tracker;
let localID = Clients.localID;
let initialName = Clients.localName;
Svc.Obs.notify("weave:engine:start-tracking");
_("initial name: " + initialName);
// Tracker already has data, so clear it.
tracker.clearChangedIDs();
let initialScore = tracker.score;
do_check_eq(Object.keys(tracker.changedIDs).length, 0);
Svc.Prefs.set("client.name", "new name");
_("new name: " + Clients.localName);
do_check_neq(initialName, Clients.localName);
do_check_eq(Object.keys(tracker.changedIDs).length, 1);
do_check_true(Clients.localID in tracker.changedIDs);
do_check_true(tracker.score > initialScore);
do_check_true(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 = Clients._store;
let tracker = Clients._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"];
Clients._sendCommandToClient(action, args, remoteId);
let newRecord = store._remoteClients[remoteId];
do_check_neq(newRecord, undefined);
do_check_eq(newRecord.commands.length, 1);
let command = newRecord.commands[0];
do_check_eq(command.command, action);
do_check_eq(command.args.length, 2);
do_check_eq(command.args, args);
do_check_neq(tracker.changedIDs[remoteId], undefined);
run_next_test();
});
add_test(function test_command_validation() {
_("Verifies that command validation works properly.");
let store = Clients._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 each (let [action, args, expectedResult] in testCommands) {
let remoteId = Utils.makeGUID();
let rec = new ClientsRec("clients", remoteId);
store.create(rec);
store.createRecord(remoteId, "clients");
Clients.sendCommand(action, args, remoteId);
let newRecord = store._remoteClients[remoteId];
do_check_neq(newRecord, undefined);
if (expectedResult) {
_("Ensuring command is sent: " + action);
do_check_eq(newRecord.commands.length, 1);
let command = newRecord.commands[0];
do_check_eq(command.command, action);
do_check_eq(command.args, args);
do_check_neq(Clients._tracker, undefined);
do_check_neq(Clients._tracker.changedIDs[remoteId], undefined);
} else {
_("Ensuring command is scrubbed: " + action);
do_check_eq(newRecord.commands, undefined);
if (store._tracker) {
do_check_eq(Clients._tracker[remoteId], undefined);
}
}
}
run_next_test();
});
add_test(function test_command_duplication() {
_("Ensures duplicate commands are detected and not added");
let store = Clients._store;
let remoteId = Utils.makeGUID();
let rec = new ClientsRec("clients", remoteId);
store.create(rec);
store.createRecord(remoteId, "clients");
let action = "resetAll";
let args = [];
Clients.sendCommand(action, args, remoteId);
Clients.sendCommand(action, args, remoteId);
let newRecord = store._remoteClients[remoteId];
do_check_eq(newRecord.commands.length, 1);
_("Check variant args length");
newRecord.commands = [];
action = "resetEngine";
Clients.sendCommand(action, [{ x: "foo" }], remoteId);
Clients.sendCommand(action, [{ x: "bar" }], remoteId);
_("Make sure we spot a real dupe argument.");
Clients.sendCommand(action, [{ x: "bar" }], remoteId);
do_check_eq(newRecord.commands.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 {
Clients.sendCommand("wipeAll", [], id);
} catch (ex) {
error = ex;
}
do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
run_next_test();
});
add_test(function test_process_incoming_commands() {
_("Ensures local commands are executed");
Clients.localCommands = [{ command: "logout", args: [] }];
let ev = "weave:service:logout:finish";
var handler = function() {
Svc.Obs.remove(ev, handler);
run_next_test();
};
Svc.Obs.add(ev, handler);
// logout command causes processIncomingCommands to return explicit false.
do_check_false(Clients.processIncomingCommands());
});
add_test(function test_command_sync() {
_("Ensure that commands are synced across clients.");
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
Svc.Prefs.set("username", "foo");
generateNewKeys();
let global = new ServerWBO('global',
{engines: {clients: {version: Clients.version,
syncID: Clients.syncID}}});
let coll = new ServerCollection();
let clientwbo = coll.wbos[Clients.localID] = new ServerWBO(Clients.localID);
let server = httpd_setup({
"/1.1/foo/storage/meta/global": global.handler(),
"/1.1/foo/storage/clients": coll.handler()
});
let remoteId = Utils.makeGUID();
let remotewbo = coll.wbos[remoteId] = new ServerWBO(remoteId);
server.registerPathHandler(
"/1.1/foo/storage/clients/" + Clients.localID, clientwbo.handler());
server.registerPathHandler(
"/1.1/foo/storage/clients/" + remoteId, remotewbo.handler());
_("Create remote client record");
let rec = new ClientsRec("clients", remoteId);
Clients._store.create(rec);
let remoteRecord = Clients._store.createRecord(remoteId, "clients");
Clients.sendCommand("wipeAll", []);
let clientRecord = Clients._store._remoteClients[remoteId];
do_check_neq(clientRecord, undefined);
do_check_eq(clientRecord.commands.length, 1);
try {
Clients.sync();
do_check_neq(clientwbo.payload, undefined);
do_check_true(Clients.lastRecordUpload > 0);
do_check_neq(remotewbo.payload, undefined);
Svc.Prefs.set("client.GUID", remoteId);
Clients._resetClient();
do_check_eq(Clients.localID, remoteId);
Clients.sync();
do_check_neq(Clients.localCommands, undefined);
do_check_eq(Clients.localCommands.length, 1);
let command = Clients.localCommands[0];
do_check_eq(command.command, "wipeAll");
do_check_eq(command.args.length, 0);
} finally {
Svc.Prefs.resetBranch("");
Records.clearCache();
server.stop(run_next_test);
}
});
add_test(function test_send_uri_to_client_for_display() {
_("Ensure sendURIToClientForDisplay() sends command properly.");
let tracker = Clients._tracker;
let store = Clients._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/";
Clients.sendURIToClientForDisplay(uri, remoteId);
let newRecord = store._remoteClients[remoteId];
do_check_neq(newRecord, undefined);
do_check_eq(newRecord.commands.length, 1);
let command = newRecord.commands[0];
do_check_eq(command.command, "displayURI");
do_check_eq(command.args.length, 2);
do_check_eq(command.args[0], uri);
do_check_true(tracker.score > initialScore);
do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
_("Ensure unknown client IDs result in exception.");
let unknownId = Utils.makeGUID();
let error;
try {
Clients.sendURIToClientForDisplay(uri, unknownId);
} catch (ex) {
error = ex;
}
do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
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 command = {
command: "displayURI",
args: [uri, remoteId],
};
Clients.localCommands = [command];
// Received 'displayURI' command should result in the topic defined below
// being called.
let ev = "weave:engine:clients:display-uri";
let handler = function(subject, data) {
Svc.Obs.remove(ev, handler);
do_check_eq(subject.uri, uri);
do_check_eq(subject.client, remoteId);
do_check_eq(data, null);
run_next_test();
};
Svc.Obs.add(ev, handler);
do_check_true(Clients.processIncomingCommands());
});
function run_test() {
initTestLogging("Trace");
Log4Moz.repository.getLogger("Sync.Engine.Clients").level = Log4Moz.Level.Trace;
run_next_test();
}