michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/engines/clients.js"); michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: michael@0: const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days michael@0: const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day michael@0: michael@0: let engine = Service.clientsEngine; michael@0: michael@0: /** michael@0: * Unpack the record with this ID, and verify that it has the same version that michael@0: * we should be putting into records. michael@0: */ michael@0: function check_record_version(user, id) { michael@0: let payload = JSON.parse(user.collection("clients").wbo(id).payload); michael@0: michael@0: let rec = new CryptoWrapper(); michael@0: rec.id = id; michael@0: rec.collection = "clients"; michael@0: rec.ciphertext = payload.ciphertext; michael@0: rec.hmac = payload.hmac; michael@0: rec.IV = payload.IV; michael@0: michael@0: let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients")); michael@0: michael@0: _("Payload is " + JSON.stringify(cleartext)); michael@0: do_check_eq(Services.appinfo.version, cleartext.version); michael@0: do_check_eq(2, cleartext.protocols.length); michael@0: do_check_eq("1.1", cleartext.protocols[0]); michael@0: do_check_eq("1.5", cleartext.protocols[1]); michael@0: } michael@0: michael@0: add_test(function test_bad_hmac() { michael@0: _("Ensure that Clients engine deletes corrupt records."); michael@0: let contents = { michael@0: meta: {global: {engines: {clients: {version: engine.version, michael@0: syncID: engine.syncID}}}}, michael@0: clients: {}, michael@0: crypto: {} michael@0: }; michael@0: let deletedCollections = []; michael@0: let deletedItems = []; michael@0: let callback = { michael@0: __proto__: SyncServerCallback, michael@0: onItemDeleted: function (username, coll, wboID) { michael@0: deletedItems.push(coll + "/" + wboID); michael@0: }, michael@0: onCollectionDeleted: function (username, coll) { michael@0: deletedCollections.push(coll); michael@0: } michael@0: } michael@0: let server = serverForUsers({"foo": "password"}, contents, callback); michael@0: let user = server.user("foo"); michael@0: michael@0: function check_clients_count(expectedCount) { michael@0: let stack = Components.stack.caller; michael@0: let coll = user.collection("clients"); michael@0: michael@0: // Treat a non-existent collection as empty. michael@0: do_check_eq(expectedCount, coll ? coll.count() : 0, stack); michael@0: } michael@0: michael@0: function check_client_deleted(id) { michael@0: let coll = user.collection("clients"); michael@0: let wbo = coll.wbo(id); michael@0: return !wbo || !wbo.payload; michael@0: } michael@0: michael@0: function uploadNewKeys() { michael@0: generateNewKeys(Service.collectionKeys); michael@0: let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); michael@0: serverKeys.encrypt(Service.identity.syncKeyBundle); michael@0: do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success); michael@0: } michael@0: michael@0: try { michael@0: ensureLegacyIdentityManager(); michael@0: let passphrase = "abcdeabcdeabcdeabcdeabcdea"; michael@0: Service.serverURL = server.baseURI; michael@0: Service.login("foo", "ilovejane", passphrase); michael@0: michael@0: generateNewKeys(Service.collectionKeys); michael@0: michael@0: _("First sync, client record is uploaded"); michael@0: do_check_eq(engine.lastRecordUpload, 0); michael@0: check_clients_count(0); michael@0: engine._sync(); michael@0: check_clients_count(1); michael@0: do_check_true(engine.lastRecordUpload > 0); michael@0: michael@0: // Our uploaded record has a version. michael@0: check_record_version(user, engine.localID); michael@0: michael@0: // Initial setup can wipe the server, so clean up. michael@0: deletedCollections = []; michael@0: deletedItems = []; michael@0: michael@0: _("Change our keys and our client ID, reupload keys."); michael@0: let oldLocalID = engine.localID; // Preserve to test for deletion! michael@0: engine.localID = Utils.makeGUID(); michael@0: engine.resetClient(); michael@0: generateNewKeys(Service.collectionKeys); michael@0: let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); michael@0: serverKeys.encrypt(Service.identity.syncKeyBundle); michael@0: do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success); michael@0: michael@0: _("Sync."); michael@0: engine._sync(); michael@0: michael@0: _("Old record " + oldLocalID + " was deleted, new one uploaded."); michael@0: check_clients_count(1); michael@0: check_client_deleted(oldLocalID); michael@0: michael@0: _("Now change our keys but don't upload them. " + michael@0: "That means we get an HMAC error but redownload keys."); michael@0: Service.lastHMACEvent = 0; michael@0: engine.localID = Utils.makeGUID(); michael@0: engine.resetClient(); michael@0: generateNewKeys(Service.collectionKeys); michael@0: deletedCollections = []; michael@0: deletedItems = []; michael@0: check_clients_count(1); michael@0: engine._sync(); michael@0: michael@0: _("Old record was not deleted, new one uploaded."); michael@0: do_check_eq(deletedCollections.length, 0); michael@0: do_check_eq(deletedItems.length, 0); michael@0: check_clients_count(2); michael@0: michael@0: _("Now try the scenario where our keys are wrong *and* there's a bad record."); michael@0: // Clean up and start fresh. michael@0: user.collection("clients")._wbos = {}; michael@0: Service.lastHMACEvent = 0; michael@0: engine.localID = Utils.makeGUID(); michael@0: engine.resetClient(); michael@0: deletedCollections = []; michael@0: deletedItems = []; michael@0: check_clients_count(0); michael@0: michael@0: uploadNewKeys(); michael@0: michael@0: // Sync once to upload a record. michael@0: engine._sync(); michael@0: check_clients_count(1); michael@0: michael@0: // Generate and upload new keys, so the old client record is wrong. michael@0: uploadNewKeys(); michael@0: michael@0: // Create a new client record and new keys. Now our keys are wrong, as well michael@0: // as the object on the server. We'll download the new keys and also delete michael@0: // the bad client record. michael@0: oldLocalID = engine.localID; // Preserve to test for deletion! michael@0: engine.localID = Utils.makeGUID(); michael@0: engine.resetClient(); michael@0: generateNewKeys(Service.collectionKeys); michael@0: let oldKey = Service.collectionKeys.keyForCollection(); michael@0: michael@0: do_check_eq(deletedCollections.length, 0); michael@0: do_check_eq(deletedItems.length, 0); michael@0: engine._sync(); michael@0: do_check_eq(deletedItems.length, 1); michael@0: check_client_deleted(oldLocalID); michael@0: check_clients_count(1); michael@0: let newKey = Service.collectionKeys.keyForCollection(); michael@0: do_check_false(oldKey.equals(newKey)); michael@0: michael@0: } finally { michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_properties() { michael@0: _("Test lastRecordUpload property"); michael@0: try { michael@0: do_check_eq(Svc.Prefs.get("clients.lastRecordUpload"), undefined); michael@0: do_check_eq(engine.lastRecordUpload, 0); michael@0: michael@0: let now = Date.now(); michael@0: engine.lastRecordUpload = now / 1000; michael@0: do_check_eq(engine.lastRecordUpload, Math.floor(now / 1000)); michael@0: } finally { michael@0: Svc.Prefs.resetBranch(""); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_sync() { michael@0: _("Ensure that Clients engine uploads a new client record once a week."); michael@0: michael@0: let contents = { michael@0: meta: {global: {engines: {clients: {version: engine.version, michael@0: syncID: engine.syncID}}}}, michael@0: clients: {}, michael@0: crypto: {} michael@0: }; michael@0: let server = serverForUsers({"foo": "password"}, contents); michael@0: let user = server.user("foo"); michael@0: michael@0: new SyncTestingInfrastructure(server.server); michael@0: generateNewKeys(Service.collectionKeys); michael@0: michael@0: function clientWBO() { michael@0: return user.collection("clients").wbo(engine.localID); michael@0: } michael@0: michael@0: try { michael@0: michael@0: _("First sync. Client record is uploaded."); michael@0: do_check_eq(clientWBO(), undefined); michael@0: do_check_eq(engine.lastRecordUpload, 0); michael@0: engine._sync(); michael@0: do_check_true(!!clientWBO().payload); michael@0: do_check_true(engine.lastRecordUpload > 0); michael@0: michael@0: _("Let's time travel more than a week back, new record should've been uploaded."); michael@0: engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH; michael@0: let lastweek = engine.lastRecordUpload; michael@0: clientWBO().payload = undefined; michael@0: engine._sync(); michael@0: do_check_true(!!clientWBO().payload); michael@0: do_check_true(engine.lastRecordUpload > lastweek); michael@0: michael@0: _("Remove client record."); michael@0: engine.removeClientData(); michael@0: do_check_eq(clientWBO().payload, undefined); michael@0: michael@0: _("Time travel one day back, no record uploaded."); michael@0: engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH; michael@0: let yesterday = engine.lastRecordUpload; michael@0: engine._sync(); michael@0: do_check_eq(clientWBO().payload, undefined); michael@0: do_check_eq(engine.lastRecordUpload, yesterday); michael@0: michael@0: } finally { michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_client_name_change() { michael@0: _("Ensure client name change incurs a client record update."); michael@0: michael@0: let tracker = engine._tracker; michael@0: michael@0: let localID = engine.localID; michael@0: let initialName = engine.localName; michael@0: michael@0: Svc.Obs.notify("weave:engine:start-tracking"); michael@0: _("initial name: " + initialName); michael@0: michael@0: // Tracker already has data, so clear it. michael@0: tracker.clearChangedIDs(); michael@0: michael@0: let initialScore = tracker.score; michael@0: michael@0: do_check_eq(Object.keys(tracker.changedIDs).length, 0); michael@0: michael@0: Svc.Prefs.set("client.name", "new name"); michael@0: michael@0: _("new name: " + engine.localName); michael@0: do_check_neq(initialName, engine.localName); michael@0: do_check_eq(Object.keys(tracker.changedIDs).length, 1); michael@0: do_check_true(engine.localID in tracker.changedIDs); michael@0: do_check_true(tracker.score > initialScore); michael@0: do_check_true(tracker.score >= SCORE_INCREMENT_XLARGE); michael@0: michael@0: Svc.Obs.notify("weave:engine:stop-tracking"); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_send_command() { michael@0: _("Verifies _sendCommandToClient puts commands in the outbound queue."); michael@0: michael@0: let store = engine._store; michael@0: let tracker = engine._tracker; michael@0: let remoteId = Utils.makeGUID(); michael@0: let rec = new ClientsRec("clients", remoteId); michael@0: michael@0: store.create(rec); michael@0: let remoteRecord = store.createRecord(remoteId, "clients"); michael@0: michael@0: let action = "testCommand"; michael@0: let args = ["foo", "bar"]; michael@0: michael@0: engine._sendCommandToClient(action, args, remoteId); michael@0: michael@0: let newRecord = store._remoteClients[remoteId]; michael@0: do_check_neq(newRecord, undefined); michael@0: do_check_eq(newRecord.commands.length, 1); michael@0: michael@0: let command = newRecord.commands[0]; michael@0: do_check_eq(command.command, action); michael@0: do_check_eq(command.args.length, 2); michael@0: do_check_eq(command.args, args); michael@0: michael@0: do_check_neq(tracker.changedIDs[remoteId], undefined); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_command_validation() { michael@0: _("Verifies that command validation works properly."); michael@0: michael@0: let store = engine._store; michael@0: michael@0: let testCommands = [ michael@0: ["resetAll", [], true ], michael@0: ["resetAll", ["foo"], false], michael@0: ["resetEngine", ["tabs"], true ], michael@0: ["resetEngine", [], false], michael@0: ["wipeAll", [], true ], michael@0: ["wipeAll", ["foo"], false], michael@0: ["wipeEngine", ["tabs"], true ], michael@0: ["wipeEngine", [], false], michael@0: ["logout", [], true ], michael@0: ["logout", ["foo"], false], michael@0: ["__UNKNOWN__", [], false] michael@0: ]; michael@0: michael@0: for each (let [action, args, expectedResult] in testCommands) { michael@0: let remoteId = Utils.makeGUID(); michael@0: let rec = new ClientsRec("clients", remoteId); michael@0: michael@0: store.create(rec); michael@0: store.createRecord(remoteId, "clients"); michael@0: michael@0: engine.sendCommand(action, args, remoteId); michael@0: michael@0: let newRecord = store._remoteClients[remoteId]; michael@0: do_check_neq(newRecord, undefined); michael@0: michael@0: if (expectedResult) { michael@0: _("Ensuring command is sent: " + action); michael@0: do_check_eq(newRecord.commands.length, 1); michael@0: michael@0: let command = newRecord.commands[0]; michael@0: do_check_eq(command.command, action); michael@0: do_check_eq(command.args, args); michael@0: michael@0: do_check_neq(engine._tracker, undefined); michael@0: do_check_neq(engine._tracker.changedIDs[remoteId], undefined); michael@0: } else { michael@0: _("Ensuring command is scrubbed: " + action); michael@0: do_check_eq(newRecord.commands, undefined); michael@0: michael@0: if (store._tracker) { michael@0: do_check_eq(engine._tracker[remoteId], undefined); michael@0: } michael@0: } michael@0: michael@0: } michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_command_duplication() { michael@0: _("Ensures duplicate commands are detected and not added"); michael@0: michael@0: let store = engine._store; michael@0: let remoteId = Utils.makeGUID(); michael@0: let rec = new ClientsRec("clients", remoteId); michael@0: store.create(rec); michael@0: store.createRecord(remoteId, "clients"); michael@0: michael@0: let action = "resetAll"; michael@0: let args = []; michael@0: michael@0: engine.sendCommand(action, args, remoteId); michael@0: engine.sendCommand(action, args, remoteId); michael@0: michael@0: let newRecord = store._remoteClients[remoteId]; michael@0: do_check_eq(newRecord.commands.length, 1); michael@0: michael@0: _("Check variant args length"); michael@0: newRecord.commands = []; michael@0: michael@0: action = "resetEngine"; michael@0: engine.sendCommand(action, [{ x: "foo" }], remoteId); michael@0: engine.sendCommand(action, [{ x: "bar" }], remoteId); michael@0: michael@0: _("Make sure we spot a real dupe argument."); michael@0: engine.sendCommand(action, [{ x: "bar" }], remoteId); michael@0: michael@0: do_check_eq(newRecord.commands.length, 2); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_command_invalid_client() { michael@0: _("Ensures invalid client IDs are caught"); michael@0: michael@0: let id = Utils.makeGUID(); michael@0: let error; michael@0: michael@0: try { michael@0: engine.sendCommand("wipeAll", [], id); michael@0: } catch (ex) { michael@0: error = ex; michael@0: } michael@0: michael@0: do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_process_incoming_commands() { michael@0: _("Ensures local commands are executed"); michael@0: michael@0: engine.localCommands = [{ command: "logout", args: [] }]; michael@0: michael@0: let ev = "weave:service:logout:finish"; michael@0: michael@0: var handler = function() { michael@0: Svc.Obs.remove(ev, handler); michael@0: run_next_test(); michael@0: }; michael@0: michael@0: Svc.Obs.add(ev, handler); michael@0: michael@0: // logout command causes processIncomingCommands to return explicit false. michael@0: do_check_false(engine.processIncomingCommands()); michael@0: }); michael@0: michael@0: add_test(function test_command_sync() { michael@0: _("Ensure that commands are synced across clients."); michael@0: michael@0: engine._store.wipe(); michael@0: generateNewKeys(Service.collectionKeys); michael@0: michael@0: let contents = { michael@0: meta: {global: {engines: {clients: {version: engine.version, michael@0: syncID: engine.syncID}}}}, michael@0: clients: {}, michael@0: crypto: {} michael@0: }; michael@0: let server = serverForUsers({"foo": "password"}, contents); michael@0: new SyncTestingInfrastructure(server.server); michael@0: michael@0: let user = server.user("foo"); michael@0: let remoteId = Utils.makeGUID(); michael@0: michael@0: function clientWBO(id) { michael@0: return user.collection("clients").wbo(id); michael@0: } michael@0: michael@0: _("Create remote client record"); michael@0: let rec = new ClientsRec("clients", remoteId); michael@0: engine._store.create(rec); michael@0: let remoteRecord = engine._store.createRecord(remoteId, "clients"); michael@0: engine.sendCommand("wipeAll", []); michael@0: michael@0: let clientRecord = engine._store._remoteClients[remoteId]; michael@0: do_check_neq(clientRecord, undefined); michael@0: do_check_eq(clientRecord.commands.length, 1); michael@0: michael@0: try { michael@0: _("Syncing."); michael@0: engine._sync(); michael@0: _("Checking record was uploaded."); michael@0: do_check_neq(clientWBO(engine.localID).payload, undefined); michael@0: do_check_true(engine.lastRecordUpload > 0); michael@0: michael@0: do_check_neq(clientWBO(remoteId).payload, undefined); michael@0: michael@0: Svc.Prefs.set("client.GUID", remoteId); michael@0: engine._resetClient(); michael@0: do_check_eq(engine.localID, remoteId); michael@0: _("Performing sync on resetted client."); michael@0: engine._sync(); michael@0: do_check_neq(engine.localCommands, undefined); michael@0: do_check_eq(engine.localCommands.length, 1); michael@0: michael@0: let command = engine.localCommands[0]; michael@0: do_check_eq(command.command, "wipeAll"); michael@0: do_check_eq(command.args.length, 0); michael@0: michael@0: } finally { michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_send_uri_to_client_for_display() { michael@0: _("Ensure sendURIToClientForDisplay() sends command properly."); michael@0: michael@0: let tracker = engine._tracker; michael@0: let store = engine._store; michael@0: michael@0: let remoteId = Utils.makeGUID(); michael@0: let rec = new ClientsRec("clients", remoteId); michael@0: rec.name = "remote"; michael@0: store.create(rec); michael@0: let remoteRecord = store.createRecord(remoteId, "clients"); michael@0: michael@0: tracker.clearChangedIDs(); michael@0: let initialScore = tracker.score; michael@0: michael@0: let uri = "http://www.mozilla.org/"; michael@0: let title = "Title of the Page"; michael@0: engine.sendURIToClientForDisplay(uri, remoteId, title); michael@0: michael@0: let newRecord = store._remoteClients[remoteId]; michael@0: michael@0: do_check_neq(newRecord, undefined); michael@0: do_check_eq(newRecord.commands.length, 1); michael@0: michael@0: let command = newRecord.commands[0]; michael@0: do_check_eq(command.command, "displayURI"); michael@0: do_check_eq(command.args.length, 3); michael@0: do_check_eq(command.args[0], uri); michael@0: do_check_eq(command.args[1], engine.localID); michael@0: do_check_eq(command.args[2], title); michael@0: michael@0: do_check_true(tracker.score > initialScore); michael@0: do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE); michael@0: michael@0: _("Ensure unknown client IDs result in exception."); michael@0: let unknownId = Utils.makeGUID(); michael@0: let error; michael@0: michael@0: try { michael@0: engine.sendURIToClientForDisplay(uri, unknownId); michael@0: } catch (ex) { michael@0: error = ex; michael@0: } michael@0: michael@0: do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_receive_display_uri() { michael@0: _("Ensure processing of received 'displayURI' commands works."); michael@0: michael@0: // We don't set up WBOs and perform syncing because other tests verify michael@0: // the command API works as advertised. This saves us a little work. michael@0: michael@0: let uri = "http://www.mozilla.org/"; michael@0: let remoteId = Utils.makeGUID(); michael@0: let title = "Page Title!"; michael@0: michael@0: let command = { michael@0: command: "displayURI", michael@0: args: [uri, remoteId, title], michael@0: }; michael@0: michael@0: engine.localCommands = [command]; michael@0: michael@0: // Received 'displayURI' command should result in the topic defined below michael@0: // being called. michael@0: let ev = "weave:engine:clients:display-uri"; michael@0: michael@0: let handler = function(subject, data) { michael@0: Svc.Obs.remove(ev, handler); michael@0: michael@0: do_check_eq(subject.uri, uri); michael@0: do_check_eq(subject.client, remoteId); michael@0: do_check_eq(subject.title, title); michael@0: do_check_eq(data, null); michael@0: michael@0: run_next_test(); michael@0: }; michael@0: michael@0: Svc.Obs.add(ev, handler); michael@0: michael@0: do_check_true(engine.processIncomingCommands()); michael@0: }); michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace; michael@0: run_next_test(); michael@0: }