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