- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery - Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers - Add mcpd auth (login/logout) and MCP proxy endpoints - Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag - Add tiered health monitoring, shell completions, e2e integration tests - 57 test files, 597 tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
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 () => {},
|
|
});
|
|
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 () => {},
|
|
});
|
|
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 () => {},
|
|
});
|
|
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 () => {},
|
|
});
|
|
await cmd.parseAsync(['--mcpd-url', 'http://override:3100'], { from: 'user' });
|
|
expect(capturedUrl).toBe('http://override:3100');
|
|
});
|
|
});
|
|
|
|
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; },
|
|
});
|
|
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 () => {},
|
|
});
|
|
await cmd.parseAsync([], { from: 'user' });
|
|
expect(output[0]).toContain('Not logged in');
|
|
});
|
|
});
|