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