import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createLoginCommand, createLogoutCommand } from '../../src/commands/auth.js'; import { saveCredentials, loadCredentials } from '../../src/auth/index.js'; import { saveConfig, DEFAULT_CONFIG } from '../../src/config/index.js'; let tempDir: string; let output: string[]; function log(...args: string[]) { output.push(args.join(' ')); } beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-auth-cmd-test-')); output = []; }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); describe('login command', () => { it('stores credentials on successful login', async () => { const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => 'alice@test.com', password: async () => 'secret123', }, log, loginRequest: async (_url, email, _password) => ({ token: 'session-token-123', user: { email }, }), logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync([], { from: 'user' }); expect(output[0]).toContain('Logged in as alice@test.com'); const creds = loadCredentials({ configDir: tempDir }); expect(creds).not.toBeNull(); expect(creds!.token).toBe('session-token-123'); expect(creds!.user).toBe('alice@test.com'); }); it('shows error on failed login', async () => { const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => 'alice@test.com', password: async () => 'wrong', }, log, loginRequest: async () => { throw new Error('Invalid credentials'); }, logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync([], { from: 'user' }); expect(output[0]).toContain('Login failed'); expect(output[0]).toContain('Invalid credentials'); const creds = loadCredentials({ configDir: tempDir }); expect(creds).toBeNull(); }); it('uses mcpdUrl from config', async () => { saveConfig({ ...DEFAULT_CONFIG, mcpdUrl: 'http://custom:3100' }, { configDir: tempDir }); let capturedUrl = ''; const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => 'user@test.com', password: async () => 'pass', }, log, loginRequest: async (url, email) => { capturedUrl = url; return { token: 'tok', user: { email } }; }, logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync([], { from: 'user' }); expect(capturedUrl).toBe('http://custom:3100'); }); it('allows --mcpd-url flag override', async () => { let capturedUrl = ''; const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => 'user@test.com', password: async () => 'pass', }, log, loginRequest: async (url, email) => { capturedUrl = url; return { token: 'tok', user: { email } }; }, logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync(['--mcpd-url', 'http://override:3100'], { from: 'user' }); expect(capturedUrl).toBe('http://override:3100'); }); }); describe('login bootstrap flow', () => { it('bootstraps first admin when no users exist', async () => { let bootstrapCalled = false; const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async (msg) => { if (msg.includes('Name')) return 'Admin User'; return 'admin@test.com'; }, password: async () => 'admin-pass', }, log, loginRequest: async () => ({ token: '', user: { email: '' } }), logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: false }), bootstrapRequest: async (_url, email, _password) => { bootstrapCalled = true; return { token: 'admin-token', user: { email } }; }, }); await cmd.parseAsync([], { from: 'user' }); expect(bootstrapCalled).toBe(true); expect(output.join('\n')).toContain('No users configured'); expect(output.join('\n')).toContain('admin@test.com'); expect(output.join('\n')).toContain('admin'); const creds = loadCredentials({ configDir: tempDir }); expect(creds).not.toBeNull(); expect(creds!.token).toBe('admin-token'); expect(creds!.user).toBe('admin@test.com'); }); it('falls back to normal login when users exist', async () => { let loginCalled = false; const cmd = createLoginCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => 'alice@test.com', password: async () => 'secret', }, log, loginRequest: async (_url, email) => { loginCalled = true; return { token: 'session-tok', user: { email } }; }, logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => { throw new Error('Should not be called'); }, }); await cmd.parseAsync([], { from: 'user' }); expect(loginCalled).toBe(true); expect(output.join('\n')).not.toContain('No users configured'); }); }); describe('logout command', () => { it('removes credentials on logout', async () => { saveCredentials({ token: 'tok', mcpdUrl: 'http://x:3100', user: 'alice' }, { configDir: tempDir }); let logoutCalled = false; const cmd = createLogoutCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => '', password: async () => '' }, log, loginRequest: async () => ({ token: '', user: { email: '' } }), logoutRequest: async () => { logoutCalled = true; }, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync([], { from: 'user' }); expect(output[0]).toContain('Logged out successfully'); expect(logoutCalled).toBe(true); const creds = loadCredentials({ configDir: tempDir }); expect(creds).toBeNull(); }); it('shows not logged in when no credentials', async () => { const cmd = createLogoutCommand({ configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, prompt: { input: async () => '', password: async () => '' }, log, loginRequest: async () => ({ token: '', user: { email: '' } }), logoutRequest: async () => {}, statusRequest: async () => ({ hasUsers: true }), bootstrapRequest: async () => ({ token: '', user: { email: '' } }), }); await cmd.parseAsync([], { from: 'user' }); expect(output[0]).toContain('Not logged in'); }); });