WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
1import type { AtpSessionData } from '@atproto/api';
2import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3import {
4 clearCurrentSessionMetadata,
5 deleteSession,
6 getCurrentSessionMetadata,
7 loadSession,
8 saveCurrentSessionMetadata,
9 saveSession,
10} from '../../src/lib/session.js';
11import { mockSessionData, mockSessionData2, mockSessionMetadata } from '../helpers/mock-data.js';
12
13// Mock @napi-rs/keyring
14const mockKeyringStorage = new Map<string, string>();
15
16vi.mock('@napi-rs/keyring', () => {
17 return {
18 AsyncEntry: vi.fn().mockImplementation((service: string, account: string) => {
19 const key = `${service}:${account}`;
20
21 return {
22 setPassword: vi.fn().mockImplementation(async (password: string) => {
23 mockKeyringStorage.set(key, password);
24 }),
25 getPassword: vi.fn().mockImplementation(async () => {
26 return mockKeyringStorage.get(key) || null;
27 }),
28 deleteCredential: vi.fn().mockImplementation(async () => {
29 return mockKeyringStorage.delete(key);
30 }),
31 };
32 }),
33 };
34});
35
36describe('Session Management', () => {
37 beforeEach(() => {
38 // Clear mock storage before each test
39 mockKeyringStorage.clear();
40 vi.clearAllMocks();
41 });
42
43 afterEach(() => {
44 // Clean up after each test
45 mockKeyringStorage.clear();
46 });
47
48 describe('saveSession', () => {
49 it('should save session to keychain using DID', async () => {
50 await saveSession(mockSessionData);
51
52 // Verify session was stored
53 const loaded = await loadSession(mockSessionData.did);
54 expect(loaded).toEqual(mockSessionData);
55 });
56
57 it('should save and retrieve session with all required fields', async () => {
58 await saveSession(mockSessionData2);
59
60 // Verify session was stored (keyed by DID)
61 const loaded = await loadSession(mockSessionData2.did);
62 expect(loaded).toEqual(mockSessionData2);
63 });
64
65 it('should throw error if session has no DID or handle', async () => {
66 const sessionData = {
67 email: 'user@example.com',
68 accessJwt: 'token123',
69 refreshJwt: 'refresh123',
70 } as AtpSessionData;
71
72 await expect(saveSession(sessionData)).rejects.toThrow(
73 'Session data must include DID or handle'
74 );
75 });
76 });
77
78 describe('loadSession', () => {
79 it('should load session from keychain', async () => {
80 await saveSession(mockSessionData);
81 const result = await loadSession(mockSessionData.did);
82
83 expect(result).toEqual(mockSessionData);
84 });
85
86 it('should return null when session not found', async () => {
87 const result = await loadSession('did:plc:notfound');
88 expect(result).toBeNull();
89 });
90 });
91
92 describe('deleteSession', () => {
93 it('should delete session from keychain', async () => {
94 await saveSession(mockSessionData);
95
96 // Verify session exists
97 let loaded = await loadSession(mockSessionData.did);
98 expect(loaded).toEqual(mockSessionData);
99
100 // Delete session
101 const deleted = await deleteSession(mockSessionData.did);
102 expect(deleted).toBe(true);
103
104 // Verify session no longer exists
105 loaded = await loadSession(mockSessionData.did);
106 expect(loaded).toBeNull();
107 });
108
109 it('should return false when deleting non-existent session', async () => {
110 const deleted = await deleteSession('did:plc:notfound');
111 expect(deleted).toBe(false);
112 });
113 });
114
115 describe('session metadata', () => {
116 it('should save and load current session metadata', async () => {
117 await saveCurrentSessionMetadata(mockSessionMetadata);
118 const result = await getCurrentSessionMetadata();
119
120 expect(result).toEqual(mockSessionMetadata);
121 });
122
123 it('should return null when no metadata exists', async () => {
124 const result = await getCurrentSessionMetadata();
125 expect(result).toBeNull();
126 });
127
128 it('should clear current session metadata', async () => {
129 await saveCurrentSessionMetadata(mockSessionMetadata);
130
131 // Verify metadata exists
132 let result = await getCurrentSessionMetadata();
133 expect(result).toEqual(mockSessionMetadata);
134
135 // Clear metadata
136 await clearCurrentSessionMetadata();
137
138 // Verify metadata no longer exists
139 result = await getCurrentSessionMetadata();
140 expect(result).toBeNull();
141 });
142 });
143});