test_sync_auth_manager.js (26320B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { AuthenticationError, SyncAuthManager } = ChromeUtils.importESModule( 5 "resource://services-sync/sync_auth.sys.mjs" 6 ); 7 const { Resource } = ChromeUtils.importESModule( 8 "resource://services-sync/resource.sys.mjs" 9 ); 10 const { initializeIdentityWithTokenServerResponse } = 11 ChromeUtils.importESModule( 12 "resource://testing-common/services/sync/fxa_utils.sys.mjs" 13 ); 14 const { HawkClient } = ChromeUtils.importESModule( 15 "resource://services-common/hawkclient.sys.mjs" 16 ); 17 const { FxAccounts } = ChromeUtils.importESModule( 18 "resource://gre/modules/FxAccounts.sys.mjs" 19 ); 20 const { FxAccountsClient } = ChromeUtils.importESModule( 21 "resource://gre/modules/FxAccountsClient.sys.mjs" 22 ); 23 const { ERRNO_INVALID_AUTH_TOKEN, SCOPE_APP_SYNC } = ChromeUtils.importESModule( 24 "resource://gre/modules/FxAccountsCommon.sys.mjs" 25 ); 26 const { Service } = ChromeUtils.importESModule( 27 "resource://services-sync/service.sys.mjs" 28 ); 29 const { Status } = ChromeUtils.importESModule( 30 "resource://services-sync/status.sys.mjs" 31 ); 32 const { TokenServerClient, TokenServerClientServerError } = 33 ChromeUtils.importESModule( 34 "resource://services-common/tokenserverclient.sys.mjs" 35 ); 36 const { AccountState, ERROR_INVALID_ACCOUNT_STATE } = 37 ChromeUtils.importESModule("resource://gre/modules/FxAccounts.sys.mjs"); 38 39 const SECOND_MS = 1000; 40 const MINUTE_MS = SECOND_MS * 60; 41 const HOUR_MS = MINUTE_MS * 60; 42 43 const MOCK_ACCESS_TOKEN = 44 "e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba"; 45 46 var globalIdentityConfig = makeIdentityConfig(); 47 var globalSyncAuthManager = new SyncAuthManager(); 48 configureFxAccountIdentity(globalSyncAuthManager, globalIdentityConfig); 49 50 /** 51 * Mock client clock and skew vs server in FxAccounts signed-in user module and 52 * API client. sync_auth.js queries these values to construct HAWK 53 * headers. We will use this to test clock skew compensation in these headers 54 * below. 55 */ 56 var MockFxAccountsClient = function () { 57 FxAccountsClient.apply(this); 58 }; 59 MockFxAccountsClient.prototype = { 60 accountStatus() { 61 return Promise.resolve(true); 62 }, 63 getScopedKeyData() { 64 return Promise.resolve({ 65 [SCOPE_APP_SYNC]: { 66 identifier: SCOPE_APP_SYNC, 67 keyRotationSecret: 68 "0000000000000000000000000000000000000000000000000000000000000000", 69 keyRotationTimestamp: 1234567890123, 70 }, 71 }); 72 }, 73 }; 74 Object.setPrototypeOf( 75 MockFxAccountsClient.prototype, 76 FxAccountsClient.prototype 77 ); 78 79 add_test(function test_initial_state() { 80 _("Verify initial state"); 81 Assert.ok(!globalSyncAuthManager._token); 82 Assert.ok(!globalSyncAuthManager._hasValidToken()); 83 run_next_test(); 84 }); 85 86 add_task(async function test_initialialize() { 87 _("Verify start after fetching token"); 88 await globalSyncAuthManager._ensureValidToken(); 89 Assert.ok(!!globalSyncAuthManager._token); 90 Assert.ok(globalSyncAuthManager._hasValidToken()); 91 }); 92 93 add_task(async function test_refreshOAuthTokenOn401() { 94 _("Refreshes the FXA OAuth token after a 401."); 95 let getTokenCount = 0; 96 let syncAuthManager = new SyncAuthManager(); 97 let identityConfig = makeIdentityConfig(); 98 let fxaInternal = makeFxAccountsInternalMock(identityConfig); 99 configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal); 100 syncAuthManager._fxaService._internal.initialize(); 101 syncAuthManager._fxaService.getOAuthToken = () => { 102 ++getTokenCount; 103 return Promise.resolve(MOCK_ACCESS_TOKEN); 104 }; 105 106 let didReturn401 = false; 107 let didReturn200 = false; 108 let mockTSC = mockTokenServer(() => { 109 if (getTokenCount <= 1) { 110 didReturn401 = true; 111 return { 112 status: 401, 113 headers: { "content-type": "application/json" }, 114 body: JSON.stringify({}), 115 }; 116 } 117 didReturn200 = true; 118 return { 119 status: 200, 120 headers: { "content-type": "application/json" }, 121 body: JSON.stringify({ 122 id: "id", 123 key: "key", 124 api_endpoint: "http://example.com/", 125 uid: "uid", 126 duration: 300, 127 }), 128 }; 129 }); 130 131 syncAuthManager._tokenServerClient = mockTSC; 132 133 await syncAuthManager._ensureValidToken(); 134 135 Assert.equal(getTokenCount, 2); 136 Assert.ok(didReturn401); 137 Assert.ok(didReturn200); 138 Assert.ok(syncAuthManager._token); 139 Assert.ok(syncAuthManager._hasValidToken()); 140 }); 141 142 add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() { 143 _("Verify sync state with auth error + account deleted"); 144 145 var identityConfig = makeIdentityConfig(); 146 var syncAuthManager = new SyncAuthManager(); 147 148 // Use the real `getOAuthToken` method that calls 149 // `mockFxAClient.accessTokenWithSessionToken`. 150 let fxaInternal = makeFxAccountsInternalMock(identityConfig); 151 delete fxaInternal.getOAuthToken; 152 153 configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal); 154 syncAuthManager._fxaService._internal.initialize(); 155 156 let accessTokenWithSessionTokenCalled = false; 157 let accountStatusCalled = false; 158 let sessionStatusCalled = false; 159 160 let AuthErrorMockFxAClient = function () { 161 FxAccountsClient.apply(this); 162 }; 163 AuthErrorMockFxAClient.prototype = { 164 accessTokenWithSessionToken() { 165 accessTokenWithSessionTokenCalled = true; 166 return Promise.reject({ 167 code: 401, 168 errno: ERRNO_INVALID_AUTH_TOKEN, 169 }); 170 }, 171 accountStatus() { 172 accountStatusCalled = true; 173 return Promise.resolve(false); 174 }, 175 sessionStatus() { 176 sessionStatusCalled = true; 177 return Promise.resolve(false); 178 }, 179 }; 180 Object.setPrototypeOf( 181 AuthErrorMockFxAClient.prototype, 182 FxAccountsClient.prototype 183 ); 184 185 let mockFxAClient = new AuthErrorMockFxAClient(); 186 syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient; 187 188 await Assert.rejects( 189 syncAuthManager._ensureValidToken(), 190 err => { 191 Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE); 192 return true; // expected error 193 }, 194 "should reject because the account was deleted" 195 ); 196 197 Assert.ok(accessTokenWithSessionTokenCalled); 198 Assert.ok(sessionStatusCalled); 199 Assert.ok(accountStatusCalled); 200 Assert.ok(!syncAuthManager._token); 201 Assert.ok(!syncAuthManager._hasValidToken()); 202 }); 203 204 add_task(async function test_getResourceAuthenticator() { 205 _( 206 "SyncAuthManager supplies a Resource Authenticator callback which returns a Hawk header." 207 ); 208 configureFxAccountIdentity(globalSyncAuthManager); 209 let authenticator = globalSyncAuthManager.getResourceAuthenticator(); 210 Assert.ok(!!authenticator); 211 let req = { 212 uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"), 213 method: "GET", 214 }; 215 let output = await authenticator(req, "GET"); 216 Assert.ok("headers" in output); 217 Assert.ok("authorization" in output.headers); 218 Assert.ok(output.headers.authorization.startsWith("Hawk")); 219 _("Expected internal state after successful call."); 220 Assert.equal( 221 globalSyncAuthManager._token.uid, 222 globalIdentityConfig.fxaccount.token.uid 223 ); 224 }); 225 226 add_task(async function test_resourceAuthenticatorSkew() { 227 _( 228 "SyncAuthManager Resource Authenticator compensates for clock skew in Hawk header." 229 ); 230 231 // Clock is skewed 12 hours into the future 232 // We pick a date in the past so we don't risk concealing bugs in code that 233 // uses new Date() instead of our given date. 234 let now = 235 new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; 236 let syncAuthManager = new SyncAuthManager(); 237 let hawkClient = new HawkClient("https://example.net/v1", "/foo"); 238 239 // mock fxa hawk client skew 240 hawkClient.now = function () { 241 dump("mocked client now: " + now + "\n"); 242 return now; 243 }; 244 // Imagine there's already been one fxa request and the hawk client has 245 // already detected skew vs the fxa auth server. 246 let localtimeOffsetMsec = -1 * 12 * HOUR_MS; 247 hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; 248 249 let fxaClient = new MockFxAccountsClient(); 250 fxaClient.hawk = hawkClient; 251 252 // Sanity check 253 Assert.equal(hawkClient.now(), now); 254 Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); 255 256 // Properly picked up by the client 257 Assert.equal(fxaClient.now(), now); 258 Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); 259 260 let identityConfig = makeIdentityConfig(); 261 let fxaInternal = makeFxAccountsInternalMock(identityConfig); 262 fxaInternal._now_is = now; 263 fxaInternal.fxAccountsClient = fxaClient; 264 265 // Mocks within mocks... 266 configureFxAccountIdentity( 267 syncAuthManager, 268 globalIdentityConfig, 269 fxaInternal 270 ); 271 272 Assert.equal(syncAuthManager._fxaService._internal.now(), now); 273 Assert.equal( 274 syncAuthManager._fxaService._internal.localtimeOffsetMsec, 275 localtimeOffsetMsec 276 ); 277 278 Assert.equal(syncAuthManager._fxaService._internal.now(), now); 279 Assert.equal( 280 syncAuthManager._fxaService._internal.localtimeOffsetMsec, 281 localtimeOffsetMsec 282 ); 283 284 let request = new Resource("https://example.net/i/like/pie/"); 285 let authenticator = syncAuthManager.getResourceAuthenticator(); 286 let output = await authenticator(request, "GET"); 287 dump("output" + JSON.stringify(output)); 288 let authHeader = output.headers.authorization; 289 Assert.ok(authHeader.startsWith("Hawk")); 290 291 // Skew correction is applied in the header and we're within the two-minute 292 // window. 293 Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS); 294 Assert.less(getTimestampDelta(authHeader, now) - 12 * HOUR_MS, 2 * MINUTE_MS); 295 }); 296 297 add_task(async function test_RESTResourceAuthenticatorSkew() { 298 _( 299 "SyncAuthManager REST Resource Authenticator compensates for clock skew in Hawk header." 300 ); 301 302 // Clock is skewed 12 hours into the future from our arbitary date 303 let now = 304 new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; 305 let syncAuthManager = new SyncAuthManager(); 306 let hawkClient = new HawkClient("https://example.net/v1", "/foo"); 307 308 // mock fxa hawk client skew 309 hawkClient.now = function () { 310 return now; 311 }; 312 // Imagine there's already been one fxa request and the hawk client has 313 // already detected skew vs the fxa auth server. 314 hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; 315 316 let fxaClient = new MockFxAccountsClient(); 317 fxaClient.hawk = hawkClient; 318 319 let identityConfig = makeIdentityConfig(); 320 let fxaInternal = makeFxAccountsInternalMock(identityConfig); 321 fxaInternal._now_is = now; 322 fxaInternal.fxAccountsClient = fxaClient; 323 324 configureFxAccountIdentity( 325 syncAuthManager, 326 globalIdentityConfig, 327 fxaInternal 328 ); 329 330 Assert.equal(syncAuthManager._fxaService._internal.now(), now); 331 332 let request = new Resource("https://example.net/i/like/pie/"); 333 let authenticator = syncAuthManager.getResourceAuthenticator(); 334 let output = await authenticator(request, "GET"); 335 dump("output" + JSON.stringify(output)); 336 let authHeader = output.headers.authorization; 337 Assert.ok(authHeader.startsWith("Hawk")); 338 339 // Skew correction is applied in the header and we're within the two-minute 340 // window. 341 Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS); 342 Assert.less(getTimestampDelta(authHeader, now) - 12 * HOUR_MS, 2 * MINUTE_MS); 343 }); 344 345 add_task(async function test_ensureLoggedIn() { 346 configureFxAccountIdentity(globalSyncAuthManager); 347 await globalSyncAuthManager._ensureValidToken(); 348 Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); 349 Assert.ok(globalSyncAuthManager._token); 350 351 // arrange for no logged in user. 352 let fxa = globalSyncAuthManager._fxaService; 353 let signedInUser = 354 fxa._internal.currentAccountState.storageManager.accountData; 355 fxa._internal.currentAccountState.storageManager.accountData = null; 356 await Assert.rejects( 357 globalSyncAuthManager._ensureValidToken(true), 358 /no user is logged in/, 359 "expecting rejection due to no user" 360 ); 361 // Restore the logged in user to what it was. 362 fxa._internal.currentAccountState.storageManager.accountData = signedInUser; 363 Status.login = LOGIN_FAILED_LOGIN_REJECTED; 364 await globalSyncAuthManager._ensureValidToken(true); 365 Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); 366 }); 367 368 add_task(async function test_tokenExpiration() { 369 _("SyncAuthManager notices token expiration:"); 370 let bimExp = new SyncAuthManager(); 371 configureFxAccountIdentity(bimExp, globalIdentityConfig); 372 373 let authenticator = bimExp.getResourceAuthenticator(); 374 Assert.ok(!!authenticator); 375 let req = { 376 uri: CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow"), 377 method: "GET", 378 }; 379 await authenticator(req, "GET"); 380 381 // Mock the clock. 382 _("Forcing the token to expire ..."); 383 Object.defineProperty(bimExp, "_now", { 384 value: function customNow() { 385 return Date.now() + 3000001; 386 }, 387 writable: true, 388 }); 389 Assert.less(bimExp._token.expiration, bimExp._now()); 390 _("... means SyncAuthManager knows to re-fetch it on the next call."); 391 Assert.ok(!bimExp._hasValidToken()); 392 }); 393 394 add_task(async function test_getTokenErrors() { 395 _("SyncAuthManager correctly handles various failures to get a token."); 396 397 _("Arrange for a 401 - Sync should reflect an auth error."); 398 initializeIdentityWithTokenServerResponse({ 399 status: 401, 400 headers: { "content-type": "application/json" }, 401 body: JSON.stringify({}), 402 }); 403 let syncAuthManager = Service.identity; 404 405 await Assert.rejects( 406 syncAuthManager._ensureValidToken(), 407 AuthenticationError, 408 "should reject due to 401" 409 ); 410 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); 411 412 // XXX - other interesting responses to return? 413 414 // And for good measure, some totally "unexpected" errors - we generally 415 // assume these problems are going to magically go away at some point. 416 _( 417 "Arrange for an empty body with a 200 response - should reflect a network error." 418 ); 419 initializeIdentityWithTokenServerResponse({ 420 status: 200, 421 headers: [], 422 body: "", 423 }); 424 syncAuthManager = Service.identity; 425 await Assert.rejects( 426 syncAuthManager._ensureValidToken(), 427 TokenServerClientServerError, 428 "should reject due to non-JSON response" 429 ); 430 Assert.equal( 431 Status.login, 432 LOGIN_FAILED_NETWORK_ERROR, 433 "login state is LOGIN_FAILED_NETWORK_ERROR" 434 ); 435 }); 436 437 add_task(async function test_refreshAccessTokenOn401() { 438 _("SyncAuthManager refreshes the FXA OAuth access token after a 401."); 439 var identityConfig = makeIdentityConfig(); 440 var syncAuthManager = new SyncAuthManager(); 441 // Use the real `getOAuthToken` method that calls 442 // `mockFxAClient.accessTokenWithSessionToken`. 443 let fxaInternal = makeFxAccountsInternalMock(identityConfig); 444 delete fxaInternal.getOAuthToken; 445 configureFxAccountIdentity(syncAuthManager, identityConfig, fxaInternal); 446 syncAuthManager._fxaService._internal.initialize(); 447 448 let getTokenCount = 0; 449 450 let CheckSignMockFxAClient = function () { 451 FxAccountsClient.apply(this); 452 }; 453 CheckSignMockFxAClient.prototype = { 454 accessTokenWithSessionToken() { 455 ++getTokenCount; 456 return Promise.resolve({ access_token: "token" }); 457 }, 458 }; 459 Object.setPrototypeOf( 460 CheckSignMockFxAClient.prototype, 461 FxAccountsClient.prototype 462 ); 463 464 let mockFxAClient = new CheckSignMockFxAClient(); 465 syncAuthManager._fxaService._internal._fxAccountsClient = mockFxAClient; 466 467 let didReturn401 = false; 468 let didReturn200 = false; 469 let mockTSC = mockTokenServer(() => { 470 if (getTokenCount <= 1) { 471 didReturn401 = true; 472 return { 473 status: 401, 474 headers: { "content-type": "application/json" }, 475 body: JSON.stringify({}), 476 }; 477 } 478 didReturn200 = true; 479 return { 480 status: 200, 481 headers: { "content-type": "application/json" }, 482 body: JSON.stringify({ 483 id: "id", 484 key: "key", 485 api_endpoint: "http://example.com/", 486 uid: "uid", 487 duration: 300, 488 }), 489 }; 490 }); 491 492 syncAuthManager._tokenServerClient = mockTSC; 493 494 await syncAuthManager._ensureValidToken(); 495 496 Assert.equal(getTokenCount, 2); 497 Assert.ok(didReturn401); 498 Assert.ok(didReturn200); 499 Assert.ok(syncAuthManager._token); 500 Assert.ok(syncAuthManager._hasValidToken()); 501 }); 502 503 add_task(async function test_getTokenErrorWithRetry() { 504 _("tokenserver sends an observer notification on various backoff headers."); 505 506 // Set Sync's backoffInterval to zero - after we simulated the backoff header 507 // it should reflect the value we sent. 508 Status.backoffInterval = 0; 509 _("Arrange for a 503 with a Retry-After header."); 510 initializeIdentityWithTokenServerResponse({ 511 status: 503, 512 headers: { "content-type": "application/json", "retry-after": "100" }, 513 body: JSON.stringify({}), 514 }); 515 let syncAuthManager = Service.identity; 516 517 await Assert.rejects( 518 syncAuthManager._ensureValidToken(), 519 TokenServerClientServerError, 520 "should reject due to 503" 521 ); 522 523 // The observer should have fired - check it got the value in the response. 524 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 525 // Sync will have the value in ms with some slop - so check it is at least that. 526 Assert.greaterOrEqual(Status.backoffInterval, 100000); 527 528 _("Arrange for a 200 with an X-Backoff header."); 529 Status.backoffInterval = 0; 530 initializeIdentityWithTokenServerResponse({ 531 status: 503, 532 headers: { "content-type": "application/json", "x-backoff": "200" }, 533 body: JSON.stringify({}), 534 }); 535 syncAuthManager = Service.identity; 536 537 await Assert.rejects( 538 syncAuthManager._ensureValidToken(), 539 TokenServerClientServerError, 540 "should reject due to no token in response" 541 ); 542 543 // The observer should have fired - check it got the value in the response. 544 Assert.greaterOrEqual(Status.backoffInterval, 200000); 545 }); 546 547 add_task(async function test_getKeysErrorWithBackoff() { 548 _( 549 "Auth server (via hawk) sends an observer notification on backoff headers." 550 ); 551 552 // Set Sync's backoffInterval to zero - after we simulated the backoff header 553 // it should reflect the value we sent. 554 Status.backoffInterval = 0; 555 _("Arrange for a 503 with a X-Backoff header."); 556 557 let config = makeIdentityConfig(); 558 // We want no scopedKeys so we attempt to fetch them. 559 delete config.fxaccount.user.scopedKeys; 560 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 561 await initializeIdentityWithHAWKResponseFactory( 562 config, 563 function (method, data, uri) { 564 Assert.equal(method, "get"); 565 Assert.equal(uri, "http://mockedserver:9999/account/keys"); 566 return { 567 status: 503, 568 headers: { "content-type": "application/json", "x-backoff": "100" }, 569 body: "{}", 570 }; 571 } 572 ); 573 574 let syncAuthManager = Service.identity; 575 await Assert.rejects( 576 syncAuthManager._ensureValidToken(), 577 TokenServerClientServerError, 578 "should reject due to 503" 579 ); 580 581 // The observer should have fired - check it got the value in the response. 582 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 583 // Sync will have the value in ms with some slop - so check it is at least that. 584 Assert.greaterOrEqual(Status.backoffInterval, 100000); 585 }); 586 587 add_task(async function test_getKeysErrorWithRetry() { 588 _("Auth server (via hawk) sends an observer notification on retry headers."); 589 590 // Set Sync's backoffInterval to zero - after we simulated the backoff header 591 // it should reflect the value we sent. 592 Status.backoffInterval = 0; 593 _("Arrange for a 503 with a Retry-After header."); 594 595 let config = makeIdentityConfig(); 596 // We want no scopedKeys so we attempt to fetch them. 597 delete config.fxaccount.user.scopedKeys; 598 config.fxaccount.user.keyFetchToken = "keyfetchtoken"; 599 await initializeIdentityWithHAWKResponseFactory( 600 config, 601 function (method, data, uri) { 602 Assert.equal(method, "get"); 603 Assert.equal(uri, "http://mockedserver:9999/account/keys"); 604 return { 605 status: 503, 606 headers: { "content-type": "application/json", "retry-after": "100" }, 607 body: "{}", 608 }; 609 } 610 ); 611 612 let syncAuthManager = Service.identity; 613 await Assert.rejects( 614 syncAuthManager._ensureValidToken(), 615 TokenServerClientServerError, 616 "should reject due to 503" 617 ); 618 619 // The observer should have fired - check it got the value in the response. 620 Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); 621 // Sync will have the value in ms with some slop - so check it is at least that. 622 Assert.greaterOrEqual(Status.backoffInterval, 100000); 623 }); 624 625 add_task(async function test_getHAWKErrors() { 626 _("SyncAuthManager correctly handles various HAWK failures."); 627 628 _("Arrange for a 401 - Sync should reflect an auth error."); 629 let config = makeIdentityConfig(); 630 await initializeIdentityWithHAWKResponseFactory( 631 config, 632 function (method, data, uri) { 633 if (uri == "http://mockedserver:9999/oauth/token") { 634 Assert.equal(method, "post"); 635 return { 636 status: 401, 637 headers: { "content-type": "application/json" }, 638 body: JSON.stringify({ 639 code: 401, 640 errno: 110, 641 error: "invalid token", 642 }), 643 }; 644 } 645 // For any follow-up requests that check account status. 646 return { 647 status: 200, 648 headers: { "content-type": "application/json" }, 649 body: JSON.stringify({}), 650 }; 651 } 652 ); 653 Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); 654 655 // XXX - other interesting responses to return? 656 657 // And for good measure, some totally "unexpected" errors - we generally 658 // assume these problems are going to magically go away at some point. 659 _( 660 "Arrange for an empty body with a 200 response - should reflect a network error." 661 ); 662 await initializeIdentityWithHAWKResponseFactory( 663 config, 664 function (method, data, uri) { 665 Assert.equal(method, "post"); 666 Assert.equal(uri, "http://mockedserver:9999/oauth/token"); 667 return { 668 status: 200, 669 headers: [], 670 body: "", 671 }; 672 } 673 ); 674 Assert.equal( 675 Status.login, 676 LOGIN_FAILED_NETWORK_ERROR, 677 "login state is LOGIN_FAILED_NETWORK_ERROR" 678 ); 679 }); 680 681 // End of tests 682 // Utility functions follow 683 684 // Create a new sync_auth object and initialize it with a 685 // hawk mock that simulates HTTP responses. 686 // The callback function will be called each time the mocked hawk server wants 687 // to make a request. The result of the callback should be the mock response 688 // object that will be returned to hawk. 689 // A token server mock will be used that doesn't hit a server, so we move 690 // directly to a hawk request. 691 async function initializeIdentityWithHAWKResponseFactory( 692 config, 693 cbGetResponse 694 ) { 695 // A mock request object. 696 function MockRESTRequest(uri, credentials, extra) { 697 this._uri = uri; 698 this._credentials = credentials; 699 this._extra = extra; 700 } 701 MockRESTRequest.prototype = { 702 setHeader() {}, 703 async post(data) { 704 this.response = cbGetResponse( 705 "post", 706 data, 707 this._uri, 708 this._credentials, 709 this._extra 710 ); 711 return this.response; 712 }, 713 async get() { 714 // Skip /status requests (sync_auth checks if the account still 715 // exists after an auth error) 716 if (this._uri.startsWith("http://mockedserver:9999/account/status")) { 717 this.response = { 718 status: 200, 719 headers: { "content-type": "application/json" }, 720 body: JSON.stringify({ exists: true }), 721 }; 722 } else { 723 this.response = cbGetResponse( 724 "get", 725 null, 726 this._uri, 727 this._credentials, 728 this._extra 729 ); 730 } 731 return this.response; 732 }, 733 }; 734 735 // The hawk client. 736 function MockedHawkClient() {} 737 MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); 738 MockedHawkClient.prototype.constructor = MockedHawkClient; 739 MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function ( 740 uri, 741 credentials, 742 extra 743 ) { 744 return new MockRESTRequest(uri, credentials, extra); 745 }; 746 // Arrange for the same observerPrefix as FxAccountsClient uses 747 MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; 748 749 // tie it all together - configureFxAccountIdentity isn't useful here :( 750 let fxaClient = new MockFxAccountsClient(); 751 fxaClient.hawk = new MockedHawkClient(); 752 let internal = { 753 fxAccountsClient: fxaClient, 754 newAccountState(credentials) { 755 // We only expect this to be called with null indicating the (mock) 756 // storage should be read. 757 if (credentials) { 758 throw new Error("Not expecting to have credentials passed"); 759 } 760 let storageManager = new MockFxaStorageManager(); 761 storageManager.initialize(config.fxaccount.user); 762 return new AccountState(storageManager); 763 }, 764 }; 765 let fxa = new FxAccounts(internal); 766 767 globalSyncAuthManager._fxaService = fxa; 768 await Assert.rejects( 769 globalSyncAuthManager._ensureValidToken(true), 770 // TODO: Ideally this should have a specific check for an error. 771 () => true, 772 "expecting rejection due to hawk error" 773 ); 774 } 775 776 function getTimestamp(hawkAuthHeader) { 777 return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; 778 } 779 780 function getTimestampDelta(hawkAuthHeader, now = Date.now()) { 781 return Math.abs(getTimestamp(hawkAuthHeader) - now); 782 } 783 784 function mockTokenServer(func) { 785 let requestLog = Log.repository.getLogger("testing.mock-rest"); 786 if (!requestLog.appenders.length) { 787 // might as well see what it says :) 788 requestLog.addAppender(new Log.DumpAppender()); 789 requestLog.level = Log.Level.Trace; 790 } 791 function MockRESTRequest() {} 792 MockRESTRequest.prototype = { 793 _log: requestLog, 794 setHeader() {}, 795 async get() { 796 this.response = func(); 797 return this.response; 798 }, 799 }; 800 // The mocked TokenServer client which will get the response. 801 function MockTSC() {} 802 MockTSC.prototype = new TokenServerClient(); 803 MockTSC.prototype.constructor = MockTSC; 804 MockTSC.prototype.newRESTRequest = function (url) { 805 return new MockRESTRequest(url); 806 }; 807 // Arrange for the same observerPrefix as sync_auth uses. 808 MockTSC.prototype.observerPrefix = "weave:service"; 809 return new MockTSC(); 810 }