import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createCreateCommand } from '../../src/commands/create.js'; import { type ApiClient, ApiError } from '../../src/api-client.js'; function mockClient(): ApiClient { return { get: vi.fn(async () => []), post: vi.fn(async () => ({ id: 'new-id', name: 'test' })), put: vi.fn(async () => ({})), delete: vi.fn(async () => {}), } as unknown as ApiClient; } describe('create command', () => { let client: ReturnType; let output: string[]; const log = (...args: unknown[]) => output.push(args.map(String).join(' ')); beforeEach(() => { client = mockClient(); output = []; }); describe('create server', () => { it('creates a server with minimal flags', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['server', 'my-server'], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({ name: 'my-server', transport: 'STDIO', replicas: 1, })); expect(output.join('\n')).toContain("server 'test' created"); }); it('creates a server with all flags', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync([ 'server', 'ha-mcp', '-d', 'Home Assistant MCP', '--docker-image', 'ghcr.io/ha-mcp:latest', '--transport', 'STREAMABLE_HTTP', '--external-url', 'http://localhost:8086/mcp', '--container-port', '3000', '--replicas', '2', '--command', 'python', '--command', '-c', '--command', 'print("hello")', '--env', 'API_KEY=secretRef:creds:API_KEY', '--env', 'BASE_URL=http://localhost', ], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/servers', { name: 'ha-mcp', description: 'Home Assistant MCP', dockerImage: 'ghcr.io/ha-mcp:latest', transport: 'STREAMABLE_HTTP', externalUrl: 'http://localhost:8086/mcp', containerPort: 3000, replicas: 2, command: ['python', '-c', 'print("hello")'], env: [ { name: 'API_KEY', valueFrom: { secretRef: { name: 'creds', key: 'API_KEY' } } }, { name: 'BASE_URL', value: 'http://localhost' }, ], }); }); it('defaults transport to STDIO', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['server', 'test'], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({ transport: 'STDIO', })); }); it('strips null values from template when using --from-template', async () => { vi.mocked(client.get).mockResolvedValueOnce([{ id: 'tpl-1', name: 'grafana', version: '1.0.0', description: 'Grafana MCP', packageName: '@leval/mcp-grafana', dockerImage: null, transport: 'STDIO', repositoryUrl: 'https://github.com/test', externalUrl: null, command: null, containerPort: null, replicas: 1, env: [{ name: 'TOKEN', required: true, description: 'A token' }], healthCheck: { tool: 'test', arguments: {} }, createdAt: '2025-01-01', updatedAt: '2025-01-01', }] as never); const cmd = createCreateCommand({ client, log }); await cmd.parseAsync([ 'server', 'my-grafana', '--from-template=grafana', '--env', 'TOKEN=secretRef:creds:TOKEN', ], { from: 'user' }); const call = vi.mocked(client.post).mock.calls[0]![1] as Record; // null fields from template should NOT be in the body expect(call).not.toHaveProperty('dockerImage'); expect(call).not.toHaveProperty('externalUrl'); expect(call).not.toHaveProperty('command'); expect(call).not.toHaveProperty('containerPort'); // non-null fields should be present expect(call.packageName).toBe('@leval/mcp-grafana'); expect(call.healthCheck).toEqual({ tool: 'test', arguments: {} }); expect(call.templateName).toBe('grafana'); }); it('throws on 409 without --force', async () => { vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists: my-server"}')); const cmd = createCreateCommand({ client, log }); await expect(cmd.parseAsync(['server', 'my-server'], { from: 'user' })).rejects.toThrow('API error 409'); }); it('updates existing server on 409 with --force', async () => { vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists"}')); vi.mocked(client.get).mockResolvedValueOnce([{ id: 'srv-1', name: 'my-server' }] as never); const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['server', 'my-server', '--force'], { from: 'user' }); expect(client.put).toHaveBeenCalledWith('/api/v1/servers/srv-1', expect.objectContaining({ transport: 'STDIO', })); expect(output.join('\n')).toContain("server 'my-server' updated"); }); }); describe('create secret', () => { it('creates a secret with --data flags', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync([ 'secret', 'ha-creds', '--data', 'TOKEN=abc123', '--data', 'URL=https://ha.local', ], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', { name: 'ha-creds', data: { TOKEN: 'abc123', URL: 'https://ha.local' }, }); expect(output.join('\n')).toContain("secret 'test' created"); }); it('creates a secret with empty data', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['secret', 'empty-secret'], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', { name: 'empty-secret', data: {}, }); }); it('throws on 409 without --force', async () => { vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists: my-creds"}')); const cmd = createCreateCommand({ client, log }); await expect(cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val'], { from: 'user' })).rejects.toThrow('API error 409'); }); it('updates existing secret on 409 with --force', async () => { vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists"}')); vi.mocked(client.get).mockResolvedValueOnce([{ id: 'sec-1', name: 'my-creds' }] as never); const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val', '--force'], { from: 'user' }); expect(client.put).toHaveBeenCalledWith('/api/v1/secrets/sec-1', { data: { KEY: 'val' } }); expect(output.join('\n')).toContain("secret 'my-creds' updated"); }); }); describe('create project', () => { it('creates a project', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['project', 'my-project', '-d', 'A test project'], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/projects', { name: 'my-project', description: 'A test project', }); expect(output.join('\n')).toContain("project 'test' created"); }); it('creates a project with no description', async () => { const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['project', 'minimal'], { from: 'user' }); expect(client.post).toHaveBeenCalledWith('/api/v1/projects', { name: 'minimal', description: '', }); }); it('updates existing project on 409 with --force', async () => { vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Project already exists"}')); vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-proj' }] as never); const cmd = createCreateCommand({ client, log }); await cmd.parseAsync(['project', 'my-proj', '-d', 'updated', '--force'], { from: 'user' }); expect(client.put).toHaveBeenCalledWith('/api/v1/projects/proj-1', { description: 'updated' }); expect(output.join('\n')).toContain("project 'my-proj' updated"); }); }); });