test_accounts_device_registration.js (33846B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { FxAccounts } = ChromeUtils.importESModule( 7 "resource://gre/modules/FxAccounts.sys.mjs" 8 ); 9 const { FxAccountsClient } = ChromeUtils.importESModule( 10 "resource://gre/modules/FxAccountsClient.sys.mjs" 11 ); 12 const { FxAccountsDevice } = ChromeUtils.importESModule( 13 "resource://gre/modules/FxAccountsDevice.sys.mjs" 14 ); 15 const { 16 ERRNO_DEVICE_SESSION_CONFLICT, 17 ERRNO_TOO_MANY_CLIENT_REQUESTS, 18 ERRNO_UNKNOWN_DEVICE, 19 ON_DEVICE_CONNECTED_NOTIFICATION, 20 ON_DEVICE_DISCONNECTED_NOTIFICATION, 21 ON_DEVICELIST_UPDATED, 22 } = ChromeUtils.importESModule( 23 "resource://gre/modules/FxAccountsCommon.sys.mjs" 24 ); 25 var { AccountState } = ChromeUtils.importESModule( 26 "resource://gre/modules/FxAccounts.sys.mjs" 27 ); 28 29 initTestLogging("Trace"); 30 31 var log = Log.repository.getLogger("Services.FxAccounts.test"); 32 log.level = Log.Level.Debug; 33 34 const BOGUS_PUBLICKEY = 35 "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc"; 36 const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw"; 37 38 Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace"); 39 40 const DEVICE_REGISTRATION_VERSION = 42; 41 42 function MockStorageManager() {} 43 44 MockStorageManager.prototype = { 45 initialize(accountData) { 46 this.accountData = accountData; 47 }, 48 49 finalize() { 50 return Promise.resolve(); 51 }, 52 53 getAccountData() { 54 return Promise.resolve(this.accountData); 55 }, 56 57 updateAccountData(updatedFields) { 58 for (let [name, value] of Object.entries(updatedFields)) { 59 if (value == null) { 60 delete this.accountData[name]; 61 } else { 62 this.accountData[name] = value; 63 } 64 } 65 return Promise.resolve(); 66 }, 67 68 deleteAccountData() { 69 this.accountData = null; 70 return Promise.resolve(); 71 }, 72 }; 73 74 function MockFxAccountsClient(device) { 75 this._email = "nobody@example.com"; 76 // Be careful relying on `this._verified` as it doesn't change if the user's 77 // state does via setting the `verified` flag in the user data. 78 this._verified = false; 79 this._deletedOnServer = false; // for testing accountStatus 80 81 // mock calls up to the auth server to determine whether the 82 // user account has been verified 83 this.recoveryEmailStatus = function () { 84 // simulate a call to /recovery_email/status 85 return Promise.resolve({ 86 email: this._email, 87 verified: this._verified, 88 }); 89 }; 90 91 this.accountKeys = function (keyFetchToken) { 92 Assert.ok(keyFetchToken, "must be called with a key-fetch-token"); 93 // ideally we'd check the verification status here to more closely simulate 94 // the server, but `this._verified` is a test-only construct and doesn't 95 // update when the user changes verification status. 96 Assert.ok(!this._deletedOnServer, "this test thinks the acct is deleted!"); 97 return { 98 kA: "test-ka", 99 wrapKB: "X".repeat(32), 100 }; 101 }; 102 103 this.accountStatus = function (uid) { 104 return Promise.resolve(!!uid && !this._deletedOnServer); 105 }; 106 107 this.registerDevice = (st, name) => Promise.resolve({ id: device.id, name }); 108 this.updateDevice = (st, id, name) => Promise.resolve({ id, name }); 109 this.signOut = () => Promise.resolve({}); 110 this.getDeviceList = st => 111 Promise.resolve([ 112 { 113 id: device.id, 114 name: device.name, 115 type: device.type, 116 pushCallback: device.pushCallback, 117 pushEndpointExpired: device.pushEndpointExpired, 118 isCurrentDevice: st === device.sessionToken, 119 }, 120 ]); 121 122 FxAccountsClient.apply(this); 123 } 124 MockFxAccountsClient.prototype = {}; 125 Object.setPrototypeOf( 126 MockFxAccountsClient.prototype, 127 FxAccountsClient.prototype 128 ); 129 130 async function MockFxAccounts(credentials, device = {}) { 131 let fxa = new FxAccounts({ 132 newAccountState(creds) { 133 // we use a real accountState but mocked storage. 134 let storage = new MockStorageManager(); 135 storage.initialize(creds); 136 return new AccountState(storage); 137 }, 138 fxAccountsClient: new MockFxAccountsClient(device, credentials), 139 fxaPushService: { 140 registerPushEndpoint() { 141 return new Promise(resolve => { 142 resolve({ 143 endpoint: "http://mochi.test:8888", 144 getKey(type) { 145 return ChromeUtils.base64URLDecode( 146 type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, 147 { padding: "ignore" } 148 ); 149 }, 150 }); 151 }); 152 }, 153 unsubscribe() { 154 return Promise.resolve(); 155 }, 156 }, 157 commands: { 158 async availableCommands() { 159 return {}; 160 }, 161 }, 162 device: { 163 DEVICE_REGISTRATION_VERSION, 164 _checkRemoteCommandsUpdateNeeded: async () => false, 165 }, 166 VERIFICATION_POLL_TIMEOUT_INITIAL: 1, 167 }); 168 fxa._internal.device._fxai = fxa._internal; 169 await fxa._internal.setSignedInUser(credentials); 170 Services.prefs.setStringPref( 171 "identity.fxaccounts.account.device.name", 172 device.name || "mock device name" 173 ); 174 return fxa; 175 } 176 177 function updateUserAccountData(fxa, data) { 178 return fxa._internal.updateUserAccountData(data); 179 } 180 181 add_task(async function test_updateDeviceRegistration_with_new_device() { 182 const deviceName = "foo"; 183 const deviceType = "bar"; 184 185 const credentials = getTestUser("baz"); 186 const fxa = await MockFxAccounts(credentials, { name: deviceName }); 187 // Remove the current device registration (setSignedInUser does one!). 188 await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); 189 190 const spy = { 191 registerDevice: { count: 0, args: [] }, 192 updateDevice: { count: 0, args: [] }, 193 getDeviceList: { count: 0, args: [] }, 194 }; 195 const client = fxa._internal.fxAccountsClient; 196 client.registerDevice = function () { 197 spy.registerDevice.count += 1; 198 spy.registerDevice.args.push(arguments); 199 return Promise.resolve({ 200 id: "newly-generated device id", 201 createdAt: Date.now(), 202 name: deviceName, 203 type: deviceType, 204 }); 205 }; 206 client.updateDevice = function () { 207 spy.updateDevice.count += 1; 208 spy.updateDevice.args.push(arguments); 209 return Promise.resolve({}); 210 }; 211 client.getDeviceList = function () { 212 spy.getDeviceList.count += 1; 213 spy.getDeviceList.args.push(arguments); 214 return Promise.resolve([]); 215 }; 216 217 await fxa.updateDeviceRegistration(); 218 219 Assert.equal(spy.updateDevice.count, 0); 220 Assert.equal(spy.getDeviceList.count, 0); 221 Assert.equal(spy.registerDevice.count, 1); 222 Assert.equal(spy.registerDevice.args[0].length, 4); 223 Assert.equal(spy.registerDevice.args[0][0], credentials.sessionToken); 224 Assert.equal(spy.registerDevice.args[0][1], deviceName); 225 Assert.equal(spy.registerDevice.args[0][2], "desktop"); 226 Assert.equal( 227 spy.registerDevice.args[0][3].pushCallback, 228 "http://mochi.test:8888" 229 ); 230 Assert.equal(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); 231 Assert.equal(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); 232 233 const state = fxa._internal.currentAccountState; 234 const data = await state.getUserAccountData(); 235 236 Assert.equal(data.device.id, "newly-generated device id"); 237 Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); 238 await fxa.signOut(true); 239 }); 240 241 add_task(async function test_updateDeviceRegistration_with_existing_device() { 242 const deviceId = "my device id"; 243 const deviceName = "phil's device"; 244 245 const credentials = getTestUser("pb"); 246 const fxa = await MockFxAccounts(credentials, { name: deviceName }); 247 await updateUserAccountData(fxa, { 248 uid: credentials.uid, 249 device: { 250 id: deviceId, 251 registeredCommandsKeys: [], 252 registrationVersion: 1, // < 42 253 }, 254 }); 255 256 const spy = { 257 registerDevice: { count: 0, args: [] }, 258 updateDevice: { count: 0, args: [] }, 259 getDeviceList: { count: 0, args: [] }, 260 }; 261 const client = fxa._internal.fxAccountsClient; 262 client.registerDevice = function () { 263 spy.registerDevice.count += 1; 264 spy.registerDevice.args.push(arguments); 265 return Promise.resolve({}); 266 }; 267 client.updateDevice = function () { 268 spy.updateDevice.count += 1; 269 spy.updateDevice.args.push(arguments); 270 return Promise.resolve({ 271 id: deviceId, 272 name: deviceName, 273 }); 274 }; 275 client.getDeviceList = function () { 276 spy.getDeviceList.count += 1; 277 spy.getDeviceList.args.push(arguments); 278 return Promise.resolve([]); 279 }; 280 await fxa.updateDeviceRegistration(); 281 282 Assert.equal(spy.registerDevice.count, 0); 283 Assert.equal(spy.getDeviceList.count, 0); 284 Assert.equal(spy.updateDevice.count, 1); 285 Assert.equal(spy.updateDevice.args[0].length, 4); 286 Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); 287 Assert.equal(spy.updateDevice.args[0][1], deviceId); 288 Assert.equal(spy.updateDevice.args[0][2], deviceName); 289 Assert.equal( 290 spy.updateDevice.args[0][3].pushCallback, 291 "http://mochi.test:8888" 292 ); 293 Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); 294 Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); 295 296 const state = fxa._internal.currentAccountState; 297 const data = await state.getUserAccountData(); 298 299 Assert.equal(data.device.id, deviceId); 300 Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); 301 await fxa.signOut(true); 302 }); 303 304 add_task( 305 async function test_updateDeviceRegistration_with_unknown_device_error() { 306 const deviceName = "foo"; 307 const deviceType = "bar"; 308 const currentDeviceId = "my device id"; 309 310 const credentials = getTestUser("baz"); 311 const fxa = await MockFxAccounts(credentials, { name: deviceName }); 312 await updateUserAccountData(fxa, { 313 uid: credentials.uid, 314 device: { 315 id: currentDeviceId, 316 registeredCommandsKeys: [], 317 registrationVersion: 1, // < 42 318 }, 319 }); 320 321 const spy = { 322 registerDevice: { count: 0, args: [] }, 323 updateDevice: { count: 0, args: [] }, 324 getDeviceList: { count: 0, args: [] }, 325 }; 326 const client = fxa._internal.fxAccountsClient; 327 client.registerDevice = function () { 328 spy.registerDevice.count += 1; 329 spy.registerDevice.args.push(arguments); 330 return Promise.resolve({ 331 id: "a different newly-generated device id", 332 createdAt: Date.now(), 333 name: deviceName, 334 type: deviceType, 335 }); 336 }; 337 client.updateDevice = function () { 338 spy.updateDevice.count += 1; 339 spy.updateDevice.args.push(arguments); 340 return Promise.reject({ 341 code: 400, 342 errno: ERRNO_UNKNOWN_DEVICE, 343 }); 344 }; 345 client.getDeviceList = function () { 346 spy.getDeviceList.count += 1; 347 spy.getDeviceList.args.push(arguments); 348 return Promise.resolve([]); 349 }; 350 351 await fxa.updateDeviceRegistration(); 352 353 Assert.equal(spy.getDeviceList.count, 0); 354 Assert.equal(spy.registerDevice.count, 0); 355 Assert.equal(spy.updateDevice.count, 1); 356 Assert.equal(spy.updateDevice.args[0].length, 4); 357 Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); 358 Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); 359 Assert.equal(spy.updateDevice.args[0][2], deviceName); 360 Assert.equal( 361 spy.updateDevice.args[0][3].pushCallback, 362 "http://mochi.test:8888" 363 ); 364 Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); 365 Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); 366 367 const state = fxa._internal.currentAccountState; 368 const data = await state.getUserAccountData(); 369 370 Assert.equal(null, data.device); 371 await fxa.signOut(true); 372 } 373 ); 374 375 add_task( 376 async function test_updateDeviceRegistration_with_device_session_conflict_error() { 377 const deviceName = "foo"; 378 const deviceType = "bar"; 379 const currentDeviceId = "my device id"; 380 const conflictingDeviceId = "conflicting device id"; 381 382 const credentials = getTestUser("baz"); 383 const fxa = await MockFxAccounts(credentials, { name: deviceName }); 384 await updateUserAccountData(fxa, { 385 uid: credentials.uid, 386 device: { 387 id: currentDeviceId, 388 registeredCommandsKeys: [], 389 registrationVersion: 1, // < 42 390 }, 391 }); 392 393 const spy = { 394 registerDevice: { count: 0, args: [] }, 395 updateDevice: { count: 0, args: [], times: [] }, 396 getDeviceList: { count: 0, args: [] }, 397 }; 398 const client = fxa._internal.fxAccountsClient; 399 client.registerDevice = function () { 400 spy.registerDevice.count += 1; 401 spy.registerDevice.args.push(arguments); 402 return Promise.resolve({}); 403 }; 404 client.updateDevice = function () { 405 spy.updateDevice.count += 1; 406 spy.updateDevice.args.push(arguments); 407 spy.updateDevice.time = Date.now(); 408 if (spy.updateDevice.count === 1) { 409 return Promise.reject({ 410 code: 400, 411 errno: ERRNO_DEVICE_SESSION_CONFLICT, 412 }); 413 } 414 return Promise.resolve({ 415 id: conflictingDeviceId, 416 name: deviceName, 417 }); 418 }; 419 client.getDeviceList = function () { 420 spy.getDeviceList.count += 1; 421 spy.getDeviceList.args.push(arguments); 422 spy.getDeviceList.time = Date.now(); 423 return Promise.resolve([ 424 { 425 id: "ignore", 426 name: "ignore", 427 type: "ignore", 428 isCurrentDevice: false, 429 }, 430 { 431 id: conflictingDeviceId, 432 name: deviceName, 433 type: deviceType, 434 isCurrentDevice: true, 435 }, 436 ]); 437 }; 438 439 await fxa.updateDeviceRegistration(); 440 441 Assert.equal(spy.registerDevice.count, 0); 442 Assert.equal(spy.updateDevice.count, 1); 443 Assert.equal(spy.updateDevice.args[0].length, 4); 444 Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); 445 Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); 446 Assert.equal(spy.updateDevice.args[0][2], deviceName); 447 Assert.equal( 448 spy.updateDevice.args[0][3].pushCallback, 449 "http://mochi.test:8888" 450 ); 451 Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); 452 Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); 453 Assert.equal(spy.getDeviceList.count, 1); 454 Assert.equal(spy.getDeviceList.args[0].length, 1); 455 Assert.equal(spy.getDeviceList.args[0][0], credentials.sessionToken); 456 Assert.greaterOrEqual(spy.getDeviceList.time, spy.updateDevice.time); 457 458 const state = fxa._internal.currentAccountState; 459 const data = await state.getUserAccountData(); 460 461 Assert.equal(data.device.id, conflictingDeviceId); 462 Assert.equal(data.device.registrationVersion, null); 463 await fxa.signOut(true); 464 } 465 ); 466 467 add_task( 468 async function test_updateDeviceRegistration_with_unrecoverable_error() { 469 const deviceName = "foo"; 470 471 const credentials = getTestUser("baz"); 472 const fxa = await MockFxAccounts(credentials, { name: deviceName }); 473 await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); 474 475 const spy = { 476 registerDevice: { count: 0, args: [] }, 477 updateDevice: { count: 0, args: [] }, 478 getDeviceList: { count: 0, args: [] }, 479 }; 480 const client = fxa._internal.fxAccountsClient; 481 client.registerDevice = function () { 482 spy.registerDevice.count += 1; 483 spy.registerDevice.args.push(arguments); 484 return Promise.reject({ 485 code: 400, 486 errno: ERRNO_TOO_MANY_CLIENT_REQUESTS, 487 }); 488 }; 489 client.updateDevice = function () { 490 spy.updateDevice.count += 1; 491 spy.updateDevice.args.push(arguments); 492 return Promise.resolve({}); 493 }; 494 client.getDeviceList = function () { 495 spy.getDeviceList.count += 1; 496 spy.getDeviceList.args.push(arguments); 497 return Promise.resolve([]); 498 }; 499 500 await fxa.updateDeviceRegistration(); 501 502 Assert.equal(spy.getDeviceList.count, 0); 503 Assert.equal(spy.updateDevice.count, 0); 504 Assert.equal(spy.registerDevice.count, 1); 505 Assert.equal(spy.registerDevice.args[0].length, 4); 506 507 const state = fxa._internal.currentAccountState; 508 const data = await state.getUserAccountData(); 509 510 Assert.equal(null, data.device); 511 await fxa.signOut(true); 512 } 513 ); 514 515 add_task( 516 async function test_getDeviceId_with_no_device_id_invokes_device_registration() { 517 const credentials = getTestUser("foo"); 518 credentials.verified = true; 519 const fxa = await MockFxAccounts(credentials); 520 await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); 521 522 const spy = { count: 0, args: [] }; 523 fxa._internal.currentAccountState.getUserAccountData = () => 524 Promise.resolve({ 525 email: credentials.email, 526 registrationVersion: DEVICE_REGISTRATION_VERSION, 527 }); 528 fxa._internal.device._registerOrUpdateDevice = function () { 529 spy.count += 1; 530 spy.args.push(arguments); 531 return Promise.resolve("bar"); 532 }; 533 534 const result = await fxa.device.getLocalId(); 535 536 Assert.equal(spy.count, 1); 537 Assert.equal(spy.args[0].length, 2); 538 Assert.equal(spy.args[0][1].email, credentials.email); 539 Assert.equal(null, spy.args[0][1].device); 540 Assert.equal(result, "bar"); 541 await fxa.signOut(true); 542 } 543 ); 544 545 add_task( 546 async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() { 547 const credentials = getTestUser("foo"); 548 credentials.verified = true; 549 const fxa = await MockFxAccounts(credentials); 550 551 const spy = { count: 0, args: [] }; 552 fxa._internal.currentAccountState.getUserAccountData = () => 553 Promise.resolve({ 554 device: { 555 id: "my id", 556 registrationVersion: 0, 557 registeredCommandsKeys: [], 558 }, 559 }); 560 fxa._internal.device._registerOrUpdateDevice = function () { 561 spy.count += 1; 562 spy.args.push(arguments); 563 return Promise.resolve("wibble"); 564 }; 565 566 const result = await fxa.device.getLocalId(); 567 568 Assert.equal(spy.count, 1); 569 Assert.equal(spy.args[0].length, 2); 570 Assert.equal(spy.args[0][1].device.id, "my id"); 571 Assert.equal(result, "wibble"); 572 await fxa.signOut(true); 573 } 574 ); 575 576 add_task( 577 async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() { 578 const credentials = getTestUser("foo"); 579 credentials.verified = true; 580 const fxa = await MockFxAccounts(credentials); 581 582 const spy = { count: 0 }; 583 fxa._internal.currentAccountState.getUserAccountData = async () => ({ 584 device: { 585 id: "foo's device id", 586 registrationVersion: DEVICE_REGISTRATION_VERSION, 587 registeredCommandsKeys: [], 588 }, 589 }); 590 fxa._internal.device._registerOrUpdateDevice = function () { 591 spy.count += 1; 592 return Promise.resolve("bar"); 593 }; 594 595 const result = await fxa.device.getLocalId(); 596 597 Assert.equal(spy.count, 0); 598 Assert.equal(result, "foo's device id"); 599 await fxa.signOut(true); 600 } 601 ); 602 603 add_task( 604 async function test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() { 605 const credentials = getTestUser("foo"); 606 credentials.verified = true; 607 const fxa = await MockFxAccounts(credentials); 608 609 const spy = { count: 0, args: [] }; 610 fxa._internal.currentAccountState.getUserAccountData = () => 611 Promise.resolve({ device: { id: "wibble" } }); 612 fxa._internal.device._registerOrUpdateDevice = function () { 613 spy.count += 1; 614 spy.args.push(arguments); 615 return Promise.resolve("wibble"); 616 }; 617 618 const result = await fxa.device.getLocalId(); 619 620 Assert.equal(spy.count, 1); 621 Assert.equal(spy.args[0].length, 2); 622 Assert.equal(spy.args[0][1].device.id, "wibble"); 623 Assert.equal(result, "wibble"); 624 await fxa.signOut(true); 625 } 626 ); 627 628 add_task(async function test_devicelist_pushendpointexpired() { 629 const deviceId = "mydeviceid"; 630 const credentials = getTestUser("baz"); 631 credentials.verified = true; 632 const fxa = await MockFxAccounts(credentials); 633 await updateUserAccountData(fxa, { 634 uid: credentials.uid, 635 device: { 636 id: deviceId, 637 registeredCommandsKeys: [], 638 registrationVersion: 1, // < 42 639 }, 640 }); 641 642 const spy = { 643 updateDevice: { count: 0, args: [] }, 644 getDeviceList: { count: 0, args: [] }, 645 }; 646 const client = fxa._internal.fxAccountsClient; 647 client.updateDevice = function () { 648 spy.updateDevice.count += 1; 649 spy.updateDevice.args.push(arguments); 650 return Promise.resolve({}); 651 }; 652 client.getDeviceList = function () { 653 spy.getDeviceList.count += 1; 654 spy.getDeviceList.args.push(arguments); 655 return Promise.resolve([ 656 { 657 id: "mydeviceid", 658 name: "foo", 659 type: "desktop", 660 isCurrentDevice: true, 661 pushEndpointExpired: true, 662 pushCallback: "https://example.com", 663 }, 664 ]); 665 }; 666 let polledForMissedCommands = false; 667 fxa._internal.commands.pollDeviceCommands = () => { 668 polledForMissedCommands = true; 669 }; 670 671 await fxa.device.refreshDeviceList(); 672 673 Assert.equal(spy.getDeviceList.count, 1); 674 Assert.equal(spy.updateDevice.count, 1); 675 Assert.ok(polledForMissedCommands); 676 await fxa.signOut(true); 677 }); 678 679 add_task(async function test_devicelist_nopushcallback() { 680 const deviceId = "mydeviceid"; 681 const credentials = getTestUser("baz"); 682 credentials.verified = true; 683 const fxa = await MockFxAccounts(credentials); 684 await updateUserAccountData(fxa, { 685 uid: credentials.uid, 686 device: { 687 id: deviceId, 688 registeredCommandsKeys: [], 689 registrationVersion: 1, 690 }, 691 }); 692 693 const spy = { 694 updateDevice: { count: 0, args: [] }, 695 getDeviceList: { count: 0, args: [] }, 696 }; 697 const client = fxa._internal.fxAccountsClient; 698 client.updateDevice = function () { 699 spy.updateDevice.count += 1; 700 spy.updateDevice.args.push(arguments); 701 return Promise.resolve({}); 702 }; 703 client.getDeviceList = function () { 704 spy.getDeviceList.count += 1; 705 spy.getDeviceList.args.push(arguments); 706 return Promise.resolve([ 707 { 708 id: "mydeviceid", 709 name: "foo", 710 type: "desktop", 711 isCurrentDevice: true, 712 pushEndpointExpired: false, 713 pushCallback: null, 714 }, 715 ]); 716 }; 717 718 let polledForMissedCommands = false; 719 fxa._internal.commands.pollDeviceCommands = () => { 720 polledForMissedCommands = true; 721 }; 722 723 await fxa.device.refreshDeviceList(); 724 725 Assert.equal(spy.getDeviceList.count, 1); 726 Assert.equal(spy.updateDevice.count, 1); 727 Assert.ok(polledForMissedCommands); 728 await fxa.signOut(true); 729 }); 730 731 add_task(async function test_refreshDeviceList() { 732 let credentials = getTestUser("baz"); 733 734 let storage = new MockStorageManager(); 735 storage.initialize(credentials); 736 let state = new AccountState(storage); 737 738 let fxAccountsClient = new MockFxAccountsClient({ 739 id: "deviceAAAAAA", 740 name: "iPhone", 741 type: "phone", 742 pushCallback: "http://mochi.test:8888", 743 pushEndpointExpired: false, 744 sessionToken: credentials.sessionToken, 745 }); 746 let spy = { 747 getDeviceList: { count: 0 }, 748 }; 749 const deviceListUpdateObserver = { 750 count: 0, 751 observe() { 752 this.count++; 753 }, 754 }; 755 Services.obs.addObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); 756 757 fxAccountsClient.getDeviceList = (function (old) { 758 return function getDeviceList() { 759 spy.getDeviceList.count += 1; 760 return old.apply(this, arguments); 761 }; 762 })(fxAccountsClient.getDeviceList); 763 let fxai = { 764 _now: Date.now(), 765 _generation: 0, 766 fxAccountsClient, 767 now() { 768 return this._now; 769 }, 770 withVerifiedAccountState(func) { 771 // Ensure `func` is called asynchronously, and simulate the possibility 772 // of a different user signng in while the promise is in-flight. 773 const currentGeneration = this._generation; 774 return Promise.resolve() 775 .then(_ => func(state)) 776 .then(result => { 777 if (currentGeneration < this._generation) { 778 throw new Error("Another user has signed in"); 779 } 780 return result; 781 }); 782 }, 783 fxaPushService: { 784 registerPushEndpoint() { 785 return new Promise(resolve => { 786 resolve({ 787 endpoint: "http://mochi.test:8888", 788 getKey(type) { 789 return ChromeUtils.base64URLDecode( 790 type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, 791 { padding: "ignore" } 792 ); 793 }, 794 }); 795 }); 796 }, 797 unsubscribe() { 798 return Promise.resolve(); 799 }, 800 getSubscription() { 801 return Promise.resolve({ 802 isExpired: () => { 803 return false; 804 }, 805 endpoint: "http://mochi.test:8888", 806 }); 807 }, 808 }, 809 async _handleTokenError(e) { 810 _(`Test failure: ${e} - ${e.stack}`); 811 throw e; 812 }, 813 }; 814 let device = new FxAccountsDevice(fxai); 815 device._checkRemoteCommandsUpdateNeeded = async () => false; 816 817 Assert.equal( 818 device.recentDeviceList, 819 null, 820 "Should not have device list initially" 821 ); 822 Assert.ok(await device.refreshDeviceList(), "Should refresh list"); 823 Assert.equal( 824 deviceListUpdateObserver.count, 825 1, 826 `${ON_DEVICELIST_UPDATED} was notified` 827 ); 828 Assert.deepEqual( 829 device.recentDeviceList, 830 [ 831 { 832 id: "deviceAAAAAA", 833 name: "iPhone", 834 type: "phone", 835 pushCallback: "http://mochi.test:8888", 836 pushEndpointExpired: false, 837 isCurrentDevice: true, 838 }, 839 ], 840 "Should fetch device list" 841 ); 842 Assert.equal( 843 spy.getDeviceList.count, 844 1, 845 "Should make request to refresh list" 846 ); 847 Assert.ok( 848 !(await device.refreshDeviceList()), 849 "Should not refresh device list if fresh" 850 ); 851 Assert.equal( 852 deviceListUpdateObserver.count, 853 1, 854 `${ON_DEVICELIST_UPDATED} was not notified` 855 ); 856 857 fxai._now += device.TIME_BETWEEN_FXA_DEVICES_FETCH_MS; 858 859 let refreshPromise = device.refreshDeviceList(); 860 let secondRefreshPromise = device.refreshDeviceList(); 861 Assert.ok( 862 await Promise.all([refreshPromise, secondRefreshPromise]), 863 "Should refresh list if stale" 864 ); 865 Assert.equal( 866 spy.getDeviceList.count, 867 2, 868 "Should only make one request if called with pending request" 869 ); 870 Assert.equal( 871 deviceListUpdateObserver.count, 872 2, 873 `${ON_DEVICELIST_UPDATED} only notified once` 874 ); 875 876 device.observe(null, ON_DEVICE_CONNECTED_NOTIFICATION); 877 await device.refreshDeviceList(); 878 Assert.equal( 879 spy.getDeviceList.count, 880 3, 881 "Should refresh device list after connecting new device" 882 ); 883 Assert.equal( 884 deviceListUpdateObserver.count, 885 3, 886 `${ON_DEVICELIST_UPDATED} notified when new device connects` 887 ); 888 device.observe( 889 null, 890 ON_DEVICE_DISCONNECTED_NOTIFICATION, 891 JSON.stringify({ isLocalDevice: false }) 892 ); 893 await device.refreshDeviceList(); 894 Assert.equal( 895 spy.getDeviceList.count, 896 4, 897 "Should refresh device list after disconnecting device" 898 ); 899 Assert.equal( 900 deviceListUpdateObserver.count, 901 4, 902 `${ON_DEVICELIST_UPDATED} notified when device disconnects` 903 ); 904 device.observe( 905 null, 906 ON_DEVICE_DISCONNECTED_NOTIFICATION, 907 JSON.stringify({ isLocalDevice: true }) 908 ); 909 await device.refreshDeviceList(); 910 Assert.equal( 911 spy.getDeviceList.count, 912 4, 913 "Should not refresh device list after disconnecting this device" 914 ); 915 Assert.equal( 916 deviceListUpdateObserver.count, 917 4, 918 `${ON_DEVICELIST_UPDATED} not notified again` 919 ); 920 921 let refreshBeforeResetPromise = device.refreshDeviceList({ 922 ignoreCached: true, 923 }); 924 fxai._generation++; 925 Assert.equal( 926 deviceListUpdateObserver.count, 927 4, 928 `${ON_DEVICELIST_UPDATED} not notified` 929 ); 930 await Assert.rejects(refreshBeforeResetPromise, /Another user has signed in/); 931 932 device.reset(); 933 Assert.equal( 934 device.recentDeviceList, 935 null, 936 "Should clear device list after resetting" 937 ); 938 Assert.ok( 939 await device.refreshDeviceList(), 940 "Should fetch new list after resetting" 941 ); 942 Assert.equal( 943 deviceListUpdateObserver.count, 944 5, 945 `${ON_DEVICELIST_UPDATED} notified after reset` 946 ); 947 Services.obs.removeObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); 948 }); 949 950 add_task(async function test_push_resubscribe() { 951 let credentials = getTestUser("baz"); 952 953 let storage = new MockStorageManager(); 954 storage.initialize(credentials); 955 let state = new AccountState(storage); 956 957 let mockDevice = { 958 id: "deviceAAAAAA", 959 name: "iPhone", 960 type: "phone", 961 pushCallback: "http://mochi.test:8888", 962 pushEndpointExpired: false, 963 sessionToken: credentials.sessionToken, 964 }; 965 966 var mockSubscription = { 967 isExpired: () => { 968 return false; 969 }, 970 endpoint: "http://mochi.test:8888", 971 }; 972 973 let fxAccountsClient = new MockFxAccountsClient(mockDevice); 974 975 const spy = { 976 _registerOrUpdateDevice: { count: 0 }, 977 }; 978 979 let fxai = { 980 _now: Date.now(), 981 _generation: 0, 982 fxAccountsClient, 983 now() { 984 return this._now; 985 }, 986 withVerifiedAccountState(func) { 987 // Ensure `func` is called asynchronously, and simulate the possibility 988 // of a different user signng in while the promise is in-flight. 989 const currentGeneration = this._generation; 990 return Promise.resolve() 991 .then(_ => func(state)) 992 .then(result => { 993 if (currentGeneration < this._generation) { 994 throw new Error("Another user has signed in"); 995 } 996 return result; 997 }); 998 }, 999 fxaPushService: { 1000 registerPushEndpoint() { 1001 return new Promise(resolve => { 1002 resolve({ 1003 endpoint: "http://mochi.test:8888", 1004 getKey(type) { 1005 return ChromeUtils.base64URLDecode( 1006 type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, 1007 { padding: "ignore" } 1008 ); 1009 }, 1010 }); 1011 }); 1012 }, 1013 unsubscribe() { 1014 return Promise.resolve(); 1015 }, 1016 getSubscription() { 1017 return Promise.resolve(mockSubscription); 1018 }, 1019 }, 1020 commands: { 1021 async pollDeviceCommands() {}, 1022 }, 1023 async _handleTokenError(e) { 1024 _(`Test failure: ${e} - ${e.stack}`); 1025 throw e; 1026 }, 1027 }; 1028 let device = new FxAccountsDevice(fxai); 1029 device._checkRemoteCommandsUpdateNeeded = async () => false; 1030 device._registerOrUpdateDevice = async () => { 1031 spy._registerOrUpdateDevice.count += 1; 1032 }; 1033 1034 Assert.ok(await device.refreshDeviceList(), "Should refresh list"); 1035 Assert.equal(spy._registerOrUpdateDevice.count, 0, "not expecting a refresh"); 1036 1037 mockDevice.pushEndpointExpired = true; 1038 Assert.ok( 1039 await device.refreshDeviceList({ ignoreCached: true }), 1040 "Should refresh list" 1041 ); 1042 Assert.equal( 1043 spy._registerOrUpdateDevice.count, 1044 1, 1045 "end-point expired means should resubscribe" 1046 ); 1047 1048 mockDevice.pushEndpointExpired = false; 1049 mockSubscription.isExpired = () => true; 1050 Assert.ok( 1051 await device.refreshDeviceList({ ignoreCached: true }), 1052 "Should refresh list" 1053 ); 1054 Assert.equal( 1055 spy._registerOrUpdateDevice.count, 1056 2, 1057 "push service saying expired should resubscribe" 1058 ); 1059 1060 mockSubscription.isExpired = () => false; 1061 mockSubscription.endpoint = "something-else"; 1062 Assert.ok( 1063 await device.refreshDeviceList({ ignoreCached: true }), 1064 "Should refresh list" 1065 ); 1066 Assert.equal( 1067 spy._registerOrUpdateDevice.count, 1068 3, 1069 "push service endpoint diff should resubscribe" 1070 ); 1071 1072 mockSubscription = null; 1073 Assert.ok( 1074 await device.refreshDeviceList({ ignoreCached: true }), 1075 "Should refresh list" 1076 ); 1077 Assert.equal( 1078 spy._registerOrUpdateDevice.count, 1079 4, 1080 "push service saying no sub should resubscribe" 1081 ); 1082 1083 // reset everything to make sure we didn't leave something behind causing the above to 1084 // not check what we thought it was. 1085 mockSubscription = { 1086 isExpired: () => { 1087 return false; 1088 }, 1089 endpoint: "http://mochi.test:8888", 1090 }; 1091 Assert.ok( 1092 await device.refreshDeviceList({ ignoreCached: true }), 1093 "Should refresh list" 1094 ); 1095 Assert.equal( 1096 spy._registerOrUpdateDevice.count, 1097 4, 1098 "resetting to good data should not resubscribe" 1099 ); 1100 }); 1101 1102 add_task(async function test_checking_remote_availableCommands_mismatch() { 1103 const credentials = getTestUser("baz"); 1104 credentials.verified = true; 1105 const fxa = await MockFxAccounts(credentials); 1106 fxa.device._checkRemoteCommandsUpdateNeeded = 1107 FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; 1108 fxa.commands.availableCommands = async () => { 1109 return { 1110 "https://identity.mozilla.com/cmd/open-uri": "local-keys", 1111 }; 1112 }; 1113 1114 const ourDevice = { 1115 isCurrentDevice: true, 1116 availableCommands: { 1117 "https://identity.mozilla.com/cmd/open-uri": "remote-keys", 1118 }, 1119 }; 1120 Assert.ok( 1121 await fxa.device._checkRemoteCommandsUpdateNeeded( 1122 ourDevice.availableCommands 1123 ) 1124 ); 1125 }); 1126 1127 add_task(async function test_checking_remote_availableCommands_match() { 1128 const credentials = getTestUser("baz"); 1129 credentials.verified = true; 1130 const fxa = await MockFxAccounts(credentials); 1131 fxa.device._checkRemoteCommandsUpdateNeeded = 1132 FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; 1133 fxa.commands.availableCommands = async () => { 1134 return { 1135 "https://identity.mozilla.com/cmd/open-uri": "local-keys", 1136 }; 1137 }; 1138 1139 const ourDevice = { 1140 isCurrentDevice: true, 1141 availableCommands: { 1142 "https://identity.mozilla.com/cmd/open-uri": "local-keys", 1143 }, 1144 }; 1145 Assert.ok( 1146 !(await fxa.device._checkRemoteCommandsUpdateNeeded( 1147 ourDevice.availableCommands 1148 )) 1149 ); 1150 }); 1151 1152 function getTestUser(name) { 1153 return { 1154 email: name + "@example.com", 1155 uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", 1156 sessionToken: name + "'s session token", 1157 verified: false, 1158 ...MOCK_ACCOUNT_KEYS, 1159 }; 1160 }