1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_clients_engine.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,584 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://services-sync/constants.js"); 1.8 +Cu.import("resource://services-sync/engines.js"); 1.9 +Cu.import("resource://services-sync/engines/clients.js"); 1.10 +Cu.import("resource://services-sync/record.js"); 1.11 +Cu.import("resource://services-sync/service.js"); 1.12 +Cu.import("resource://services-sync/util.js"); 1.13 +Cu.import("resource://testing-common/services/sync/utils.js"); 1.14 + 1.15 +const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days 1.16 +const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day 1.17 + 1.18 +let engine = Service.clientsEngine; 1.19 + 1.20 +/** 1.21 + * Unpack the record with this ID, and verify that it has the same version that 1.22 + * we should be putting into records. 1.23 + */ 1.24 +function check_record_version(user, id) { 1.25 + let payload = JSON.parse(user.collection("clients").wbo(id).payload); 1.26 + 1.27 + let rec = new CryptoWrapper(); 1.28 + rec.id = id; 1.29 + rec.collection = "clients"; 1.30 + rec.ciphertext = payload.ciphertext; 1.31 + rec.hmac = payload.hmac; 1.32 + rec.IV = payload.IV; 1.33 + 1.34 + let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients")); 1.35 + 1.36 + _("Payload is " + JSON.stringify(cleartext)); 1.37 + do_check_eq(Services.appinfo.version, cleartext.version); 1.38 + do_check_eq(2, cleartext.protocols.length); 1.39 + do_check_eq("1.1", cleartext.protocols[0]); 1.40 + do_check_eq("1.5", cleartext.protocols[1]); 1.41 +} 1.42 + 1.43 +add_test(function test_bad_hmac() { 1.44 + _("Ensure that Clients engine deletes corrupt records."); 1.45 + let contents = { 1.46 + meta: {global: {engines: {clients: {version: engine.version, 1.47 + syncID: engine.syncID}}}}, 1.48 + clients: {}, 1.49 + crypto: {} 1.50 + }; 1.51 + let deletedCollections = []; 1.52 + let deletedItems = []; 1.53 + let callback = { 1.54 + __proto__: SyncServerCallback, 1.55 + onItemDeleted: function (username, coll, wboID) { 1.56 + deletedItems.push(coll + "/" + wboID); 1.57 + }, 1.58 + onCollectionDeleted: function (username, coll) { 1.59 + deletedCollections.push(coll); 1.60 + } 1.61 + } 1.62 + let server = serverForUsers({"foo": "password"}, contents, callback); 1.63 + let user = server.user("foo"); 1.64 + 1.65 + function check_clients_count(expectedCount) { 1.66 + let stack = Components.stack.caller; 1.67 + let coll = user.collection("clients"); 1.68 + 1.69 + // Treat a non-existent collection as empty. 1.70 + do_check_eq(expectedCount, coll ? coll.count() : 0, stack); 1.71 + } 1.72 + 1.73 + function check_client_deleted(id) { 1.74 + let coll = user.collection("clients"); 1.75 + let wbo = coll.wbo(id); 1.76 + return !wbo || !wbo.payload; 1.77 + } 1.78 + 1.79 + function uploadNewKeys() { 1.80 + generateNewKeys(Service.collectionKeys); 1.81 + let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); 1.82 + serverKeys.encrypt(Service.identity.syncKeyBundle); 1.83 + do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success); 1.84 + } 1.85 + 1.86 + try { 1.87 + ensureLegacyIdentityManager(); 1.88 + let passphrase = "abcdeabcdeabcdeabcdeabcdea"; 1.89 + Service.serverURL = server.baseURI; 1.90 + Service.login("foo", "ilovejane", passphrase); 1.91 + 1.92 + generateNewKeys(Service.collectionKeys); 1.93 + 1.94 + _("First sync, client record is uploaded"); 1.95 + do_check_eq(engine.lastRecordUpload, 0); 1.96 + check_clients_count(0); 1.97 + engine._sync(); 1.98 + check_clients_count(1); 1.99 + do_check_true(engine.lastRecordUpload > 0); 1.100 + 1.101 + // Our uploaded record has a version. 1.102 + check_record_version(user, engine.localID); 1.103 + 1.104 + // Initial setup can wipe the server, so clean up. 1.105 + deletedCollections = []; 1.106 + deletedItems = []; 1.107 + 1.108 + _("Change our keys and our client ID, reupload keys."); 1.109 + let oldLocalID = engine.localID; // Preserve to test for deletion! 1.110 + engine.localID = Utils.makeGUID(); 1.111 + engine.resetClient(); 1.112 + generateNewKeys(Service.collectionKeys); 1.113 + let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); 1.114 + serverKeys.encrypt(Service.identity.syncKeyBundle); 1.115 + do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success); 1.116 + 1.117 + _("Sync."); 1.118 + engine._sync(); 1.119 + 1.120 + _("Old record " + oldLocalID + " was deleted, new one uploaded."); 1.121 + check_clients_count(1); 1.122 + check_client_deleted(oldLocalID); 1.123 + 1.124 + _("Now change our keys but don't upload them. " + 1.125 + "That means we get an HMAC error but redownload keys."); 1.126 + Service.lastHMACEvent = 0; 1.127 + engine.localID = Utils.makeGUID(); 1.128 + engine.resetClient(); 1.129 + generateNewKeys(Service.collectionKeys); 1.130 + deletedCollections = []; 1.131 + deletedItems = []; 1.132 + check_clients_count(1); 1.133 + engine._sync(); 1.134 + 1.135 + _("Old record was not deleted, new one uploaded."); 1.136 + do_check_eq(deletedCollections.length, 0); 1.137 + do_check_eq(deletedItems.length, 0); 1.138 + check_clients_count(2); 1.139 + 1.140 + _("Now try the scenario where our keys are wrong *and* there's a bad record."); 1.141 + // Clean up and start fresh. 1.142 + user.collection("clients")._wbos = {}; 1.143 + Service.lastHMACEvent = 0; 1.144 + engine.localID = Utils.makeGUID(); 1.145 + engine.resetClient(); 1.146 + deletedCollections = []; 1.147 + deletedItems = []; 1.148 + check_clients_count(0); 1.149 + 1.150 + uploadNewKeys(); 1.151 + 1.152 + // Sync once to upload a record. 1.153 + engine._sync(); 1.154 + check_clients_count(1); 1.155 + 1.156 + // Generate and upload new keys, so the old client record is wrong. 1.157 + uploadNewKeys(); 1.158 + 1.159 + // Create a new client record and new keys. Now our keys are wrong, as well 1.160 + // as the object on the server. We'll download the new keys and also delete 1.161 + // the bad client record. 1.162 + oldLocalID = engine.localID; // Preserve to test for deletion! 1.163 + engine.localID = Utils.makeGUID(); 1.164 + engine.resetClient(); 1.165 + generateNewKeys(Service.collectionKeys); 1.166 + let oldKey = Service.collectionKeys.keyForCollection(); 1.167 + 1.168 + do_check_eq(deletedCollections.length, 0); 1.169 + do_check_eq(deletedItems.length, 0); 1.170 + engine._sync(); 1.171 + do_check_eq(deletedItems.length, 1); 1.172 + check_client_deleted(oldLocalID); 1.173 + check_clients_count(1); 1.174 + let newKey = Service.collectionKeys.keyForCollection(); 1.175 + do_check_false(oldKey.equals(newKey)); 1.176 + 1.177 + } finally { 1.178 + Svc.Prefs.resetBranch(""); 1.179 + Service.recordManager.clearCache(); 1.180 + server.stop(run_next_test); 1.181 + } 1.182 +}); 1.183 + 1.184 +add_test(function test_properties() { 1.185 + _("Test lastRecordUpload property"); 1.186 + try { 1.187 + do_check_eq(Svc.Prefs.get("clients.lastRecordUpload"), undefined); 1.188 + do_check_eq(engine.lastRecordUpload, 0); 1.189 + 1.190 + let now = Date.now(); 1.191 + engine.lastRecordUpload = now / 1000; 1.192 + do_check_eq(engine.lastRecordUpload, Math.floor(now / 1000)); 1.193 + } finally { 1.194 + Svc.Prefs.resetBranch(""); 1.195 + run_next_test(); 1.196 + } 1.197 +}); 1.198 + 1.199 +add_test(function test_sync() { 1.200 + _("Ensure that Clients engine uploads a new client record once a week."); 1.201 + 1.202 + let contents = { 1.203 + meta: {global: {engines: {clients: {version: engine.version, 1.204 + syncID: engine.syncID}}}}, 1.205 + clients: {}, 1.206 + crypto: {} 1.207 + }; 1.208 + let server = serverForUsers({"foo": "password"}, contents); 1.209 + let user = server.user("foo"); 1.210 + 1.211 + new SyncTestingInfrastructure(server.server); 1.212 + generateNewKeys(Service.collectionKeys); 1.213 + 1.214 + function clientWBO() { 1.215 + return user.collection("clients").wbo(engine.localID); 1.216 + } 1.217 + 1.218 + try { 1.219 + 1.220 + _("First sync. Client record is uploaded."); 1.221 + do_check_eq(clientWBO(), undefined); 1.222 + do_check_eq(engine.lastRecordUpload, 0); 1.223 + engine._sync(); 1.224 + do_check_true(!!clientWBO().payload); 1.225 + do_check_true(engine.lastRecordUpload > 0); 1.226 + 1.227 + _("Let's time travel more than a week back, new record should've been uploaded."); 1.228 + engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH; 1.229 + let lastweek = engine.lastRecordUpload; 1.230 + clientWBO().payload = undefined; 1.231 + engine._sync(); 1.232 + do_check_true(!!clientWBO().payload); 1.233 + do_check_true(engine.lastRecordUpload > lastweek); 1.234 + 1.235 + _("Remove client record."); 1.236 + engine.removeClientData(); 1.237 + do_check_eq(clientWBO().payload, undefined); 1.238 + 1.239 + _("Time travel one day back, no record uploaded."); 1.240 + engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH; 1.241 + let yesterday = engine.lastRecordUpload; 1.242 + engine._sync(); 1.243 + do_check_eq(clientWBO().payload, undefined); 1.244 + do_check_eq(engine.lastRecordUpload, yesterday); 1.245 + 1.246 + } finally { 1.247 + Svc.Prefs.resetBranch(""); 1.248 + Service.recordManager.clearCache(); 1.249 + server.stop(run_next_test); 1.250 + } 1.251 +}); 1.252 + 1.253 +add_test(function test_client_name_change() { 1.254 + _("Ensure client name change incurs a client record update."); 1.255 + 1.256 + let tracker = engine._tracker; 1.257 + 1.258 + let localID = engine.localID; 1.259 + let initialName = engine.localName; 1.260 + 1.261 + Svc.Obs.notify("weave:engine:start-tracking"); 1.262 + _("initial name: " + initialName); 1.263 + 1.264 + // Tracker already has data, so clear it. 1.265 + tracker.clearChangedIDs(); 1.266 + 1.267 + let initialScore = tracker.score; 1.268 + 1.269 + do_check_eq(Object.keys(tracker.changedIDs).length, 0); 1.270 + 1.271 + Svc.Prefs.set("client.name", "new name"); 1.272 + 1.273 + _("new name: " + engine.localName); 1.274 + do_check_neq(initialName, engine.localName); 1.275 + do_check_eq(Object.keys(tracker.changedIDs).length, 1); 1.276 + do_check_true(engine.localID in tracker.changedIDs); 1.277 + do_check_true(tracker.score > initialScore); 1.278 + do_check_true(tracker.score >= SCORE_INCREMENT_XLARGE); 1.279 + 1.280 + Svc.Obs.notify("weave:engine:stop-tracking"); 1.281 + 1.282 + run_next_test(); 1.283 +}); 1.284 + 1.285 +add_test(function test_send_command() { 1.286 + _("Verifies _sendCommandToClient puts commands in the outbound queue."); 1.287 + 1.288 + let store = engine._store; 1.289 + let tracker = engine._tracker; 1.290 + let remoteId = Utils.makeGUID(); 1.291 + let rec = new ClientsRec("clients", remoteId); 1.292 + 1.293 + store.create(rec); 1.294 + let remoteRecord = store.createRecord(remoteId, "clients"); 1.295 + 1.296 + let action = "testCommand"; 1.297 + let args = ["foo", "bar"]; 1.298 + 1.299 + engine._sendCommandToClient(action, args, remoteId); 1.300 + 1.301 + let newRecord = store._remoteClients[remoteId]; 1.302 + do_check_neq(newRecord, undefined); 1.303 + do_check_eq(newRecord.commands.length, 1); 1.304 + 1.305 + let command = newRecord.commands[0]; 1.306 + do_check_eq(command.command, action); 1.307 + do_check_eq(command.args.length, 2); 1.308 + do_check_eq(command.args, args); 1.309 + 1.310 + do_check_neq(tracker.changedIDs[remoteId], undefined); 1.311 + 1.312 + run_next_test(); 1.313 +}); 1.314 + 1.315 +add_test(function test_command_validation() { 1.316 + _("Verifies that command validation works properly."); 1.317 + 1.318 + let store = engine._store; 1.319 + 1.320 + let testCommands = [ 1.321 + ["resetAll", [], true ], 1.322 + ["resetAll", ["foo"], false], 1.323 + ["resetEngine", ["tabs"], true ], 1.324 + ["resetEngine", [], false], 1.325 + ["wipeAll", [], true ], 1.326 + ["wipeAll", ["foo"], false], 1.327 + ["wipeEngine", ["tabs"], true ], 1.328 + ["wipeEngine", [], false], 1.329 + ["logout", [], true ], 1.330 + ["logout", ["foo"], false], 1.331 + ["__UNKNOWN__", [], false] 1.332 + ]; 1.333 + 1.334 + for each (let [action, args, expectedResult] in testCommands) { 1.335 + let remoteId = Utils.makeGUID(); 1.336 + let rec = new ClientsRec("clients", remoteId); 1.337 + 1.338 + store.create(rec); 1.339 + store.createRecord(remoteId, "clients"); 1.340 + 1.341 + engine.sendCommand(action, args, remoteId); 1.342 + 1.343 + let newRecord = store._remoteClients[remoteId]; 1.344 + do_check_neq(newRecord, undefined); 1.345 + 1.346 + if (expectedResult) { 1.347 + _("Ensuring command is sent: " + action); 1.348 + do_check_eq(newRecord.commands.length, 1); 1.349 + 1.350 + let command = newRecord.commands[0]; 1.351 + do_check_eq(command.command, action); 1.352 + do_check_eq(command.args, args); 1.353 + 1.354 + do_check_neq(engine._tracker, undefined); 1.355 + do_check_neq(engine._tracker.changedIDs[remoteId], undefined); 1.356 + } else { 1.357 + _("Ensuring command is scrubbed: " + action); 1.358 + do_check_eq(newRecord.commands, undefined); 1.359 + 1.360 + if (store._tracker) { 1.361 + do_check_eq(engine._tracker[remoteId], undefined); 1.362 + } 1.363 + } 1.364 + 1.365 + } 1.366 + run_next_test(); 1.367 +}); 1.368 + 1.369 +add_test(function test_command_duplication() { 1.370 + _("Ensures duplicate commands are detected and not added"); 1.371 + 1.372 + let store = engine._store; 1.373 + let remoteId = Utils.makeGUID(); 1.374 + let rec = new ClientsRec("clients", remoteId); 1.375 + store.create(rec); 1.376 + store.createRecord(remoteId, "clients"); 1.377 + 1.378 + let action = "resetAll"; 1.379 + let args = []; 1.380 + 1.381 + engine.sendCommand(action, args, remoteId); 1.382 + engine.sendCommand(action, args, remoteId); 1.383 + 1.384 + let newRecord = store._remoteClients[remoteId]; 1.385 + do_check_eq(newRecord.commands.length, 1); 1.386 + 1.387 + _("Check variant args length"); 1.388 + newRecord.commands = []; 1.389 + 1.390 + action = "resetEngine"; 1.391 + engine.sendCommand(action, [{ x: "foo" }], remoteId); 1.392 + engine.sendCommand(action, [{ x: "bar" }], remoteId); 1.393 + 1.394 + _("Make sure we spot a real dupe argument."); 1.395 + engine.sendCommand(action, [{ x: "bar" }], remoteId); 1.396 + 1.397 + do_check_eq(newRecord.commands.length, 2); 1.398 + 1.399 + run_next_test(); 1.400 +}); 1.401 + 1.402 +add_test(function test_command_invalid_client() { 1.403 + _("Ensures invalid client IDs are caught"); 1.404 + 1.405 + let id = Utils.makeGUID(); 1.406 + let error; 1.407 + 1.408 + try { 1.409 + engine.sendCommand("wipeAll", [], id); 1.410 + } catch (ex) { 1.411 + error = ex; 1.412 + } 1.413 + 1.414 + do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); 1.415 + 1.416 + run_next_test(); 1.417 +}); 1.418 + 1.419 +add_test(function test_process_incoming_commands() { 1.420 + _("Ensures local commands are executed"); 1.421 + 1.422 + engine.localCommands = [{ command: "logout", args: [] }]; 1.423 + 1.424 + let ev = "weave:service:logout:finish"; 1.425 + 1.426 + var handler = function() { 1.427 + Svc.Obs.remove(ev, handler); 1.428 + run_next_test(); 1.429 + }; 1.430 + 1.431 + Svc.Obs.add(ev, handler); 1.432 + 1.433 + // logout command causes processIncomingCommands to return explicit false. 1.434 + do_check_false(engine.processIncomingCommands()); 1.435 +}); 1.436 + 1.437 +add_test(function test_command_sync() { 1.438 + _("Ensure that commands are synced across clients."); 1.439 + 1.440 + engine._store.wipe(); 1.441 + generateNewKeys(Service.collectionKeys); 1.442 + 1.443 + let contents = { 1.444 + meta: {global: {engines: {clients: {version: engine.version, 1.445 + syncID: engine.syncID}}}}, 1.446 + clients: {}, 1.447 + crypto: {} 1.448 + }; 1.449 + let server = serverForUsers({"foo": "password"}, contents); 1.450 + new SyncTestingInfrastructure(server.server); 1.451 + 1.452 + let user = server.user("foo"); 1.453 + let remoteId = Utils.makeGUID(); 1.454 + 1.455 + function clientWBO(id) { 1.456 + return user.collection("clients").wbo(id); 1.457 + } 1.458 + 1.459 + _("Create remote client record"); 1.460 + let rec = new ClientsRec("clients", remoteId); 1.461 + engine._store.create(rec); 1.462 + let remoteRecord = engine._store.createRecord(remoteId, "clients"); 1.463 + engine.sendCommand("wipeAll", []); 1.464 + 1.465 + let clientRecord = engine._store._remoteClients[remoteId]; 1.466 + do_check_neq(clientRecord, undefined); 1.467 + do_check_eq(clientRecord.commands.length, 1); 1.468 + 1.469 + try { 1.470 + _("Syncing."); 1.471 + engine._sync(); 1.472 + _("Checking record was uploaded."); 1.473 + do_check_neq(clientWBO(engine.localID).payload, undefined); 1.474 + do_check_true(engine.lastRecordUpload > 0); 1.475 + 1.476 + do_check_neq(clientWBO(remoteId).payload, undefined); 1.477 + 1.478 + Svc.Prefs.set("client.GUID", remoteId); 1.479 + engine._resetClient(); 1.480 + do_check_eq(engine.localID, remoteId); 1.481 + _("Performing sync on resetted client."); 1.482 + engine._sync(); 1.483 + do_check_neq(engine.localCommands, undefined); 1.484 + do_check_eq(engine.localCommands.length, 1); 1.485 + 1.486 + let command = engine.localCommands[0]; 1.487 + do_check_eq(command.command, "wipeAll"); 1.488 + do_check_eq(command.args.length, 0); 1.489 + 1.490 + } finally { 1.491 + Svc.Prefs.resetBranch(""); 1.492 + Service.recordManager.clearCache(); 1.493 + server.stop(run_next_test); 1.494 + } 1.495 +}); 1.496 + 1.497 +add_test(function test_send_uri_to_client_for_display() { 1.498 + _("Ensure sendURIToClientForDisplay() sends command properly."); 1.499 + 1.500 + let tracker = engine._tracker; 1.501 + let store = engine._store; 1.502 + 1.503 + let remoteId = Utils.makeGUID(); 1.504 + let rec = new ClientsRec("clients", remoteId); 1.505 + rec.name = "remote"; 1.506 + store.create(rec); 1.507 + let remoteRecord = store.createRecord(remoteId, "clients"); 1.508 + 1.509 + tracker.clearChangedIDs(); 1.510 + let initialScore = tracker.score; 1.511 + 1.512 + let uri = "http://www.mozilla.org/"; 1.513 + let title = "Title of the Page"; 1.514 + engine.sendURIToClientForDisplay(uri, remoteId, title); 1.515 + 1.516 + let newRecord = store._remoteClients[remoteId]; 1.517 + 1.518 + do_check_neq(newRecord, undefined); 1.519 + do_check_eq(newRecord.commands.length, 1); 1.520 + 1.521 + let command = newRecord.commands[0]; 1.522 + do_check_eq(command.command, "displayURI"); 1.523 + do_check_eq(command.args.length, 3); 1.524 + do_check_eq(command.args[0], uri); 1.525 + do_check_eq(command.args[1], engine.localID); 1.526 + do_check_eq(command.args[2], title); 1.527 + 1.528 + do_check_true(tracker.score > initialScore); 1.529 + do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE); 1.530 + 1.531 + _("Ensure unknown client IDs result in exception."); 1.532 + let unknownId = Utils.makeGUID(); 1.533 + let error; 1.534 + 1.535 + try { 1.536 + engine.sendURIToClientForDisplay(uri, unknownId); 1.537 + } catch (ex) { 1.538 + error = ex; 1.539 + } 1.540 + 1.541 + do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); 1.542 + 1.543 + run_next_test(); 1.544 +}); 1.545 + 1.546 +add_test(function test_receive_display_uri() { 1.547 + _("Ensure processing of received 'displayURI' commands works."); 1.548 + 1.549 + // We don't set up WBOs and perform syncing because other tests verify 1.550 + // the command API works as advertised. This saves us a little work. 1.551 + 1.552 + let uri = "http://www.mozilla.org/"; 1.553 + let remoteId = Utils.makeGUID(); 1.554 + let title = "Page Title!"; 1.555 + 1.556 + let command = { 1.557 + command: "displayURI", 1.558 + args: [uri, remoteId, title], 1.559 + }; 1.560 + 1.561 + engine.localCommands = [command]; 1.562 + 1.563 + // Received 'displayURI' command should result in the topic defined below 1.564 + // being called. 1.565 + let ev = "weave:engine:clients:display-uri"; 1.566 + 1.567 + let handler = function(subject, data) { 1.568 + Svc.Obs.remove(ev, handler); 1.569 + 1.570 + do_check_eq(subject.uri, uri); 1.571 + do_check_eq(subject.client, remoteId); 1.572 + do_check_eq(subject.title, title); 1.573 + do_check_eq(data, null); 1.574 + 1.575 + run_next_test(); 1.576 + }; 1.577 + 1.578 + Svc.Obs.add(ev, handler); 1.579 + 1.580 + do_check_true(engine.processIncomingCommands()); 1.581 +}); 1.582 + 1.583 +function run_test() { 1.584 + initTestLogging("Trace"); 1.585 + Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace; 1.586 + run_next_test(); 1.587 +}