Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/ */
4 Cu.import("resource://services-sync/constants.js");
5 Cu.import("resource://services-sync/engines.js");
6 Cu.import("resource://services-sync/engines/clients.js");
7 Cu.import("resource://services-sync/record.js");
8 Cu.import("resource://services-sync/service.js");
9 Cu.import("resource://services-sync/util.js");
10 Cu.import("resource://testing-common/services/sync/utils.js");
12 const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
13 const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
15 let engine = Service.clientsEngine;
17 /**
18 * Unpack the record with this ID, and verify that it has the same version that
19 * we should be putting into records.
20 */
21 function check_record_version(user, id) {
22 let payload = JSON.parse(user.collection("clients").wbo(id).payload);
24 let rec = new CryptoWrapper();
25 rec.id = id;
26 rec.collection = "clients";
27 rec.ciphertext = payload.ciphertext;
28 rec.hmac = payload.hmac;
29 rec.IV = payload.IV;
31 let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients"));
33 _("Payload is " + JSON.stringify(cleartext));
34 do_check_eq(Services.appinfo.version, cleartext.version);
35 do_check_eq(2, cleartext.protocols.length);
36 do_check_eq("1.1", cleartext.protocols[0]);
37 do_check_eq("1.5", cleartext.protocols[1]);
38 }
40 add_test(function test_bad_hmac() {
41 _("Ensure that Clients engine deletes corrupt records.");
42 let contents = {
43 meta: {global: {engines: {clients: {version: engine.version,
44 syncID: engine.syncID}}}},
45 clients: {},
46 crypto: {}
47 };
48 let deletedCollections = [];
49 let deletedItems = [];
50 let callback = {
51 __proto__: SyncServerCallback,
52 onItemDeleted: function (username, coll, wboID) {
53 deletedItems.push(coll + "/" + wboID);
54 },
55 onCollectionDeleted: function (username, coll) {
56 deletedCollections.push(coll);
57 }
58 }
59 let server = serverForUsers({"foo": "password"}, contents, callback);
60 let user = server.user("foo");
62 function check_clients_count(expectedCount) {
63 let stack = Components.stack.caller;
64 let coll = user.collection("clients");
66 // Treat a non-existent collection as empty.
67 do_check_eq(expectedCount, coll ? coll.count() : 0, stack);
68 }
70 function check_client_deleted(id) {
71 let coll = user.collection("clients");
72 let wbo = coll.wbo(id);
73 return !wbo || !wbo.payload;
74 }
76 function uploadNewKeys() {
77 generateNewKeys(Service.collectionKeys);
78 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
79 serverKeys.encrypt(Service.identity.syncKeyBundle);
80 do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
81 }
83 try {
84 ensureLegacyIdentityManager();
85 let passphrase = "abcdeabcdeabcdeabcdeabcdea";
86 Service.serverURL = server.baseURI;
87 Service.login("foo", "ilovejane", passphrase);
89 generateNewKeys(Service.collectionKeys);
91 _("First sync, client record is uploaded");
92 do_check_eq(engine.lastRecordUpload, 0);
93 check_clients_count(0);
94 engine._sync();
95 check_clients_count(1);
96 do_check_true(engine.lastRecordUpload > 0);
98 // Our uploaded record has a version.
99 check_record_version(user, engine.localID);
101 // Initial setup can wipe the server, so clean up.
102 deletedCollections = [];
103 deletedItems = [];
105 _("Change our keys and our client ID, reupload keys.");
106 let oldLocalID = engine.localID; // Preserve to test for deletion!
107 engine.localID = Utils.makeGUID();
108 engine.resetClient();
109 generateNewKeys(Service.collectionKeys);
110 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
111 serverKeys.encrypt(Service.identity.syncKeyBundle);
112 do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success);
114 _("Sync.");
115 engine._sync();
117 _("Old record " + oldLocalID + " was deleted, new one uploaded.");
118 check_clients_count(1);
119 check_client_deleted(oldLocalID);
121 _("Now change our keys but don't upload them. " +
122 "That means we get an HMAC error but redownload keys.");
123 Service.lastHMACEvent = 0;
124 engine.localID = Utils.makeGUID();
125 engine.resetClient();
126 generateNewKeys(Service.collectionKeys);
127 deletedCollections = [];
128 deletedItems = [];
129 check_clients_count(1);
130 engine._sync();
132 _("Old record was not deleted, new one uploaded.");
133 do_check_eq(deletedCollections.length, 0);
134 do_check_eq(deletedItems.length, 0);
135 check_clients_count(2);
137 _("Now try the scenario where our keys are wrong *and* there's a bad record.");
138 // Clean up and start fresh.
139 user.collection("clients")._wbos = {};
140 Service.lastHMACEvent = 0;
141 engine.localID = Utils.makeGUID();
142 engine.resetClient();
143 deletedCollections = [];
144 deletedItems = [];
145 check_clients_count(0);
147 uploadNewKeys();
149 // Sync once to upload a record.
150 engine._sync();
151 check_clients_count(1);
153 // Generate and upload new keys, so the old client record is wrong.
154 uploadNewKeys();
156 // Create a new client record and new keys. Now our keys are wrong, as well
157 // as the object on the server. We'll download the new keys and also delete
158 // the bad client record.
159 oldLocalID = engine.localID; // Preserve to test for deletion!
160 engine.localID = Utils.makeGUID();
161 engine.resetClient();
162 generateNewKeys(Service.collectionKeys);
163 let oldKey = Service.collectionKeys.keyForCollection();
165 do_check_eq(deletedCollections.length, 0);
166 do_check_eq(deletedItems.length, 0);
167 engine._sync();
168 do_check_eq(deletedItems.length, 1);
169 check_client_deleted(oldLocalID);
170 check_clients_count(1);
171 let newKey = Service.collectionKeys.keyForCollection();
172 do_check_false(oldKey.equals(newKey));
174 } finally {
175 Svc.Prefs.resetBranch("");
176 Service.recordManager.clearCache();
177 server.stop(run_next_test);
178 }
179 });
181 add_test(function test_properties() {
182 _("Test lastRecordUpload property");
183 try {
184 do_check_eq(Svc.Prefs.get("clients.lastRecordUpload"), undefined);
185 do_check_eq(engine.lastRecordUpload, 0);
187 let now = Date.now();
188 engine.lastRecordUpload = now / 1000;
189 do_check_eq(engine.lastRecordUpload, Math.floor(now / 1000));
190 } finally {
191 Svc.Prefs.resetBranch("");
192 run_next_test();
193 }
194 });
196 add_test(function test_sync() {
197 _("Ensure that Clients engine uploads a new client record once a week.");
199 let contents = {
200 meta: {global: {engines: {clients: {version: engine.version,
201 syncID: engine.syncID}}}},
202 clients: {},
203 crypto: {}
204 };
205 let server = serverForUsers({"foo": "password"}, contents);
206 let user = server.user("foo");
208 new SyncTestingInfrastructure(server.server);
209 generateNewKeys(Service.collectionKeys);
211 function clientWBO() {
212 return user.collection("clients").wbo(engine.localID);
213 }
215 try {
217 _("First sync. Client record is uploaded.");
218 do_check_eq(clientWBO(), undefined);
219 do_check_eq(engine.lastRecordUpload, 0);
220 engine._sync();
221 do_check_true(!!clientWBO().payload);
222 do_check_true(engine.lastRecordUpload > 0);
224 _("Let's time travel more than a week back, new record should've been uploaded.");
225 engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
226 let lastweek = engine.lastRecordUpload;
227 clientWBO().payload = undefined;
228 engine._sync();
229 do_check_true(!!clientWBO().payload);
230 do_check_true(engine.lastRecordUpload > lastweek);
232 _("Remove client record.");
233 engine.removeClientData();
234 do_check_eq(clientWBO().payload, undefined);
236 _("Time travel one day back, no record uploaded.");
237 engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
238 let yesterday = engine.lastRecordUpload;
239 engine._sync();
240 do_check_eq(clientWBO().payload, undefined);
241 do_check_eq(engine.lastRecordUpload, yesterday);
243 } finally {
244 Svc.Prefs.resetBranch("");
245 Service.recordManager.clearCache();
246 server.stop(run_next_test);
247 }
248 });
250 add_test(function test_client_name_change() {
251 _("Ensure client name change incurs a client record update.");
253 let tracker = engine._tracker;
255 let localID = engine.localID;
256 let initialName = engine.localName;
258 Svc.Obs.notify("weave:engine:start-tracking");
259 _("initial name: " + initialName);
261 // Tracker already has data, so clear it.
262 tracker.clearChangedIDs();
264 let initialScore = tracker.score;
266 do_check_eq(Object.keys(tracker.changedIDs).length, 0);
268 Svc.Prefs.set("client.name", "new name");
270 _("new name: " + engine.localName);
271 do_check_neq(initialName, engine.localName);
272 do_check_eq(Object.keys(tracker.changedIDs).length, 1);
273 do_check_true(engine.localID in tracker.changedIDs);
274 do_check_true(tracker.score > initialScore);
275 do_check_true(tracker.score >= SCORE_INCREMENT_XLARGE);
277 Svc.Obs.notify("weave:engine:stop-tracking");
279 run_next_test();
280 });
282 add_test(function test_send_command() {
283 _("Verifies _sendCommandToClient puts commands in the outbound queue.");
285 let store = engine._store;
286 let tracker = engine._tracker;
287 let remoteId = Utils.makeGUID();
288 let rec = new ClientsRec("clients", remoteId);
290 store.create(rec);
291 let remoteRecord = store.createRecord(remoteId, "clients");
293 let action = "testCommand";
294 let args = ["foo", "bar"];
296 engine._sendCommandToClient(action, args, remoteId);
298 let newRecord = store._remoteClients[remoteId];
299 do_check_neq(newRecord, undefined);
300 do_check_eq(newRecord.commands.length, 1);
302 let command = newRecord.commands[0];
303 do_check_eq(command.command, action);
304 do_check_eq(command.args.length, 2);
305 do_check_eq(command.args, args);
307 do_check_neq(tracker.changedIDs[remoteId], undefined);
309 run_next_test();
310 });
312 add_test(function test_command_validation() {
313 _("Verifies that command validation works properly.");
315 let store = engine._store;
317 let testCommands = [
318 ["resetAll", [], true ],
319 ["resetAll", ["foo"], false],
320 ["resetEngine", ["tabs"], true ],
321 ["resetEngine", [], false],
322 ["wipeAll", [], true ],
323 ["wipeAll", ["foo"], false],
324 ["wipeEngine", ["tabs"], true ],
325 ["wipeEngine", [], false],
326 ["logout", [], true ],
327 ["logout", ["foo"], false],
328 ["__UNKNOWN__", [], false]
329 ];
331 for each (let [action, args, expectedResult] in testCommands) {
332 let remoteId = Utils.makeGUID();
333 let rec = new ClientsRec("clients", remoteId);
335 store.create(rec);
336 store.createRecord(remoteId, "clients");
338 engine.sendCommand(action, args, remoteId);
340 let newRecord = store._remoteClients[remoteId];
341 do_check_neq(newRecord, undefined);
343 if (expectedResult) {
344 _("Ensuring command is sent: " + action);
345 do_check_eq(newRecord.commands.length, 1);
347 let command = newRecord.commands[0];
348 do_check_eq(command.command, action);
349 do_check_eq(command.args, args);
351 do_check_neq(engine._tracker, undefined);
352 do_check_neq(engine._tracker.changedIDs[remoteId], undefined);
353 } else {
354 _("Ensuring command is scrubbed: " + action);
355 do_check_eq(newRecord.commands, undefined);
357 if (store._tracker) {
358 do_check_eq(engine._tracker[remoteId], undefined);
359 }
360 }
362 }
363 run_next_test();
364 });
366 add_test(function test_command_duplication() {
367 _("Ensures duplicate commands are detected and not added");
369 let store = engine._store;
370 let remoteId = Utils.makeGUID();
371 let rec = new ClientsRec("clients", remoteId);
372 store.create(rec);
373 store.createRecord(remoteId, "clients");
375 let action = "resetAll";
376 let args = [];
378 engine.sendCommand(action, args, remoteId);
379 engine.sendCommand(action, args, remoteId);
381 let newRecord = store._remoteClients[remoteId];
382 do_check_eq(newRecord.commands.length, 1);
384 _("Check variant args length");
385 newRecord.commands = [];
387 action = "resetEngine";
388 engine.sendCommand(action, [{ x: "foo" }], remoteId);
389 engine.sendCommand(action, [{ x: "bar" }], remoteId);
391 _("Make sure we spot a real dupe argument.");
392 engine.sendCommand(action, [{ x: "bar" }], remoteId);
394 do_check_eq(newRecord.commands.length, 2);
396 run_next_test();
397 });
399 add_test(function test_command_invalid_client() {
400 _("Ensures invalid client IDs are caught");
402 let id = Utils.makeGUID();
403 let error;
405 try {
406 engine.sendCommand("wipeAll", [], id);
407 } catch (ex) {
408 error = ex;
409 }
411 do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
413 run_next_test();
414 });
416 add_test(function test_process_incoming_commands() {
417 _("Ensures local commands are executed");
419 engine.localCommands = [{ command: "logout", args: [] }];
421 let ev = "weave:service:logout:finish";
423 var handler = function() {
424 Svc.Obs.remove(ev, handler);
425 run_next_test();
426 };
428 Svc.Obs.add(ev, handler);
430 // logout command causes processIncomingCommands to return explicit false.
431 do_check_false(engine.processIncomingCommands());
432 });
434 add_test(function test_command_sync() {
435 _("Ensure that commands are synced across clients.");
437 engine._store.wipe();
438 generateNewKeys(Service.collectionKeys);
440 let contents = {
441 meta: {global: {engines: {clients: {version: engine.version,
442 syncID: engine.syncID}}}},
443 clients: {},
444 crypto: {}
445 };
446 let server = serverForUsers({"foo": "password"}, contents);
447 new SyncTestingInfrastructure(server.server);
449 let user = server.user("foo");
450 let remoteId = Utils.makeGUID();
452 function clientWBO(id) {
453 return user.collection("clients").wbo(id);
454 }
456 _("Create remote client record");
457 let rec = new ClientsRec("clients", remoteId);
458 engine._store.create(rec);
459 let remoteRecord = engine._store.createRecord(remoteId, "clients");
460 engine.sendCommand("wipeAll", []);
462 let clientRecord = engine._store._remoteClients[remoteId];
463 do_check_neq(clientRecord, undefined);
464 do_check_eq(clientRecord.commands.length, 1);
466 try {
467 _("Syncing.");
468 engine._sync();
469 _("Checking record was uploaded.");
470 do_check_neq(clientWBO(engine.localID).payload, undefined);
471 do_check_true(engine.lastRecordUpload > 0);
473 do_check_neq(clientWBO(remoteId).payload, undefined);
475 Svc.Prefs.set("client.GUID", remoteId);
476 engine._resetClient();
477 do_check_eq(engine.localID, remoteId);
478 _("Performing sync on resetted client.");
479 engine._sync();
480 do_check_neq(engine.localCommands, undefined);
481 do_check_eq(engine.localCommands.length, 1);
483 let command = engine.localCommands[0];
484 do_check_eq(command.command, "wipeAll");
485 do_check_eq(command.args.length, 0);
487 } finally {
488 Svc.Prefs.resetBranch("");
489 Service.recordManager.clearCache();
490 server.stop(run_next_test);
491 }
492 });
494 add_test(function test_send_uri_to_client_for_display() {
495 _("Ensure sendURIToClientForDisplay() sends command properly.");
497 let tracker = engine._tracker;
498 let store = engine._store;
500 let remoteId = Utils.makeGUID();
501 let rec = new ClientsRec("clients", remoteId);
502 rec.name = "remote";
503 store.create(rec);
504 let remoteRecord = store.createRecord(remoteId, "clients");
506 tracker.clearChangedIDs();
507 let initialScore = tracker.score;
509 let uri = "http://www.mozilla.org/";
510 let title = "Title of the Page";
511 engine.sendURIToClientForDisplay(uri, remoteId, title);
513 let newRecord = store._remoteClients[remoteId];
515 do_check_neq(newRecord, undefined);
516 do_check_eq(newRecord.commands.length, 1);
518 let command = newRecord.commands[0];
519 do_check_eq(command.command, "displayURI");
520 do_check_eq(command.args.length, 3);
521 do_check_eq(command.args[0], uri);
522 do_check_eq(command.args[1], engine.localID);
523 do_check_eq(command.args[2], title);
525 do_check_true(tracker.score > initialScore);
526 do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
528 _("Ensure unknown client IDs result in exception.");
529 let unknownId = Utils.makeGUID();
530 let error;
532 try {
533 engine.sendURIToClientForDisplay(uri, unknownId);
534 } catch (ex) {
535 error = ex;
536 }
538 do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
540 run_next_test();
541 });
543 add_test(function test_receive_display_uri() {
544 _("Ensure processing of received 'displayURI' commands works.");
546 // We don't set up WBOs and perform syncing because other tests verify
547 // the command API works as advertised. This saves us a little work.
549 let uri = "http://www.mozilla.org/";
550 let remoteId = Utils.makeGUID();
551 let title = "Page Title!";
553 let command = {
554 command: "displayURI",
555 args: [uri, remoteId, title],
556 };
558 engine.localCommands = [command];
560 // Received 'displayURI' command should result in the topic defined below
561 // being called.
562 let ev = "weave:engine:clients:display-uri";
564 let handler = function(subject, data) {
565 Svc.Obs.remove(ev, handler);
567 do_check_eq(subject.uri, uri);
568 do_check_eq(subject.client, remoteId);
569 do_check_eq(subject.title, title);
570 do_check_eq(data, null);
572 run_next_test();
573 };
575 Svc.Obs.add(ev, handler);
577 do_check_true(engine.processIncomingCommands());
578 });
580 function run_test() {
581 initTestLogging("Trace");
582 Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
583 run_next_test();
584 }