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