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! :)
at main 174 lines 5.4 kB view raw
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 36// Mock node:fs/promises for metadata file storage 37const mockFileStorage = new Map<string, string>(); 38 39vi.mock('node:fs/promises', () => ({ 40 mkdir: vi.fn().mockResolvedValue(undefined), 41 writeFile: vi.fn().mockImplementation(async (path: string, content: string) => { 42 mockFileStorage.set(path as string, content); 43 }), 44 readFile: vi.fn().mockImplementation(async (path: string) => { 45 const content = mockFileStorage.get(path as string); 46 if (content === undefined) { 47 const err = Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), { 48 code: 'ENOENT', 49 }); 50 throw err; 51 } 52 return content; 53 }), 54 unlink: vi.fn().mockImplementation(async (path: string) => { 55 if (!mockFileStorage.has(path as string)) { 56 const err = Object.assign(new Error(`ENOENT: no such file or directory, unlink '${path}'`), { 57 code: 'ENOENT', 58 }); 59 throw err; 60 } 61 mockFileStorage.delete(path as string); 62 }), 63})); 64 65describe('Session Management', () => { 66 beforeEach(() => { 67 // Clear mock storage before each test 68 mockKeyringStorage.clear(); 69 mockFileStorage.clear(); 70 vi.clearAllMocks(); 71 }); 72 73 afterEach(() => { 74 // Clean up after each test 75 mockKeyringStorage.clear(); 76 mockFileStorage.clear(); 77 }); 78 79 describe('saveSession', () => { 80 it('should save session to keychain using DID', async () => { 81 await saveSession(mockSessionData); 82 83 // Verify session was stored 84 const loaded = await loadSession(mockSessionData.did); 85 expect(loaded).toEqual(mockSessionData); 86 }); 87 88 it('should save and retrieve session with all required fields', async () => { 89 await saveSession(mockSessionData2); 90 91 // Verify session was stored (keyed by DID) 92 const loaded = await loadSession(mockSessionData2.did); 93 expect(loaded).toEqual(mockSessionData2); 94 }); 95 96 it('should throw error if session has no DID or handle', async () => { 97 const sessionData = { 98 email: 'user@example.com', 99 accessJwt: 'token123', 100 refreshJwt: 'refresh123', 101 } as AtpSessionData; 102 103 await expect(saveSession(sessionData)).rejects.toThrow( 104 'Session data must include DID or handle' 105 ); 106 }); 107 }); 108 109 describe('loadSession', () => { 110 it('should load session from keychain', async () => { 111 await saveSession(mockSessionData); 112 const result = await loadSession(mockSessionData.did); 113 114 expect(result).toEqual(mockSessionData); 115 }); 116 117 it('should return null when session not found', async () => { 118 const result = await loadSession('did:plc:notfound'); 119 expect(result).toBeNull(); 120 }); 121 }); 122 123 describe('deleteSession', () => { 124 it('should delete session from keychain', async () => { 125 await saveSession(mockSessionData); 126 127 // Verify session exists 128 let loaded = await loadSession(mockSessionData.did); 129 expect(loaded).toEqual(mockSessionData); 130 131 // Delete session 132 const deleted = await deleteSession(mockSessionData.did); 133 expect(deleted).toBe(true); 134 135 // Verify session no longer exists 136 loaded = await loadSession(mockSessionData.did); 137 expect(loaded).toBeNull(); 138 }); 139 140 it('should return false when deleting non-existent session', async () => { 141 const deleted = await deleteSession('did:plc:notfound'); 142 expect(deleted).toBe(false); 143 }); 144 }); 145 146 describe('session metadata', () => { 147 it('should save and load current session metadata', async () => { 148 await saveCurrentSessionMetadata(mockSessionMetadata); 149 const result = await getCurrentSessionMetadata(); 150 151 expect(result).toEqual(mockSessionMetadata); 152 }); 153 154 it('should return null when no metadata exists', async () => { 155 const result = await getCurrentSessionMetadata(); 156 expect(result).toBeNull(); 157 }); 158 159 it('should clear current session metadata', async () => { 160 await saveCurrentSessionMetadata(mockSessionMetadata); 161 162 // Verify metadata exists 163 let result = await getCurrentSessionMetadata(); 164 expect(result).toEqual(mockSessionMetadata); 165 166 // Clear metadata 167 await clearCurrentSessionMetadata(); 168 169 // Verify metadata no longer exists 170 result = await getCurrentSessionMetadata(); 171 expect(result).toBeNull(); 172 }); 173 }); 174});