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 pull-request-format-testing 173 lines 5.9 kB view raw
1import type { AtpSessionData } from '@atproto/api'; 2import { beforeEach, describe, expect, it, vi } from 'vitest'; 3import { TangledApiClient } from '../../src/lib/api-client.js'; 4import * as sessionModule from '../../src/lib/session.js'; 5import { mockSessionData, mockSessionMetadata } from '../helpers/mock-data.js'; 6 7// Mock @atproto/api 8vi.mock('@atproto/api', () => { 9 return { 10 AtpAgent: vi.fn().mockImplementation(() => { 11 let currentSession: AtpSessionData | undefined = undefined; 12 13 return { 14 service: { toString: () => 'https://bsky.social' }, 15 get session() { 16 return currentSession; 17 }, 18 login: vi.fn().mockImplementation(async () => { 19 currentSession = mockSessionData; 20 return { 21 success: true, 22 data: mockSessionData, 23 }; 24 }), 25 resumeSession: vi.fn().mockImplementation(async (session) => { 26 currentSession = session; 27 }), 28 }; 29 }), 30 }; 31}); 32 33// Mock session management 34vi.mock('../../src/lib/session.js', () => ({ 35 saveSession: vi.fn(), 36 loadSession: vi.fn(), 37 deleteSession: vi.fn(), 38 saveCurrentSessionMetadata: vi.fn(), 39 getCurrentSessionMetadata: vi.fn(), 40 clearCurrentSessionMetadata: vi.fn(), 41})); 42 43describe('TangledApiClient', () => { 44 let client: TangledApiClient; 45 46 beforeEach(() => { 47 vi.clearAllMocks(); 48 49 // Reset mock implementations 50 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null); 51 vi.mocked(sessionModule.loadSession).mockResolvedValue(null); 52 53 client = new TangledApiClient(); 54 }); 55 56 describe('login', () => { 57 it('should login successfully and save session', async () => { 58 const result = await client.login('user.bsky.social', 'password'); 59 60 expect(result).toEqual(mockSessionData); 61 expect(vi.mocked(sessionModule.saveSession)).toHaveBeenCalledWith(mockSessionData); 62 expect(vi.mocked(sessionModule.saveCurrentSessionMetadata)).toHaveBeenCalledWith({ 63 handle: mockSessionData.handle, 64 did: mockSessionData.did, 65 pds: 'https://bsky.social', 66 lastUsed: expect.any(String), 67 }); 68 }); 69 70 it('should support custom domain handles', async () => { 71 const result = await client.login('markbennett.ca', 'password'); 72 73 expect(result).toEqual(mockSessionData); 74 expect(vi.mocked(sessionModule.saveSession)).toHaveBeenCalled(); 75 }); 76 77 it('should throw error on login failure', async () => { 78 const agent = client.getAgent(); 79 vi.mocked(agent.login).mockResolvedValueOnce({ 80 success: false, 81 headers: {}, 82 data: undefined, 83 } as never); 84 85 await expect(client.login('user.bsky.social', 'wrong')).rejects.toThrow( 86 'Login failed: No session data received' 87 ); 88 }); 89 }); 90 91 describe('logout', () => { 92 it('should logout and clear session', async () => { 93 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata); 94 95 await client.logout(); 96 97 expect(vi.mocked(sessionModule.deleteSession)).toHaveBeenCalledWith(mockSessionMetadata.did); 98 expect(vi.mocked(sessionModule.clearCurrentSessionMetadata)).toHaveBeenCalled(); 99 }); 100 101 it('should throw error if no active session', async () => { 102 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null); 103 104 await expect(client.logout()).rejects.toThrow('No active session found'); 105 }); 106 }); 107 108 describe('resumeSession', () => { 109 it('should resume session from stored data', async () => { 110 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata); 111 vi.mocked(sessionModule.loadSession).mockResolvedValue(mockSessionData); 112 113 const resumed = await client.resumeSession(); 114 115 expect(resumed).toBe(true); 116 expect(vi.mocked(sessionModule.loadSession)).toHaveBeenCalledWith(mockSessionMetadata.did); 117 expect(vi.mocked(sessionModule.saveCurrentSessionMetadata)).toHaveBeenCalledWith({ 118 ...mockSessionMetadata, 119 lastUsed: expect.any(String), 120 }); 121 }); 122 123 it('should return false if no metadata exists', async () => { 124 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null); 125 126 const resumed = await client.resumeSession(); 127 128 expect(resumed).toBe(false); 129 }); 130 131 it('should return false and cleanup if session data is missing', async () => { 132 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata); 133 vi.mocked(sessionModule.loadSession).mockResolvedValue(null); 134 135 const resumed = await client.resumeSession(); 136 137 expect(resumed).toBe(false); 138 expect(vi.mocked(sessionModule.clearCurrentSessionMetadata)).toHaveBeenCalled(); 139 }); 140 141 it('should return false and cleanup on resume error', async () => { 142 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata); 143 vi.mocked(sessionModule.loadSession).mockResolvedValue(mockSessionData); 144 145 const agent = client.getAgent(); 146 vi.mocked(agent.resumeSession).mockRejectedValueOnce(new Error('Resume failed')); 147 148 const resumed = await client.resumeSession(); 149 150 expect(resumed).toBe(false); 151 expect(vi.mocked(sessionModule.clearCurrentSessionMetadata)).toHaveBeenCalled(); 152 }); 153 }); 154 155 describe('isAuthenticated', () => { 156 it('should return false when not authenticated', () => { 157 expect(client.isAuthenticated()).toBe(false); 158 }); 159 }); 160 161 describe('getAgent', () => { 162 it('should return the AtpAgent instance', () => { 163 const agent = client.getAgent(); 164 expect(agent).toBeDefined(); 165 }); 166 }); 167 168 describe('getSession', () => { 169 it('should return undefined when not authenticated', () => { 170 expect(client.getSession()).toBeUndefined(); 171 }); 172 }); 173});