Bluesky app fork with some witchin' additions 馃挮
at main 1696 lines 51 kB view raw
1import {BskyAgent} from '@atproto/api' 2import {describe, expect, it, jest} from '@jest/globals' 3 4import {agentToSessionAccountOrThrow} from '../agent' 5import {type Action, getInitialState, reducer, type State} from '../reducer' 6 7jest.mock('jwt-decode', () => ({ 8 jwtDecode(_token: string) { 9 return {} 10 }, 11})) 12 13jest.mock('../../birthdate') 14jest.mock('../../../ageAssurance/data') 15jest.mock('#/lib/notifications/notifications', () => ({ 16 unregisterPushToken(_agents: BskyAgent[]) { 17 return Promise.resolve() 18 }, 19})) 20 21describe('session', () => { 22 it('can log in and out', () => { 23 let state = getInitialState([]) 24 expect(printState(state)).toMatchInlineSnapshot(` 25 { 26 "accounts": [], 27 "currentAgentState": { 28 "agent": { 29 "service": "https://public.api.bsky.app/", 30 }, 31 "did": undefined, 32 }, 33 "needsPersist": false, 34 } 35 `) 36 37 const agent = new BskyAgent({service: 'https://alice.com'}) 38 agent.sessionManager.session = { 39 active: true, 40 did: 'alice-did', 41 handle: 'alice.test', 42 accessJwt: 'alice-access-jwt-1', 43 refreshJwt: 'alice-refresh-jwt-1', 44 } 45 state = run(state, [ 46 { 47 type: 'switched-to-account', 48 newAgent: agent, 49 newAccount: agentToSessionAccountOrThrow(agent), 50 }, 51 ]) 52 expect(state.currentAgentState.did).toBe('alice-did') 53 expect(state.accounts.length).toBe(1) 54 expect(state.accounts[0].did).toBe('alice-did') 55 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 56 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') 57 expect(printState(state)).toMatchInlineSnapshot(` 58 { 59 "accounts": [ 60 { 61 "accessJwt": "alice-access-jwt-1", 62 "active": true, 63 "did": "alice-did", 64 "email": undefined, 65 "emailAuthFactor": false, 66 "emailConfirmed": false, 67 "handle": "alice.test", 68 "isSelfHosted": true, 69 "pdsUrl": undefined, 70 "refreshJwt": "alice-refresh-jwt-1", 71 "service": "https://alice.com/", 72 "signupQueued": false, 73 "status": undefined, 74 }, 75 ], 76 "currentAgentState": { 77 "agent": { 78 "service": "https://alice.com/", 79 }, 80 "did": "alice-did", 81 }, 82 "needsPersist": true, 83 } 84 `) 85 86 state = run(state, [ 87 { 88 type: 'logged-out-every-account', 89 }, 90 ]) 91 // Should keep the account but clear out the tokens. 92 expect(state.currentAgentState.did).toBe(undefined) 93 expect(state.accounts.length).toBe(1) 94 expect(state.accounts[0].did).toBe('alice-did') 95 expect(state.accounts[0].accessJwt).toBe(undefined) 96 expect(state.accounts[0].refreshJwt).toBe(undefined) 97 expect(printState(state)).toMatchInlineSnapshot(` 98 { 99 "accounts": [ 100 { 101 "accessJwt": undefined, 102 "active": true, 103 "did": "alice-did", 104 "email": undefined, 105 "emailAuthFactor": false, 106 "emailConfirmed": false, 107 "handle": "alice.test", 108 "isSelfHosted": true, 109 "pdsUrl": undefined, 110 "refreshJwt": undefined, 111 "service": "https://alice.com/", 112 "signupQueued": false, 113 "status": undefined, 114 }, 115 ], 116 "currentAgentState": { 117 "agent": { 118 "service": "https://public.api.bsky.app/", 119 }, 120 "did": undefined, 121 }, 122 "needsPersist": true, 123 } 124 `) 125 }) 126 127 it('switches to the latest account, stores all of them', () => { 128 let state = getInitialState([]) 129 130 const agent1 = new BskyAgent({service: 'https://alice.com'}) 131 agent1.sessionManager.session = { 132 active: true, 133 did: 'alice-did', 134 handle: 'alice.test', 135 accessJwt: 'alice-access-jwt-1', 136 refreshJwt: 'alice-refresh-jwt-1', 137 } 138 state = run(state, [ 139 { 140 // Switch to Alice. 141 type: 'switched-to-account', 142 newAgent: agent1, 143 newAccount: agentToSessionAccountOrThrow(agent1), 144 }, 145 ]) 146 expect(state.accounts.length).toBe(1) 147 expect(state.accounts[0].did).toBe('alice-did') 148 expect(state.currentAgentState.did).toBe('alice-did') 149 expect(state.currentAgentState.agent).toBe(agent1) 150 expect(printState(state)).toMatchInlineSnapshot(` 151 { 152 "accounts": [ 153 { 154 "accessJwt": "alice-access-jwt-1", 155 "active": true, 156 "did": "alice-did", 157 "email": undefined, 158 "emailAuthFactor": false, 159 "emailConfirmed": false, 160 "handle": "alice.test", 161 "isSelfHosted": true, 162 "pdsUrl": undefined, 163 "refreshJwt": "alice-refresh-jwt-1", 164 "service": "https://alice.com/", 165 "signupQueued": false, 166 "status": undefined, 167 }, 168 ], 169 "currentAgentState": { 170 "agent": { 171 "service": "https://alice.com/", 172 }, 173 "did": "alice-did", 174 }, 175 "needsPersist": true, 176 } 177 `) 178 179 const agent2 = new BskyAgent({service: 'https://bob.com'}) 180 agent2.sessionManager.session = { 181 active: true, 182 did: 'bob-did', 183 handle: 'bob.test', 184 accessJwt: 'bob-access-jwt-1', 185 refreshJwt: 'bob-refresh-jwt-1', 186 } 187 state = run(state, [ 188 { 189 // Switch to Bob. 190 type: 'switched-to-account', 191 newAgent: agent2, 192 newAccount: agentToSessionAccountOrThrow(agent2), 193 }, 194 ]) 195 expect(state.accounts.length).toBe(2) 196 // Bob should float upwards. 197 expect(state.accounts[0].did).toBe('bob-did') 198 expect(state.accounts[1].did).toBe('alice-did') 199 expect(state.currentAgentState.did).toBe('bob-did') 200 expect(state.currentAgentState.agent).toBe(agent2) 201 expect(printState(state)).toMatchInlineSnapshot(` 202 { 203 "accounts": [ 204 { 205 "accessJwt": "bob-access-jwt-1", 206 "active": true, 207 "did": "bob-did", 208 "email": undefined, 209 "emailAuthFactor": false, 210 "emailConfirmed": false, 211 "handle": "bob.test", 212 "isSelfHosted": true, 213 "pdsUrl": undefined, 214 "refreshJwt": "bob-refresh-jwt-1", 215 "service": "https://bob.com/", 216 "signupQueued": false, 217 "status": undefined, 218 }, 219 { 220 "accessJwt": "alice-access-jwt-1", 221 "active": true, 222 "did": "alice-did", 223 "email": undefined, 224 "emailAuthFactor": false, 225 "emailConfirmed": false, 226 "handle": "alice.test", 227 "isSelfHosted": true, 228 "pdsUrl": undefined, 229 "refreshJwt": "alice-refresh-jwt-1", 230 "service": "https://alice.com/", 231 "signupQueued": false, 232 "status": undefined, 233 }, 234 ], 235 "currentAgentState": { 236 "agent": { 237 "service": "https://bob.com/", 238 }, 239 "did": "bob-did", 240 }, 241 "needsPersist": true, 242 } 243 `) 244 245 const agent3 = new BskyAgent({service: 'https://alice.com'}) 246 agent3.sessionManager.session = { 247 active: true, 248 did: 'alice-did', 249 handle: 'alice-updated.test', 250 accessJwt: 'alice-access-jwt-2', 251 refreshJwt: 'alice-refresh-jwt-2', 252 } 253 state = run(state, [ 254 { 255 // Switch back to Alice. 256 type: 'switched-to-account', 257 newAgent: agent3, 258 newAccount: agentToSessionAccountOrThrow(agent3), 259 }, 260 ]) 261 expect(state.accounts.length).toBe(2) 262 // Alice should float upwards. 263 expect(state.accounts[0].did).toBe('alice-did') 264 expect(state.accounts[0].handle).toBe('alice-updated.test') 265 expect(state.currentAgentState.did).toBe('alice-did') 266 expect(state.currentAgentState.agent).toBe(agent3) 267 expect(printState(state)).toMatchInlineSnapshot(` 268 { 269 "accounts": [ 270 { 271 "accessJwt": "alice-access-jwt-2", 272 "active": true, 273 "did": "alice-did", 274 "email": undefined, 275 "emailAuthFactor": false, 276 "emailConfirmed": false, 277 "handle": "alice-updated.test", 278 "isSelfHosted": true, 279 "pdsUrl": undefined, 280 "refreshJwt": "alice-refresh-jwt-2", 281 "service": "https://alice.com/", 282 "signupQueued": false, 283 "status": undefined, 284 }, 285 { 286 "accessJwt": "bob-access-jwt-1", 287 "active": true, 288 "did": "bob-did", 289 "email": undefined, 290 "emailAuthFactor": false, 291 "emailConfirmed": false, 292 "handle": "bob.test", 293 "isSelfHosted": true, 294 "pdsUrl": undefined, 295 "refreshJwt": "bob-refresh-jwt-1", 296 "service": "https://bob.com/", 297 "signupQueued": false, 298 "status": undefined, 299 }, 300 ], 301 "currentAgentState": { 302 "agent": { 303 "service": "https://alice.com/", 304 }, 305 "did": "alice-did", 306 }, 307 "needsPersist": true, 308 } 309 `) 310 311 const agent4 = new BskyAgent({service: 'https://jay.com'}) 312 agent4.sessionManager.session = { 313 active: true, 314 did: 'jay-did', 315 handle: 'jay.test', 316 accessJwt: 'jay-access-jwt-1', 317 refreshJwt: 'jay-refresh-jwt-1', 318 } 319 state = run(state, [ 320 { 321 // Switch to Jay. 322 type: 'switched-to-account', 323 newAgent: agent4, 324 newAccount: agentToSessionAccountOrThrow(agent4), 325 }, 326 ]) 327 expect(state.accounts.length).toBe(3) 328 expect(state.accounts[0].did).toBe('jay-did') 329 expect(state.currentAgentState.did).toBe('jay-did') 330 expect(state.currentAgentState.agent).toBe(agent4) 331 expect(printState(state)).toMatchInlineSnapshot(` 332 { 333 "accounts": [ 334 { 335 "accessJwt": "jay-access-jwt-1", 336 "active": true, 337 "did": "jay-did", 338 "email": undefined, 339 "emailAuthFactor": false, 340 "emailConfirmed": false, 341 "handle": "jay.test", 342 "isSelfHosted": true, 343 "pdsUrl": undefined, 344 "refreshJwt": "jay-refresh-jwt-1", 345 "service": "https://jay.com/", 346 "signupQueued": false, 347 "status": undefined, 348 }, 349 { 350 "accessJwt": "alice-access-jwt-2", 351 "active": true, 352 "did": "alice-did", 353 "email": undefined, 354 "emailAuthFactor": false, 355 "emailConfirmed": false, 356 "handle": "alice-updated.test", 357 "isSelfHosted": true, 358 "pdsUrl": undefined, 359 "refreshJwt": "alice-refresh-jwt-2", 360 "service": "https://alice.com/", 361 "signupQueued": false, 362 "status": undefined, 363 }, 364 { 365 "accessJwt": "bob-access-jwt-1", 366 "active": true, 367 "did": "bob-did", 368 "email": undefined, 369 "emailAuthFactor": false, 370 "emailConfirmed": false, 371 "handle": "bob.test", 372 "isSelfHosted": true, 373 "pdsUrl": undefined, 374 "refreshJwt": "bob-refresh-jwt-1", 375 "service": "https://bob.com/", 376 "signupQueued": false, 377 "status": undefined, 378 }, 379 ], 380 "currentAgentState": { 381 "agent": { 382 "service": "https://jay.com/", 383 }, 384 "did": "jay-did", 385 }, 386 "needsPersist": true, 387 } 388 `) 389 390 state = run(state, [ 391 { 392 // Log everyone out. 393 type: 'logged-out-every-account', 394 }, 395 ]) 396 expect(state.accounts.length).toBe(3) 397 expect(state.currentAgentState.did).toBe(undefined) 398 // All tokens should be gone. 399 expect(state.accounts[0].accessJwt).toBe(undefined) 400 expect(state.accounts[0].refreshJwt).toBe(undefined) 401 expect(state.accounts[1].accessJwt).toBe(undefined) 402 expect(state.accounts[1].refreshJwt).toBe(undefined) 403 expect(state.accounts[2].accessJwt).toBe(undefined) 404 expect(state.accounts[2].refreshJwt).toBe(undefined) 405 expect(printState(state)).toMatchInlineSnapshot(` 406 { 407 "accounts": [ 408 { 409 "accessJwt": undefined, 410 "active": true, 411 "did": "jay-did", 412 "email": undefined, 413 "emailAuthFactor": false, 414 "emailConfirmed": false, 415 "handle": "jay.test", 416 "isSelfHosted": true, 417 "pdsUrl": undefined, 418 "refreshJwt": undefined, 419 "service": "https://jay.com/", 420 "signupQueued": false, 421 "status": undefined, 422 }, 423 { 424 "accessJwt": undefined, 425 "active": true, 426 "did": "alice-did", 427 "email": undefined, 428 "emailAuthFactor": false, 429 "emailConfirmed": false, 430 "handle": "alice-updated.test", 431 "isSelfHosted": true, 432 "pdsUrl": undefined, 433 "refreshJwt": undefined, 434 "service": "https://alice.com/", 435 "signupQueued": false, 436 "status": undefined, 437 }, 438 { 439 "accessJwt": undefined, 440 "active": true, 441 "did": "bob-did", 442 "email": undefined, 443 "emailAuthFactor": false, 444 "emailConfirmed": false, 445 "handle": "bob.test", 446 "isSelfHosted": true, 447 "pdsUrl": undefined, 448 "refreshJwt": undefined, 449 "service": "https://bob.com/", 450 "signupQueued": false, 451 "status": undefined, 452 }, 453 ], 454 "currentAgentState": { 455 "agent": { 456 "service": "https://public.api.bsky.app/", 457 }, 458 "did": undefined, 459 }, 460 "needsPersist": true, 461 } 462 `) 463 }) 464 465 it('can log back in after logging out', () => { 466 let state = getInitialState([]) 467 468 const agent1 = new BskyAgent({service: 'https://alice.com'}) 469 agent1.sessionManager.session = { 470 active: true, 471 did: 'alice-did', 472 handle: 'alice.test', 473 accessJwt: 'alice-access-jwt-1', 474 refreshJwt: 'alice-refresh-jwt-1', 475 } 476 state = run(state, [ 477 { 478 type: 'switched-to-account', 479 newAgent: agent1, 480 newAccount: agentToSessionAccountOrThrow(agent1), 481 }, 482 ]) 483 expect(state.accounts.length).toBe(1) 484 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 485 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') 486 expect(state.currentAgentState.did).toBe('alice-did') 487 488 state = run(state, [ 489 { 490 type: 'logged-out-every-account', 491 }, 492 ]) 493 expect(state.accounts.length).toBe(1) 494 expect(state.accounts[0].accessJwt).toBe(undefined) 495 expect(state.accounts[0].refreshJwt).toBe(undefined) 496 expect(state.currentAgentState.did).toBe(undefined) 497 expect(printState(state)).toMatchInlineSnapshot(` 498 { 499 "accounts": [ 500 { 501 "accessJwt": undefined, 502 "active": true, 503 "did": "alice-did", 504 "email": undefined, 505 "emailAuthFactor": false, 506 "emailConfirmed": false, 507 "handle": "alice.test", 508 "isSelfHosted": true, 509 "pdsUrl": undefined, 510 "refreshJwt": undefined, 511 "service": "https://alice.com/", 512 "signupQueued": false, 513 "status": undefined, 514 }, 515 ], 516 "currentAgentState": { 517 "agent": { 518 "service": "https://public.api.bsky.app/", 519 }, 520 "did": undefined, 521 }, 522 "needsPersist": true, 523 } 524 `) 525 526 const agent2 = new BskyAgent({service: 'https://alice.com'}) 527 agent2.sessionManager.session = { 528 active: true, 529 did: 'alice-did', 530 handle: 'alice.test', 531 accessJwt: 'alice-access-jwt-2', 532 refreshJwt: 'alice-refresh-jwt-2', 533 } 534 state = run(state, [ 535 { 536 type: 'switched-to-account', 537 newAgent: agent2, 538 newAccount: agentToSessionAccountOrThrow(agent2), 539 }, 540 ]) 541 expect(state.accounts.length).toBe(1) 542 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') 543 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') 544 expect(state.currentAgentState.did).toBe('alice-did') 545 expect(printState(state)).toMatchInlineSnapshot(` 546 { 547 "accounts": [ 548 { 549 "accessJwt": "alice-access-jwt-2", 550 "active": true, 551 "did": "alice-did", 552 "email": undefined, 553 "emailAuthFactor": false, 554 "emailConfirmed": false, 555 "handle": "alice.test", 556 "isSelfHosted": true, 557 "pdsUrl": undefined, 558 "refreshJwt": "alice-refresh-jwt-2", 559 "service": "https://alice.com/", 560 "signupQueued": false, 561 "status": undefined, 562 }, 563 ], 564 "currentAgentState": { 565 "agent": { 566 "service": "https://alice.com/", 567 }, 568 "did": "alice-did", 569 }, 570 "needsPersist": true, 571 } 572 `) 573 }) 574 575 it('can remove active account', () => { 576 let state = getInitialState([]) 577 578 const agent1 = new BskyAgent({service: 'https://alice.com'}) 579 agent1.sessionManager.session = { 580 active: true, 581 did: 'alice-did', 582 handle: 'alice.test', 583 accessJwt: 'alice-access-jwt-1', 584 refreshJwt: 'alice-refresh-jwt-1', 585 } 586 state = run(state, [ 587 { 588 type: 'switched-to-account', 589 newAgent: agent1, 590 newAccount: agentToSessionAccountOrThrow(agent1), 591 }, 592 ]) 593 expect(state.accounts.length).toBe(1) 594 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 595 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') 596 expect(state.currentAgentState.did).toBe('alice-did') 597 598 state = run(state, [ 599 { 600 type: 'removed-account', 601 accountDid: 'alice-did', 602 }, 603 ]) 604 expect(state.accounts.length).toBe(0) 605 expect(state.currentAgentState.did).toBe(undefined) 606 expect(printState(state)).toMatchInlineSnapshot(` 607 { 608 "accounts": [], 609 "currentAgentState": { 610 "agent": { 611 "service": "https://public.api.bsky.app/", 612 }, 613 "did": undefined, 614 }, 615 "needsPersist": true, 616 } 617 `) 618 }) 619 620 it('can remove inactive account', () => { 621 let state = getInitialState([]) 622 623 const agent1 = new BskyAgent({service: 'https://alice.com'}) 624 agent1.sessionManager.session = { 625 active: true, 626 did: 'alice-did', 627 handle: 'alice.test', 628 accessJwt: 'alice-access-jwt-1', 629 refreshJwt: 'alice-refresh-jwt-1', 630 } 631 const agent2 = new BskyAgent({service: 'https://bob.com'}) 632 agent2.sessionManager.session = { 633 active: true, 634 did: 'bob-did', 635 handle: 'bob.test', 636 accessJwt: 'bob-access-jwt-1', 637 refreshJwt: 'bob-refresh-jwt-1', 638 } 639 state = run(state, [ 640 { 641 type: 'switched-to-account', 642 newAgent: agent1, 643 newAccount: agentToSessionAccountOrThrow(agent1), 644 }, 645 { 646 type: 'switched-to-account', 647 newAgent: agent2, 648 newAccount: agentToSessionAccountOrThrow(agent2), 649 }, 650 ]) 651 expect(state.accounts.length).toBe(2) 652 expect(state.currentAgentState.did).toBe('bob-did') 653 654 state = run(state, [ 655 { 656 type: 'removed-account', 657 accountDid: 'alice-did', 658 }, 659 ]) 660 expect(state.accounts.length).toBe(1) 661 expect(state.currentAgentState.did).toBe('bob-did') 662 expect(printState(state)).toMatchInlineSnapshot(` 663 { 664 "accounts": [ 665 { 666 "accessJwt": "bob-access-jwt-1", 667 "active": true, 668 "did": "bob-did", 669 "email": undefined, 670 "emailAuthFactor": false, 671 "emailConfirmed": false, 672 "handle": "bob.test", 673 "isSelfHosted": true, 674 "pdsUrl": undefined, 675 "refreshJwt": "bob-refresh-jwt-1", 676 "service": "https://bob.com/", 677 "signupQueued": false, 678 "status": undefined, 679 }, 680 ], 681 "currentAgentState": { 682 "agent": { 683 "service": "https://bob.com/", 684 }, 685 "did": "bob-did", 686 }, 687 "needsPersist": true, 688 } 689 `) 690 691 state = run(state, [ 692 { 693 type: 'removed-account', 694 accountDid: 'bob-did', 695 }, 696 ]) 697 expect(state.accounts.length).toBe(0) 698 expect(state.currentAgentState.did).toBe(undefined) 699 }) 700 701 it('can log out of the current account', () => { 702 let state = getInitialState([]) 703 704 const agent1 = new BskyAgent({service: 'https://alice.com'}) 705 agent1.sessionManager.session = { 706 active: true, 707 did: 'alice-did', 708 handle: 'alice.test', 709 accessJwt: 'alice-access-jwt-1', 710 refreshJwt: 'alice-refresh-jwt-1', 711 } 712 state = run(state, [ 713 { 714 type: 'switched-to-account', 715 newAgent: agent1, 716 newAccount: agentToSessionAccountOrThrow(agent1), 717 }, 718 ]) 719 expect(state.accounts.length).toBe(1) 720 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 721 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') 722 expect(state.currentAgentState.did).toBe('alice-did') 723 724 const agent2 = new BskyAgent({service: 'https://bob.com'}) 725 agent2.sessionManager.session = { 726 active: true, 727 did: 'bob-did', 728 handle: 'bob.test', 729 accessJwt: 'bob-access-jwt-1', 730 refreshJwt: 'bob-refresh-jwt-1', 731 } 732 state = run(state, [ 733 { 734 type: 'switched-to-account', 735 newAgent: agent2, 736 newAccount: agentToSessionAccountOrThrow(agent2), 737 }, 738 ]) 739 expect(state.accounts.length).toBe(2) 740 expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1') 741 expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-1') 742 expect(state.currentAgentState.did).toBe('bob-did') 743 744 state = run(state, [ 745 { 746 type: 'logged-out-current-account', 747 }, 748 ]) 749 expect(state.accounts.length).toBe(2) 750 expect(state.accounts[0].accessJwt).toBe(undefined) 751 expect(state.accounts[0].refreshJwt).toBe(undefined) 752 expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-1') 753 expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-1') 754 expect(state.currentAgentState.did).toBe(undefined) 755 expect(printState(state)).toMatchInlineSnapshot(` 756 { 757 "accounts": [ 758 { 759 "accessJwt": undefined, 760 "active": true, 761 "did": "bob-did", 762 "email": undefined, 763 "emailAuthFactor": false, 764 "emailConfirmed": false, 765 "handle": "bob.test", 766 "isSelfHosted": true, 767 "pdsUrl": undefined, 768 "refreshJwt": undefined, 769 "service": "https://bob.com/", 770 "signupQueued": false, 771 "status": undefined, 772 }, 773 { 774 "accessJwt": "alice-access-jwt-1", 775 "active": true, 776 "did": "alice-did", 777 "email": undefined, 778 "emailAuthFactor": false, 779 "emailConfirmed": false, 780 "handle": "alice.test", 781 "isSelfHosted": true, 782 "pdsUrl": undefined, 783 "refreshJwt": "alice-refresh-jwt-1", 784 "service": "https://alice.com/", 785 "signupQueued": false, 786 "status": undefined, 787 }, 788 ], 789 "currentAgentState": { 790 "agent": { 791 "service": "https://public.api.bsky.app/", 792 }, 793 "did": undefined, 794 }, 795 "needsPersist": true, 796 } 797 `) 798 }) 799 800 it('updates stored account with refreshed tokens', () => { 801 let state = getInitialState([]) 802 803 const agent1 = new BskyAgent({service: 'https://alice.com'}) 804 agent1.sessionManager.session = { 805 active: true, 806 did: 'alice-did', 807 handle: 'alice.test', 808 accessJwt: 'alice-access-jwt-1', 809 refreshJwt: 'alice-refresh-jwt-1', 810 } 811 state = run(state, [ 812 { 813 type: 'switched-to-account', 814 newAgent: agent1, 815 newAccount: agentToSessionAccountOrThrow(agent1), 816 }, 817 ]) 818 expect(state.accounts.length).toBe(1) 819 expect(state.currentAgentState.did).toBe('alice-did') 820 821 agent1.sessionManager.session = { 822 active: true, 823 did: 'alice-did', 824 handle: 'alice-updated.test', 825 accessJwt: 'alice-access-jwt-2', 826 refreshJwt: 'alice-refresh-jwt-2', 827 email: 'alice@foo.bar', 828 emailAuthFactor: false, 829 emailConfirmed: false, 830 } 831 state = run(state, [ 832 { 833 type: 'received-agent-event', 834 accountDid: 'alice-did', 835 agent: agent1, 836 refreshedAccount: agentToSessionAccountOrThrow(agent1), 837 sessionEvent: 'update', 838 }, 839 ]) 840 expect(state.accounts.length).toBe(1) 841 expect(state.accounts[0].email).toBe('alice@foo.bar') 842 expect(state.accounts[0].handle).toBe('alice-updated.test') 843 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') 844 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') 845 expect(state.currentAgentState.did).toBe('alice-did') 846 expect(printState(state)).toMatchInlineSnapshot(` 847 { 848 "accounts": [ 849 { 850 "accessJwt": "alice-access-jwt-2", 851 "active": true, 852 "did": "alice-did", 853 "email": "alice@foo.bar", 854 "emailAuthFactor": false, 855 "emailConfirmed": false, 856 "handle": "alice-updated.test", 857 "isSelfHosted": true, 858 "pdsUrl": undefined, 859 "refreshJwt": "alice-refresh-jwt-2", 860 "service": "https://alice.com/", 861 "signupQueued": false, 862 "status": undefined, 863 }, 864 ], 865 "currentAgentState": { 866 "agent": { 867 "service": "https://alice.com/", 868 }, 869 "did": "alice-did", 870 }, 871 "needsPersist": true, 872 } 873 `) 874 875 agent1.sessionManager.session = { 876 active: true, 877 did: 'alice-did', 878 handle: 'alice-updated.test', 879 accessJwt: 'alice-access-jwt-3', 880 refreshJwt: 'alice-refresh-jwt-3', 881 email: 'alice@foo.baz', 882 emailAuthFactor: true, 883 emailConfirmed: true, 884 } 885 state = run(state, [ 886 { 887 type: 'received-agent-event', 888 accountDid: 'alice-did', 889 agent: agent1, 890 refreshedAccount: agentToSessionAccountOrThrow(agent1), 891 sessionEvent: 'update', 892 }, 893 ]) 894 expect(state.accounts.length).toBe(1) 895 expect(state.accounts[0].email).toBe('alice@foo.baz') 896 expect(state.accounts[0].handle).toBe('alice-updated.test') 897 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3') 898 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-3') 899 expect(state.currentAgentState.did).toBe('alice-did') 900 expect(printState(state)).toMatchInlineSnapshot(` 901 { 902 "accounts": [ 903 { 904 "accessJwt": "alice-access-jwt-3", 905 "active": true, 906 "did": "alice-did", 907 "email": "alice@foo.baz", 908 "emailAuthFactor": true, 909 "emailConfirmed": true, 910 "handle": "alice-updated.test", 911 "isSelfHosted": true, 912 "pdsUrl": undefined, 913 "refreshJwt": "alice-refresh-jwt-3", 914 "service": "https://alice.com/", 915 "signupQueued": false, 916 "status": undefined, 917 }, 918 ], 919 "currentAgentState": { 920 "agent": { 921 "service": "https://alice.com/", 922 }, 923 "did": "alice-did", 924 }, 925 "needsPersist": true, 926 } 927 `) 928 929 agent1.sessionManager.session = { 930 active: true, 931 did: 'alice-did', 932 handle: 'alice-updated.test', 933 accessJwt: 'alice-access-jwt-4', 934 refreshJwt: 'alice-refresh-jwt-4', 935 email: 'alice@foo.baz', 936 emailAuthFactor: false, 937 emailConfirmed: false, 938 } 939 state = run(state, [ 940 { 941 type: 'received-agent-event', 942 accountDid: 'alice-did', 943 agent: agent1, 944 refreshedAccount: agentToSessionAccountOrThrow(agent1), 945 sessionEvent: 'update', 946 }, 947 ]) 948 expect(state.accounts.length).toBe(1) 949 expect(state.accounts[0].email).toBe('alice@foo.baz') 950 expect(state.accounts[0].handle).toBe('alice-updated.test') 951 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-4') 952 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-4') 953 expect(state.currentAgentState.did).toBe('alice-did') 954 expect(printState(state)).toMatchInlineSnapshot(` 955 { 956 "accounts": [ 957 { 958 "accessJwt": "alice-access-jwt-4", 959 "active": true, 960 "did": "alice-did", 961 "email": "alice@foo.baz", 962 "emailAuthFactor": false, 963 "emailConfirmed": false, 964 "handle": "alice-updated.test", 965 "isSelfHosted": true, 966 "pdsUrl": undefined, 967 "refreshJwt": "alice-refresh-jwt-4", 968 "service": "https://alice.com/", 969 "signupQueued": false, 970 "status": undefined, 971 }, 972 ], 973 "currentAgentState": { 974 "agent": { 975 "service": "https://alice.com/", 976 }, 977 "did": "alice-did", 978 }, 979 "needsPersist": true, 980 } 981 `) 982 }) 983 984 it('bails out of update on identical objects', () => { 985 let state = getInitialState([]) 986 987 const agent1 = new BskyAgent({service: 'https://alice.com'}) 988 agent1.sessionManager.session = { 989 active: true, 990 did: 'alice-did', 991 handle: 'alice.test', 992 accessJwt: 'alice-access-jwt-1', 993 refreshJwt: 'alice-refresh-jwt-1', 994 } 995 state = run(state, [ 996 { 997 type: 'switched-to-account', 998 newAgent: agent1, 999 newAccount: agentToSessionAccountOrThrow(agent1), 1000 }, 1001 ]) 1002 expect(state.accounts.length).toBe(1) 1003 expect(state.currentAgentState.did).toBe('alice-did') 1004 1005 agent1.sessionManager.session = { 1006 active: true, 1007 did: 'alice-did', 1008 handle: 'alice-updated.test', 1009 accessJwt: 'alice-access-jwt-2', 1010 refreshJwt: 'alice-refresh-jwt-2', 1011 } 1012 state = run(state, [ 1013 { 1014 type: 'received-agent-event', 1015 accountDid: 'alice-did', 1016 agent: agent1, 1017 refreshedAccount: agentToSessionAccountOrThrow(agent1), 1018 sessionEvent: 'update', 1019 }, 1020 ]) 1021 expect(state.accounts.length).toBe(1) 1022 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') 1023 1024 const lastState = state 1025 state = run(state, [ 1026 { 1027 type: 'received-agent-event', 1028 accountDid: 'alice-did', 1029 agent: agent1, 1030 refreshedAccount: agentToSessionAccountOrThrow(agent1), 1031 sessionEvent: 'update', 1032 }, 1033 ]) 1034 expect(lastState === state).toBe(true) 1035 1036 agent1.sessionManager.session = { 1037 active: true, 1038 did: 'alice-did', 1039 handle: 'alice-updated.test', 1040 accessJwt: 'alice-access-jwt-3', 1041 refreshJwt: 'alice-refresh-jwt-3', 1042 } 1043 state = run(state, [ 1044 { 1045 type: 'received-agent-event', 1046 accountDid: 'alice-did', 1047 agent: agent1, 1048 refreshedAccount: agentToSessionAccountOrThrow(agent1), 1049 sessionEvent: 'update', 1050 }, 1051 ]) 1052 expect(state.accounts.length).toBe(1) 1053 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3') 1054 }) 1055 1056 it('accepts updates from a stale agent', () => { 1057 let state = getInitialState([]) 1058 1059 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1060 agent1.sessionManager.session = { 1061 active: true, 1062 did: 'alice-did', 1063 handle: 'alice.test', 1064 accessJwt: 'alice-access-jwt-1', 1065 refreshJwt: 'alice-refresh-jwt-1', 1066 } 1067 1068 const agent2 = new BskyAgent({service: 'https://bob.com'}) 1069 agent2.sessionManager.session = { 1070 active: true, 1071 did: 'bob-did', 1072 handle: 'bob.test', 1073 accessJwt: 'bob-access-jwt-1', 1074 refreshJwt: 'bob-refresh-jwt-1', 1075 } 1076 1077 state = run(state, [ 1078 { 1079 // Switch to Alice. 1080 type: 'switched-to-account', 1081 newAgent: agent1, 1082 newAccount: agentToSessionAccountOrThrow(agent1), 1083 }, 1084 { 1085 // Switch to Bob. 1086 type: 'switched-to-account', 1087 newAgent: agent2, 1088 newAccount: agentToSessionAccountOrThrow(agent2), 1089 }, 1090 ]) 1091 expect(state.accounts.length).toBe(2) 1092 expect(state.currentAgentState.did).toBe('bob-did') 1093 1094 agent1.sessionManager.session = { 1095 active: true, 1096 did: 'alice-did', 1097 handle: 'alice-updated.test', 1098 accessJwt: 'alice-access-jwt-2', 1099 refreshJwt: 'alice-refresh-jwt-2', 1100 email: 'alice@foo.bar', 1101 emailAuthFactor: false, 1102 emailConfirmed: false, 1103 } 1104 state = run(state, [ 1105 { 1106 type: 'received-agent-event', 1107 accountDid: 'alice-did', 1108 agent: agent1, 1109 refreshedAccount: agentToSessionAccountOrThrow(agent1), 1110 sessionEvent: 'update', 1111 }, 1112 ]) 1113 expect(state.accounts.length).toBe(2) 1114 expect(state.accounts[1].did).toBe('alice-did') 1115 // Should update Alice's tokens because otherwise they'll be stale. 1116 expect(state.accounts[1].handle).toBe('alice-updated.test') 1117 expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-2') 1118 expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-2') 1119 expect(printState(state)).toMatchInlineSnapshot(` 1120 { 1121 "accounts": [ 1122 { 1123 "accessJwt": "bob-access-jwt-1", 1124 "active": true, 1125 "did": "bob-did", 1126 "email": undefined, 1127 "emailAuthFactor": false, 1128 "emailConfirmed": false, 1129 "handle": "bob.test", 1130 "isSelfHosted": true, 1131 "pdsUrl": undefined, 1132 "refreshJwt": "bob-refresh-jwt-1", 1133 "service": "https://bob.com/", 1134 "signupQueued": false, 1135 "status": undefined, 1136 }, 1137 { 1138 "accessJwt": "alice-access-jwt-2", 1139 "active": true, 1140 "did": "alice-did", 1141 "email": "alice@foo.bar", 1142 "emailAuthFactor": false, 1143 "emailConfirmed": false, 1144 "handle": "alice-updated.test", 1145 "isSelfHosted": true, 1146 "pdsUrl": undefined, 1147 "refreshJwt": "alice-refresh-jwt-2", 1148 "service": "https://alice.com/", 1149 "signupQueued": false, 1150 "status": undefined, 1151 }, 1152 ], 1153 "currentAgentState": { 1154 "agent": { 1155 "service": "https://bob.com/", 1156 }, 1157 "did": "bob-did", 1158 }, 1159 "needsPersist": true, 1160 } 1161 `) 1162 1163 agent2.sessionManager.session = { 1164 active: true, 1165 did: 'bob-did', 1166 handle: 'bob-updated.test', 1167 accessJwt: 'bob-access-jwt-2', 1168 refreshJwt: 'bob-refresh-jwt-2', 1169 } 1170 state = run(state, [ 1171 { 1172 // Update Bob. 1173 type: 'received-agent-event', 1174 accountDid: 'bob-did', 1175 agent: agent2, 1176 refreshedAccount: agentToSessionAccountOrThrow(agent2), 1177 sessionEvent: 'update', 1178 }, 1179 ]) 1180 expect(state.accounts.length).toBe(2) 1181 expect(state.accounts[0].did).toBe('bob-did') 1182 // Should update Bob's tokens because otherwise they'll be stale. 1183 expect(state.accounts[0].handle).toBe('bob-updated.test') 1184 expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-2') 1185 expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-2') 1186 expect(printState(state)).toMatchInlineSnapshot(` 1187 { 1188 "accounts": [ 1189 { 1190 "accessJwt": "bob-access-jwt-2", 1191 "active": true, 1192 "did": "bob-did", 1193 "email": undefined, 1194 "emailAuthFactor": false, 1195 "emailConfirmed": false, 1196 "handle": "bob-updated.test", 1197 "isSelfHosted": true, 1198 "pdsUrl": undefined, 1199 "refreshJwt": "bob-refresh-jwt-2", 1200 "service": "https://bob.com/", 1201 "signupQueued": false, 1202 "status": undefined, 1203 }, 1204 { 1205 "accessJwt": "alice-access-jwt-2", 1206 "active": true, 1207 "did": "alice-did", 1208 "email": "alice@foo.bar", 1209 "emailAuthFactor": false, 1210 "emailConfirmed": false, 1211 "handle": "alice-updated.test", 1212 "isSelfHosted": true, 1213 "pdsUrl": undefined, 1214 "refreshJwt": "alice-refresh-jwt-2", 1215 "service": "https://alice.com/", 1216 "signupQueued": false, 1217 "status": undefined, 1218 }, 1219 ], 1220 "currentAgentState": { 1221 "agent": { 1222 "service": "https://bob.com/", 1223 }, 1224 "did": "bob-did", 1225 }, 1226 "needsPersist": true, 1227 } 1228 `) 1229 1230 // Ignore other events for inactive agent. 1231 const lastState = state 1232 agent1.sessionManager.session = undefined 1233 state = run(state, [ 1234 { 1235 type: 'received-agent-event', 1236 accountDid: 'alice-did', 1237 agent: agent1, 1238 refreshedAccount: undefined, 1239 sessionEvent: 'network-error', 1240 }, 1241 ]) 1242 expect(lastState === state).toBe(true) 1243 state = run(state, [ 1244 { 1245 type: 'received-agent-event', 1246 accountDid: 'alice-did', 1247 agent: agent1, 1248 refreshedAccount: undefined, 1249 sessionEvent: 'expired', 1250 }, 1251 ]) 1252 expect(lastState === state).toBe(true) 1253 }) 1254 1255 it('ignores updates from a removed agent', () => { 1256 let state = getInitialState([]) 1257 1258 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1259 agent1.sessionManager.session = { 1260 active: true, 1261 did: 'alice-did', 1262 handle: 'alice.test', 1263 accessJwt: 'alice-access-jwt-1', 1264 refreshJwt: 'alice-refresh-jwt-1', 1265 } 1266 1267 const agent2 = new BskyAgent({service: 'https://bob.com'}) 1268 agent2.sessionManager.session = { 1269 active: true, 1270 did: 'bob-did', 1271 handle: 'bob.test', 1272 accessJwt: 'bob-access-jwt-1', 1273 refreshJwt: 'bob-refresh-jwt-1', 1274 } 1275 1276 state = run(state, [ 1277 { 1278 type: 'switched-to-account', 1279 newAgent: agent1, 1280 newAccount: agentToSessionAccountOrThrow(agent1), 1281 }, 1282 { 1283 type: 'switched-to-account', 1284 newAgent: agent2, 1285 newAccount: agentToSessionAccountOrThrow(agent2), 1286 }, 1287 { 1288 type: 'removed-account', 1289 accountDid: 'alice-did', 1290 }, 1291 ]) 1292 expect(state.accounts.length).toBe(1) 1293 expect(state.currentAgentState.did).toBe('bob-did') 1294 1295 agent1.sessionManager.session = { 1296 active: true, 1297 did: 'alice-did', 1298 handle: 'alice.test', 1299 accessJwt: 'alice-access-jwt-2', 1300 refreshJwt: 'alice-refresh-jwt-2', 1301 } 1302 state = run(state, [ 1303 { 1304 type: 'received-agent-event', 1305 accountDid: 'alice-did', 1306 agent: agent1, 1307 refreshedAccount: agentToSessionAccountOrThrow(agent1), 1308 sessionEvent: 'update', 1309 }, 1310 ]) 1311 expect(state.accounts.length).toBe(1) 1312 expect(state.accounts[0].did).toBe('bob-did') 1313 expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1') 1314 expect(state.currentAgentState.did).toBe('bob-did') 1315 }) 1316 1317 it('ignores network errors', () => { 1318 let state = getInitialState([]) 1319 1320 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1321 agent1.sessionManager.session = { 1322 active: true, 1323 did: 'alice-did', 1324 handle: 'alice.test', 1325 accessJwt: 'alice-access-jwt-1', 1326 refreshJwt: 'alice-refresh-jwt-1', 1327 } 1328 state = run(state, [ 1329 { 1330 // Switch to Alice. 1331 type: 'switched-to-account', 1332 newAgent: agent1, 1333 newAccount: agentToSessionAccountOrThrow(agent1), 1334 }, 1335 ]) 1336 expect(state.accounts.length).toBe(1) 1337 expect(state.currentAgentState.did).toBe('alice-did') 1338 1339 agent1.sessionManager.session = undefined 1340 state = run(state, [ 1341 { 1342 type: 'received-agent-event', 1343 accountDid: 'alice-did', 1344 agent: agent1, 1345 refreshedAccount: undefined, 1346 sessionEvent: 'network-error', 1347 }, 1348 ]) 1349 expect(state.accounts.length).toBe(1) 1350 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 1351 expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') 1352 expect(state.currentAgentState.did).toBe('alice-did') 1353 expect(printState(state)).toMatchInlineSnapshot(` 1354 { 1355 "accounts": [ 1356 { 1357 "accessJwt": "alice-access-jwt-1", 1358 "active": true, 1359 "did": "alice-did", 1360 "email": undefined, 1361 "emailAuthFactor": false, 1362 "emailConfirmed": false, 1363 "handle": "alice.test", 1364 "isSelfHosted": true, 1365 "pdsUrl": undefined, 1366 "refreshJwt": "alice-refresh-jwt-1", 1367 "service": "https://alice.com/", 1368 "signupQueued": false, 1369 "status": undefined, 1370 }, 1371 ], 1372 "currentAgentState": { 1373 "agent": { 1374 "service": "https://alice.com/", 1375 }, 1376 "did": "alice-did", 1377 }, 1378 "needsPersist": true, 1379 } 1380 `) 1381 }) 1382 1383 it('resets tokens on expired event', () => { 1384 let state = getInitialState([]) 1385 1386 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1387 agent1.sessionManager.session = { 1388 active: true, 1389 did: 'alice-did', 1390 handle: 'alice.test', 1391 accessJwt: 'alice-access-jwt-1', 1392 refreshJwt: 'alice-refresh-jwt-1', 1393 } 1394 state = run(state, [ 1395 { 1396 type: 'switched-to-account', 1397 newAgent: agent1, 1398 newAccount: agentToSessionAccountOrThrow(agent1), 1399 }, 1400 ]) 1401 expect(state.accounts.length).toBe(1) 1402 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 1403 expect(state.currentAgentState.did).toBe('alice-did') 1404 1405 agent1.sessionManager.session = undefined 1406 state = run(state, [ 1407 { 1408 type: 'received-agent-event', 1409 accountDid: 'alice-did', 1410 agent: agent1, 1411 refreshedAccount: undefined, 1412 sessionEvent: 'expired', 1413 }, 1414 ]) 1415 expect(state.accounts.length).toBe(1) 1416 expect(state.accounts[0].accessJwt).toBe(undefined) 1417 expect(state.accounts[0].refreshJwt).toBe(undefined) 1418 expect(state.currentAgentState.did).toBe(undefined) 1419 expect(printState(state)).toMatchInlineSnapshot(` 1420 { 1421 "accounts": [ 1422 { 1423 "accessJwt": undefined, 1424 "active": true, 1425 "did": "alice-did", 1426 "email": undefined, 1427 "emailAuthFactor": false, 1428 "emailConfirmed": false, 1429 "handle": "alice.test", 1430 "isSelfHosted": true, 1431 "pdsUrl": undefined, 1432 "refreshJwt": undefined, 1433 "service": "https://alice.com/", 1434 "signupQueued": false, 1435 "status": undefined, 1436 }, 1437 ], 1438 "currentAgentState": { 1439 "agent": { 1440 "service": "https://public.api.bsky.app/", 1441 }, 1442 "did": undefined, 1443 }, 1444 "needsPersist": true, 1445 } 1446 `) 1447 }) 1448 1449 it('resets tokens on created-failed event', () => { 1450 let state = getInitialState([]) 1451 1452 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1453 agent1.sessionManager.session = { 1454 active: true, 1455 did: 'alice-did', 1456 handle: 'alice.test', 1457 accessJwt: 'alice-access-jwt-1', 1458 refreshJwt: 'alice-refresh-jwt-1', 1459 } 1460 state = run(state, [ 1461 { 1462 type: 'switched-to-account', 1463 newAgent: agent1, 1464 newAccount: agentToSessionAccountOrThrow(agent1), 1465 }, 1466 ]) 1467 expect(state.accounts.length).toBe(1) 1468 expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') 1469 expect(state.currentAgentState.did).toBe('alice-did') 1470 1471 agent1.sessionManager.session = undefined 1472 state = run(state, [ 1473 { 1474 type: 'received-agent-event', 1475 accountDid: 'alice-did', 1476 agent: agent1, 1477 refreshedAccount: undefined, 1478 sessionEvent: 'create-failed', 1479 }, 1480 ]) 1481 expect(state.accounts.length).toBe(1) 1482 expect(state.accounts[0].accessJwt).toBe(undefined) 1483 expect(state.accounts[0].refreshJwt).toBe(undefined) 1484 expect(state.currentAgentState.did).toBe(undefined) 1485 expect(printState(state)).toMatchInlineSnapshot(` 1486 { 1487 "accounts": [ 1488 { 1489 "accessJwt": undefined, 1490 "active": true, 1491 "did": "alice-did", 1492 "email": undefined, 1493 "emailAuthFactor": false, 1494 "emailConfirmed": false, 1495 "handle": "alice.test", 1496 "isSelfHosted": true, 1497 "pdsUrl": undefined, 1498 "refreshJwt": undefined, 1499 "service": "https://alice.com/", 1500 "signupQueued": false, 1501 "status": undefined, 1502 }, 1503 ], 1504 "currentAgentState": { 1505 "agent": { 1506 "service": "https://public.api.bsky.app/", 1507 }, 1508 "did": undefined, 1509 }, 1510 "needsPersist": true, 1511 } 1512 `) 1513 }) 1514 1515 it('replaces local accounts with synced accounts', () => { 1516 let state = getInitialState([]) 1517 1518 const agent1 = new BskyAgent({service: 'https://alice.com'}) 1519 agent1.sessionManager.session = { 1520 active: true, 1521 did: 'alice-did', 1522 handle: 'alice.test', 1523 accessJwt: 'alice-access-jwt-1', 1524 refreshJwt: 'alice-refresh-jwt-1', 1525 } 1526 const agent2 = new BskyAgent({service: 'https://bob.com'}) 1527 agent2.sessionManager.session = { 1528 active: true, 1529 did: 'bob-did', 1530 handle: 'bob.test', 1531 accessJwt: 'bob-access-jwt-1', 1532 refreshJwt: 'bob-refresh-jwt-1', 1533 } 1534 state = run(state, [ 1535 { 1536 type: 'switched-to-account', 1537 newAgent: agent1, 1538 newAccount: agentToSessionAccountOrThrow(agent1), 1539 }, 1540 { 1541 type: 'switched-to-account', 1542 newAgent: agent2, 1543 newAccount: agentToSessionAccountOrThrow(agent2), 1544 }, 1545 ]) 1546 expect(state.accounts.length).toBe(2) 1547 expect(state.currentAgentState.did).toBe('bob-did') 1548 1549 const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'}) 1550 anotherTabAgent1.sessionManager.session = { 1551 active: true, 1552 did: 'jay-did', 1553 handle: 'jay.test', 1554 accessJwt: 'jay-access-jwt-1', 1555 refreshJwt: 'jay-refresh-jwt-1', 1556 } 1557 const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'}) 1558 anotherTabAgent2.sessionManager.session = { 1559 active: true, 1560 did: 'bob-did', 1561 handle: 'bob.test', 1562 accessJwt: 'bob-access-jwt-2', 1563 refreshJwt: 'bob-refresh-jwt-2', 1564 } 1565 state = run(state, [ 1566 { 1567 type: 'synced-accounts', 1568 syncedAccounts: [ 1569 agentToSessionAccountOrThrow(anotherTabAgent1), 1570 agentToSessionAccountOrThrow(anotherTabAgent2), 1571 ], 1572 syncedCurrentDid: 'bob-did', 1573 }, 1574 ]) 1575 expect(state.accounts.length).toBe(2) 1576 expect(state.accounts[0].did).toBe('jay-did') 1577 expect(state.accounts[1].did).toBe('bob-did') 1578 expect(state.accounts[1].accessJwt).toBe('bob-access-jwt-2') 1579 // Keep Bob logged in. 1580 // (We patch up agent.session outside the reducer for this to work.) 1581 expect(state.currentAgentState.did).toBe('bob-did') 1582 expect(state.needsPersist).toBe(false) 1583 expect(printState(state)).toMatchInlineSnapshot(` 1584 { 1585 "accounts": [ 1586 { 1587 "accessJwt": "jay-access-jwt-1", 1588 "active": true, 1589 "did": "jay-did", 1590 "email": undefined, 1591 "emailAuthFactor": false, 1592 "emailConfirmed": false, 1593 "handle": "jay.test", 1594 "isSelfHosted": true, 1595 "pdsUrl": undefined, 1596 "refreshJwt": "jay-refresh-jwt-1", 1597 "service": "https://jay.com/", 1598 "signupQueued": false, 1599 "status": undefined, 1600 }, 1601 { 1602 "accessJwt": "bob-access-jwt-2", 1603 "active": true, 1604 "did": "bob-did", 1605 "email": undefined, 1606 "emailAuthFactor": false, 1607 "emailConfirmed": false, 1608 "handle": "bob.test", 1609 "isSelfHosted": true, 1610 "pdsUrl": undefined, 1611 "refreshJwt": "bob-refresh-jwt-2", 1612 "service": "https://alice.com/", 1613 "signupQueued": false, 1614 "status": undefined, 1615 }, 1616 ], 1617 "currentAgentState": { 1618 "agent": { 1619 "service": "https://bob.com/", 1620 }, 1621 "did": "bob-did", 1622 }, 1623 "needsPersist": false, 1624 } 1625 `) 1626 1627 const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'}) 1628 anotherTabAgent3.sessionManager.session = { 1629 active: true, 1630 did: 'clarence-did', 1631 handle: 'clarence.test', 1632 accessJwt: 'clarence-access-jwt-2', 1633 refreshJwt: 'clarence-refresh-jwt-2', 1634 } 1635 state = run(state, [ 1636 { 1637 type: 'synced-accounts', 1638 syncedAccounts: [agentToSessionAccountOrThrow(anotherTabAgent3)], 1639 syncedCurrentDid: 'clarence-did', 1640 }, 1641 ]) 1642 expect(state.accounts.length).toBe(1) 1643 expect(state.accounts[0].did).toBe('clarence-did') 1644 // Log out because we have no matching user. 1645 // (In practice, we'll resume this session outside the reducer.) 1646 expect(state.currentAgentState.did).toBe(undefined) 1647 expect(state.needsPersist).toBe(false) 1648 expect(printState(state)).toMatchInlineSnapshot(` 1649 { 1650 "accounts": [ 1651 { 1652 "accessJwt": "clarence-access-jwt-2", 1653 "active": true, 1654 "did": "clarence-did", 1655 "email": undefined, 1656 "emailAuthFactor": false, 1657 "emailConfirmed": false, 1658 "handle": "clarence.test", 1659 "isSelfHosted": true, 1660 "pdsUrl": undefined, 1661 "refreshJwt": "clarence-refresh-jwt-2", 1662 "service": "https://clarence.com/", 1663 "signupQueued": false, 1664 "status": undefined, 1665 }, 1666 ], 1667 "currentAgentState": { 1668 "agent": { 1669 "service": "https://public.api.bsky.app/", 1670 }, 1671 "did": undefined, 1672 }, 1673 "needsPersist": false, 1674 } 1675 `) 1676 }) 1677}) 1678 1679function run(initialState: State, actions: Action[]): State { 1680 let state = initialState 1681 for (let action of actions) { 1682 state = reducer(state, action) 1683 } 1684 return state 1685} 1686 1687function printState(state: State) { 1688 return { 1689 accounts: state.accounts, 1690 currentAgentState: { 1691 agent: {service: state.currentAgentState.agent.service}, 1692 did: state.currentAgentState.did, 1693 }, 1694 needsPersist: state.needsPersist, 1695 } 1696}