2026-02-21 04:26:18 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { McpServerService, NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
|
|
|
|
|
import type { IMcpServerRepository } from '../src/repositories/interfaces.js';
|
|
|
|
|
|
|
|
|
|
function mockRepo(): IMcpServerRepository {
|
|
|
|
|
return {
|
|
|
|
|
findAll: vi.fn(async () => []),
|
|
|
|
|
findById: vi.fn(async () => null),
|
|
|
|
|
findByName: vi.fn(async () => null),
|
|
|
|
|
create: vi.fn(async (data) => ({
|
|
|
|
|
id: 'new-id',
|
|
|
|
|
name: data.name,
|
|
|
|
|
description: data.description ?? '',
|
|
|
|
|
packageName: data.packageName ?? null,
|
|
|
|
|
dockerImage: null,
|
|
|
|
|
transport: data.transport ?? 'STDIO',
|
|
|
|
|
repositoryUrl: data.repositoryUrl ?? null,
|
feat: replace profiles with kubernetes-style secrets
Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).
- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:40:58 +00:00
|
|
|
env: data.env ?? [],
|
2026-02-21 04:26:18 +00:00
|
|
|
version: 1,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})),
|
|
|
|
|
update: vi.fn(async (id, data) => ({
|
|
|
|
|
id,
|
|
|
|
|
name: 'test',
|
|
|
|
|
description: (data.description as string) ?? '',
|
|
|
|
|
packageName: null,
|
|
|
|
|
dockerImage: null,
|
|
|
|
|
transport: 'STDIO' as const,
|
|
|
|
|
repositoryUrl: null,
|
feat: replace profiles with kubernetes-style secrets
Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).
- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:40:58 +00:00
|
|
|
env: [],
|
2026-02-21 04:26:18 +00:00
|
|
|
version: 2,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})),
|
|
|
|
|
delete: vi.fn(async () => {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('McpServerService', () => {
|
|
|
|
|
let repo: ReturnType<typeof mockRepo>;
|
|
|
|
|
let service: McpServerService;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
repo = mockRepo();
|
|
|
|
|
service = new McpServerService(repo);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('list', () => {
|
|
|
|
|
it('returns all servers', async () => {
|
|
|
|
|
const servers = await service.list();
|
|
|
|
|
expect(repo.findAll).toHaveBeenCalled();
|
|
|
|
|
expect(servers).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getById', () => {
|
|
|
|
|
it('returns server when found', async () => {
|
|
|
|
|
const server = { id: '1', name: 'test' };
|
|
|
|
|
vi.mocked(repo.findById).mockResolvedValue(server as never);
|
|
|
|
|
const result = await service.getById('1');
|
|
|
|
|
expect(result.id).toBe('1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when not found', async () => {
|
|
|
|
|
await expect(service.getById('missing')).rejects.toThrow(NotFoundError);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('create', () => {
|
|
|
|
|
it('creates a server with valid input', async () => {
|
|
|
|
|
const result = await service.create({ name: 'my-server' });
|
|
|
|
|
expect(result.name).toBe('my-server');
|
|
|
|
|
expect(repo.create).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws ConflictError when name exists', async () => {
|
|
|
|
|
vi.mocked(repo.findByName).mockResolvedValue({ id: '1', name: 'existing' } as never);
|
|
|
|
|
await expect(service.create({ name: 'existing' })).rejects.toThrow(ConflictError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws on invalid input', async () => {
|
|
|
|
|
await expect(service.create({ name: '' })).rejects.toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('update', () => {
|
|
|
|
|
it('updates an existing server', async () => {
|
|
|
|
|
vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'test' } as never);
|
|
|
|
|
await service.update('1', { description: 'updated' });
|
|
|
|
|
expect(repo.update).toHaveBeenCalledWith('1', { description: 'updated' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when server does not exist', async () => {
|
|
|
|
|
await expect(service.update('missing', {})).rejects.toThrow(NotFoundError);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('delete', () => {
|
|
|
|
|
it('deletes an existing server', async () => {
|
|
|
|
|
vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'test' } as never);
|
|
|
|
|
await service.delete('1');
|
|
|
|
|
expect(repo.delete).toHaveBeenCalledWith('1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when server does not exist', async () => {
|
|
|
|
|
await expect(service.delete('missing')).rejects.toThrow(NotFoundError);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|