|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 * http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
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"); |
|
11 |
|
12 const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days |
|
13 const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day |
|
14 |
|
15 let engine = Service.clientsEngine; |
|
16 |
|
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); |
|
23 |
|
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; |
|
30 |
|
31 let cleartext = rec.decrypt(Service.collectionKeys.keyForCollection("clients")); |
|
32 |
|
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 } |
|
39 |
|
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"); |
|
61 |
|
62 function check_clients_count(expectedCount) { |
|
63 let stack = Components.stack.caller; |
|
64 let coll = user.collection("clients"); |
|
65 |
|
66 // Treat a non-existent collection as empty. |
|
67 do_check_eq(expectedCount, coll ? coll.count() : 0, stack); |
|
68 } |
|
69 |
|
70 function check_client_deleted(id) { |
|
71 let coll = user.collection("clients"); |
|
72 let wbo = coll.wbo(id); |
|
73 return !wbo || !wbo.payload; |
|
74 } |
|
75 |
|
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 } |
|
82 |
|
83 try { |
|
84 ensureLegacyIdentityManager(); |
|
85 let passphrase = "abcdeabcdeabcdeabcdeabcdea"; |
|
86 Service.serverURL = server.baseURI; |
|
87 Service.login("foo", "ilovejane", passphrase); |
|
88 |
|
89 generateNewKeys(Service.collectionKeys); |
|
90 |
|
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); |
|
97 |
|
98 // Our uploaded record has a version. |
|
99 check_record_version(user, engine.localID); |
|
100 |
|
101 // Initial setup can wipe the server, so clean up. |
|
102 deletedCollections = []; |
|
103 deletedItems = []; |
|
104 |
|
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); |
|
113 |
|
114 _("Sync."); |
|
115 engine._sync(); |
|
116 |
|
117 _("Old record " + oldLocalID + " was deleted, new one uploaded."); |
|
118 check_clients_count(1); |
|
119 check_client_deleted(oldLocalID); |
|
120 |
|
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(); |
|
131 |
|
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); |
|
136 |
|
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); |
|
146 |
|
147 uploadNewKeys(); |
|
148 |
|
149 // Sync once to upload a record. |
|
150 engine._sync(); |
|
151 check_clients_count(1); |
|
152 |
|
153 // Generate and upload new keys, so the old client record is wrong. |
|
154 uploadNewKeys(); |
|
155 |
|
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(); |
|
164 |
|
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)); |
|
173 |
|
174 } finally { |
|
175 Svc.Prefs.resetBranch(""); |
|
176 Service.recordManager.clearCache(); |
|
177 server.stop(run_next_test); |
|
178 } |
|
179 }); |
|
180 |
|
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); |
|
186 |
|
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 }); |
|
195 |
|
196 add_test(function test_sync() { |
|
197 _("Ensure that Clients engine uploads a new client record once a week."); |
|
198 |
|
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"); |
|
207 |
|
208 new SyncTestingInfrastructure(server.server); |
|
209 generateNewKeys(Service.collectionKeys); |
|
210 |
|
211 function clientWBO() { |
|
212 return user.collection("clients").wbo(engine.localID); |
|
213 } |
|
214 |
|
215 try { |
|
216 |
|
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); |
|
223 |
|
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); |
|
231 |
|
232 _("Remove client record."); |
|
233 engine.removeClientData(); |
|
234 do_check_eq(clientWBO().payload, undefined); |
|
235 |
|
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); |
|
242 |
|
243 } finally { |
|
244 Svc.Prefs.resetBranch(""); |
|
245 Service.recordManager.clearCache(); |
|
246 server.stop(run_next_test); |
|
247 } |
|
248 }); |
|
249 |
|
250 add_test(function test_client_name_change() { |
|
251 _("Ensure client name change incurs a client record update."); |
|
252 |
|
253 let tracker = engine._tracker; |
|
254 |
|
255 let localID = engine.localID; |
|
256 let initialName = engine.localName; |
|
257 |
|
258 Svc.Obs.notify("weave:engine:start-tracking"); |
|
259 _("initial name: " + initialName); |
|
260 |
|
261 // Tracker already has data, so clear it. |
|
262 tracker.clearChangedIDs(); |
|
263 |
|
264 let initialScore = tracker.score; |
|
265 |
|
266 do_check_eq(Object.keys(tracker.changedIDs).length, 0); |
|
267 |
|
268 Svc.Prefs.set("client.name", "new name"); |
|
269 |
|
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); |
|
276 |
|
277 Svc.Obs.notify("weave:engine:stop-tracking"); |
|
278 |
|
279 run_next_test(); |
|
280 }); |
|
281 |
|
282 add_test(function test_send_command() { |
|
283 _("Verifies _sendCommandToClient puts commands in the outbound queue."); |
|
284 |
|
285 let store = engine._store; |
|
286 let tracker = engine._tracker; |
|
287 let remoteId = Utils.makeGUID(); |
|
288 let rec = new ClientsRec("clients", remoteId); |
|
289 |
|
290 store.create(rec); |
|
291 let remoteRecord = store.createRecord(remoteId, "clients"); |
|
292 |
|
293 let action = "testCommand"; |
|
294 let args = ["foo", "bar"]; |
|
295 |
|
296 engine._sendCommandToClient(action, args, remoteId); |
|
297 |
|
298 let newRecord = store._remoteClients[remoteId]; |
|
299 do_check_neq(newRecord, undefined); |
|
300 do_check_eq(newRecord.commands.length, 1); |
|
301 |
|
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); |
|
306 |
|
307 do_check_neq(tracker.changedIDs[remoteId], undefined); |
|
308 |
|
309 run_next_test(); |
|
310 }); |
|
311 |
|
312 add_test(function test_command_validation() { |
|
313 _("Verifies that command validation works properly."); |
|
314 |
|
315 let store = engine._store; |
|
316 |
|
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 ]; |
|
330 |
|
331 for each (let [action, args, expectedResult] in testCommands) { |
|
332 let remoteId = Utils.makeGUID(); |
|
333 let rec = new ClientsRec("clients", remoteId); |
|
334 |
|
335 store.create(rec); |
|
336 store.createRecord(remoteId, "clients"); |
|
337 |
|
338 engine.sendCommand(action, args, remoteId); |
|
339 |
|
340 let newRecord = store._remoteClients[remoteId]; |
|
341 do_check_neq(newRecord, undefined); |
|
342 |
|
343 if (expectedResult) { |
|
344 _("Ensuring command is sent: " + action); |
|
345 do_check_eq(newRecord.commands.length, 1); |
|
346 |
|
347 let command = newRecord.commands[0]; |
|
348 do_check_eq(command.command, action); |
|
349 do_check_eq(command.args, args); |
|
350 |
|
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); |
|
356 |
|
357 if (store._tracker) { |
|
358 do_check_eq(engine._tracker[remoteId], undefined); |
|
359 } |
|
360 } |
|
361 |
|
362 } |
|
363 run_next_test(); |
|
364 }); |
|
365 |
|
366 add_test(function test_command_duplication() { |
|
367 _("Ensures duplicate commands are detected and not added"); |
|
368 |
|
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"); |
|
374 |
|
375 let action = "resetAll"; |
|
376 let args = []; |
|
377 |
|
378 engine.sendCommand(action, args, remoteId); |
|
379 engine.sendCommand(action, args, remoteId); |
|
380 |
|
381 let newRecord = store._remoteClients[remoteId]; |
|
382 do_check_eq(newRecord.commands.length, 1); |
|
383 |
|
384 _("Check variant args length"); |
|
385 newRecord.commands = []; |
|
386 |
|
387 action = "resetEngine"; |
|
388 engine.sendCommand(action, [{ x: "foo" }], remoteId); |
|
389 engine.sendCommand(action, [{ x: "bar" }], remoteId); |
|
390 |
|
391 _("Make sure we spot a real dupe argument."); |
|
392 engine.sendCommand(action, [{ x: "bar" }], remoteId); |
|
393 |
|
394 do_check_eq(newRecord.commands.length, 2); |
|
395 |
|
396 run_next_test(); |
|
397 }); |
|
398 |
|
399 add_test(function test_command_invalid_client() { |
|
400 _("Ensures invalid client IDs are caught"); |
|
401 |
|
402 let id = Utils.makeGUID(); |
|
403 let error; |
|
404 |
|
405 try { |
|
406 engine.sendCommand("wipeAll", [], id); |
|
407 } catch (ex) { |
|
408 error = ex; |
|
409 } |
|
410 |
|
411 do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); |
|
412 |
|
413 run_next_test(); |
|
414 }); |
|
415 |
|
416 add_test(function test_process_incoming_commands() { |
|
417 _("Ensures local commands are executed"); |
|
418 |
|
419 engine.localCommands = [{ command: "logout", args: [] }]; |
|
420 |
|
421 let ev = "weave:service:logout:finish"; |
|
422 |
|
423 var handler = function() { |
|
424 Svc.Obs.remove(ev, handler); |
|
425 run_next_test(); |
|
426 }; |
|
427 |
|
428 Svc.Obs.add(ev, handler); |
|
429 |
|
430 // logout command causes processIncomingCommands to return explicit false. |
|
431 do_check_false(engine.processIncomingCommands()); |
|
432 }); |
|
433 |
|
434 add_test(function test_command_sync() { |
|
435 _("Ensure that commands are synced across clients."); |
|
436 |
|
437 engine._store.wipe(); |
|
438 generateNewKeys(Service.collectionKeys); |
|
439 |
|
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); |
|
448 |
|
449 let user = server.user("foo"); |
|
450 let remoteId = Utils.makeGUID(); |
|
451 |
|
452 function clientWBO(id) { |
|
453 return user.collection("clients").wbo(id); |
|
454 } |
|
455 |
|
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", []); |
|
461 |
|
462 let clientRecord = engine._store._remoteClients[remoteId]; |
|
463 do_check_neq(clientRecord, undefined); |
|
464 do_check_eq(clientRecord.commands.length, 1); |
|
465 |
|
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); |
|
472 |
|
473 do_check_neq(clientWBO(remoteId).payload, undefined); |
|
474 |
|
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); |
|
482 |
|
483 let command = engine.localCommands[0]; |
|
484 do_check_eq(command.command, "wipeAll"); |
|
485 do_check_eq(command.args.length, 0); |
|
486 |
|
487 } finally { |
|
488 Svc.Prefs.resetBranch(""); |
|
489 Service.recordManager.clearCache(); |
|
490 server.stop(run_next_test); |
|
491 } |
|
492 }); |
|
493 |
|
494 add_test(function test_send_uri_to_client_for_display() { |
|
495 _("Ensure sendURIToClientForDisplay() sends command properly."); |
|
496 |
|
497 let tracker = engine._tracker; |
|
498 let store = engine._store; |
|
499 |
|
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"); |
|
505 |
|
506 tracker.clearChangedIDs(); |
|
507 let initialScore = tracker.score; |
|
508 |
|
509 let uri = "http://www.mozilla.org/"; |
|
510 let title = "Title of the Page"; |
|
511 engine.sendURIToClientForDisplay(uri, remoteId, title); |
|
512 |
|
513 let newRecord = store._remoteClients[remoteId]; |
|
514 |
|
515 do_check_neq(newRecord, undefined); |
|
516 do_check_eq(newRecord.commands.length, 1); |
|
517 |
|
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); |
|
524 |
|
525 do_check_true(tracker.score > initialScore); |
|
526 do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE); |
|
527 |
|
528 _("Ensure unknown client IDs result in exception."); |
|
529 let unknownId = Utils.makeGUID(); |
|
530 let error; |
|
531 |
|
532 try { |
|
533 engine.sendURIToClientForDisplay(uri, unknownId); |
|
534 } catch (ex) { |
|
535 error = ex; |
|
536 } |
|
537 |
|
538 do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0); |
|
539 |
|
540 run_next_test(); |
|
541 }); |
|
542 |
|
543 add_test(function test_receive_display_uri() { |
|
544 _("Ensure processing of received 'displayURI' commands works."); |
|
545 |
|
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. |
|
548 |
|
549 let uri = "http://www.mozilla.org/"; |
|
550 let remoteId = Utils.makeGUID(); |
|
551 let title = "Page Title!"; |
|
552 |
|
553 let command = { |
|
554 command: "displayURI", |
|
555 args: [uri, remoteId, title], |
|
556 }; |
|
557 |
|
558 engine.localCommands = [command]; |
|
559 |
|
560 // Received 'displayURI' command should result in the topic defined below |
|
561 // being called. |
|
562 let ev = "weave:engine:clients:display-uri"; |
|
563 |
|
564 let handler = function(subject, data) { |
|
565 Svc.Obs.remove(ev, handler); |
|
566 |
|
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); |
|
571 |
|
572 run_next_test(); |
|
573 }; |
|
574 |
|
575 Svc.Obs.add(ev, handler); |
|
576 |
|
577 do_check_true(engine.processIncomingCommands()); |
|
578 }); |
|
579 |
|
580 function run_test() { |
|
581 initTestLogging("Trace"); |
|
582 Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace; |
|
583 run_next_test(); |
|
584 } |