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 { beforeEach, describe, expect, it, vi } from 'vitest';
2import { createAuthCommand } from '../../src/commands/auth.js';
3import * as apiClientModule from '../../src/lib/api-client.js';
4import * as sessionModule from '../../src/lib/session.js';
5import * as promptsModule from '../../src/utils/prompts.js';
6import { mockSessionData, mockSessionMetadata } from '../helpers/mock-data.js';
7
8// Mock modules
9vi.mock('../../src/lib/api-client.js');
10vi.mock('../../src/lib/session.js');
11vi.mock('../../src/utils/prompts.js');
12
13describe('Auth Commands', () => {
14 let mockClient: {
15 login: ReturnType<typeof vi.fn>;
16 logout: ReturnType<typeof vi.fn>;
17 };
18 let consoleLogSpy: ReturnType<typeof vi.fn>;
19 let consoleErrorSpy: ReturnType<typeof vi.fn>;
20 let processExitSpy: ReturnType<typeof vi.fn>;
21
22 beforeEach(() => {
23 vi.clearAllMocks();
24
25 // Mock API client
26 mockClient = {
27 login: vi.fn(),
28 logout: vi.fn(),
29 };
30 vi.mocked(apiClientModule.createApiClient).mockReturnValue(mockClient as never);
31
32 // Mock console methods
33 consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) as never;
34 consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) as never;
35
36 // Mock process.exit to throw to stop execution (mimicking real behavior)
37 processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
38 throw new Error(`process.exit(${code})`);
39 }) as never;
40 });
41
42 describe('login command', () => {
43 it('should login successfully with valid credentials', async () => {
44 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null);
45 vi.mocked(promptsModule.promptForLogin).mockResolvedValue({
46 identifier: 'user.bsky.social',
47 password: 'test-password',
48 });
49 mockClient.login.mockResolvedValue(mockSessionData);
50
51 const auth = createAuthCommand();
52 await auth.parseAsync(['node', 'test', 'login']);
53
54 expect(promptsModule.promptForLogin).toHaveBeenCalled();
55 expect(mockClient.login).toHaveBeenCalledWith('user.bsky.social', 'test-password');
56 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Successfully logged in'));
57 expect(consoleLogSpy).toHaveBeenCalledWith(
58 expect.stringContaining(`@${mockSessionData.handle}`)
59 );
60 });
61
62 it('should prevent login when already authenticated', async () => {
63 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata);
64
65 const auth = createAuthCommand();
66 await expect(auth.parseAsync(['node', 'test', 'login'])).rejects.toThrow('process.exit');
67
68 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Already logged in'));
69 expect(promptsModule.promptForLogin).not.toHaveBeenCalled();
70 expect(processExitSpy).toHaveBeenCalled();
71 });
72
73 it('should handle login errors gracefully', async () => {
74 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null);
75 vi.mocked(promptsModule.promptForLogin).mockResolvedValue({
76 identifier: 'user.bsky.social',
77 password: 'wrong-password',
78 });
79 mockClient.login.mockRejectedValue(new Error('Invalid credentials'));
80
81 const auth = createAuthCommand();
82 await expect(auth.parseAsync(['node', 'test', 'login'])).rejects.toThrow('process.exit(1)');
83
84 expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Login failed'));
85 expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid credentials'));
86 expect(processExitSpy).toHaveBeenCalledWith(1);
87 });
88 });
89
90 describe('logout command', () => {
91 it('should logout successfully when authenticated', async () => {
92 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata);
93 mockClient.logout.mockResolvedValue(undefined);
94
95 const auth = createAuthCommand();
96 await auth.parseAsync(['node', 'test', 'logout']);
97
98 expect(mockClient.logout).toHaveBeenCalled();
99 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Logged out'));
100 expect(consoleLogSpy).toHaveBeenCalledWith(
101 expect.stringContaining(`@${mockSessionMetadata.handle}`)
102 );
103 });
104
105 it('should handle logout when not authenticated', async () => {
106 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null);
107
108 const auth = createAuthCommand();
109 await expect(auth.parseAsync(['node', 'test', 'logout'])).rejects.toThrow('process.exit');
110
111 expect(mockClient.logout).not.toHaveBeenCalled();
112 expect(consoleLogSpy).toHaveBeenCalledWith('Not currently logged in');
113 expect(processExitSpy).toHaveBeenCalled();
114 });
115
116 it('should handle logout errors gracefully', async () => {
117 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata);
118 mockClient.logout.mockRejectedValue(new Error('Logout failed'));
119
120 const auth = createAuthCommand();
121 await expect(auth.parseAsync(['node', 'test', 'logout'])).rejects.toThrow('process.exit(1)');
122
123 expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Logout failed'));
124 expect(processExitSpy).toHaveBeenCalledWith(1);
125 });
126 });
127
128 describe('status command', () => {
129 it('should show authenticated status with session details', async () => {
130 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(mockSessionMetadata);
131
132 const auth = createAuthCommand();
133 await auth.parseAsync(['node', 'test', 'status']);
134
135 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Authenticated'));
136 expect(consoleLogSpy).toHaveBeenCalledWith(
137 expect.stringContaining(`@${mockSessionMetadata.handle}`)
138 );
139 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining(mockSessionMetadata.did));
140 });
141
142 it('should show not authenticated status', async () => {
143 vi.mocked(sessionModule.getCurrentSessionMetadata).mockResolvedValue(null);
144
145 const auth = createAuthCommand();
146 await auth.parseAsync(['node', 'test', 'status']);
147
148 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Not authenticated'));
149 expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('tangled auth login'));
150 });
151
152 it('should handle status check errors gracefully', async () => {
153 vi.mocked(sessionModule.getCurrentSessionMetadata).mockRejectedValue(
154 new Error('Failed to read session')
155 );
156
157 const auth = createAuthCommand();
158 await expect(auth.parseAsync(['node', 'test', 'status'])).rejects.toThrow('process.exit(1)');
159
160 expect(consoleErrorSpy).toHaveBeenCalledWith(
161 expect.stringContaining('Failed to check status')
162 );
163 expect(processExitSpy).toHaveBeenCalledWith(1);
164 });
165 });
166});