import { describe, it, expect, vi, beforeEach } from 'vitest'; import { McpProfileService } from '../src/services/mcp-profile.service.js'; import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js'; import type { IMcpProfileRepository, IMcpServerRepository } from '../src/repositories/interfaces.js'; function mockProfileRepo(): IMcpProfileRepository { return { findAll: vi.fn(async () => []), findById: vi.fn(async () => null), findByServerAndName: vi.fn(async () => null), create: vi.fn(async (data) => ({ id: 'new-id', name: data.name, serverId: data.serverId, permissions: data.permissions ?? [], envOverrides: data.envOverrides ?? {}, version: 1, createdAt: new Date(), updatedAt: new Date(), })), update: vi.fn(async (id, data) => ({ id, name: data.name ?? 'test', serverId: 'srv-1', permissions: data.permissions ?? [], envOverrides: data.envOverrides ?? {}, version: 2, createdAt: new Date(), updatedAt: new Date(), })), delete: vi.fn(async () => {}), }; } function mockServerRepo(): IMcpServerRepository { return { findAll: vi.fn(async () => []), findById: vi.fn(async () => null), findByName: vi.fn(async () => null), create: vi.fn(async () => ({} as never)), update: vi.fn(async () => ({} as never)), delete: vi.fn(async () => {}), }; } describe('McpProfileService', () => { let profileRepo: ReturnType; let serverRepo: ReturnType; let service: McpProfileService; beforeEach(() => { profileRepo = mockProfileRepo(); serverRepo = mockServerRepo(); service = new McpProfileService(profileRepo, serverRepo); }); describe('list', () => { it('returns all profiles', async () => { await service.list(); expect(profileRepo.findAll).toHaveBeenCalledWith(undefined); }); it('filters by serverId', async () => { await service.list('srv-1'); expect(profileRepo.findAll).toHaveBeenCalledWith('srv-1'); }); }); describe('getById', () => { it('returns profile when found', async () => { vi.mocked(profileRepo.findById).mockResolvedValue({ id: '1', name: 'test' } 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 profile when server exists', async () => { vi.mocked(serverRepo.findById).mockResolvedValue({ id: 'srv-1', name: 'test' } as never); const result = await service.create({ name: 'readonly', serverId: 'srv-1' }); expect(result.name).toBe('readonly'); }); it('throws NotFoundError when server does not exist', async () => { await expect(service.create({ name: 'test', serverId: 'missing' })).rejects.toThrow(NotFoundError); }); it('throws ConflictError when profile name exists for server', async () => { vi.mocked(serverRepo.findById).mockResolvedValue({ id: 'srv-1', name: 'test' } as never); vi.mocked(profileRepo.findByServerAndName).mockResolvedValue({ id: '1' } as never); await expect(service.create({ name: 'dup', serverId: 'srv-1' })).rejects.toThrow(ConflictError); }); }); describe('update', () => { it('updates an existing profile', async () => { vi.mocked(profileRepo.findById).mockResolvedValue({ id: '1', name: 'old', serverId: 'srv-1' } as never); await service.update('1', { permissions: ['read'] }); expect(profileRepo.update).toHaveBeenCalled(); }); it('checks uniqueness when renaming', async () => { vi.mocked(profileRepo.findById).mockResolvedValue({ id: '1', name: 'old', serverId: 'srv-1' } as never); vi.mocked(profileRepo.findByServerAndName).mockResolvedValue({ id: '2' } as never); await expect(service.update('1', { name: 'taken' })).rejects.toThrow(ConflictError); }); it('throws NotFoundError when profile does not exist', async () => { await expect(service.update('missing', {})).rejects.toThrow(NotFoundError); }); }); describe('delete', () => { it('deletes an existing profile', async () => { vi.mocked(profileRepo.findById).mockResolvedValue({ id: '1' } as never); await service.delete('1'); expect(profileRepo.delete).toHaveBeenCalledWith('1'); }); it('throws NotFoundError when profile does not exist', async () => { await expect(service.delete('missing')).rejects.toThrow(NotFoundError); }); }); });