Bluesky app fork with some witchin' additions 💫

Add session reducer tests (#3860)

authored by danabra.mov and committed by

GitHub 814ec2bd 0910525e

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