import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import type { PrismaClient } from '@prisma/client'; import { setupTestDb, cleanupTestDb, clearAllTables, getTestClient } from './helpers.js'; let prisma: PrismaClient; beforeAll(async () => { prisma = await setupTestDb(); }, 30_000); afterAll(async () => { await cleanupTestDb(); }); beforeEach(async () => { await clearAllTables(prisma); }); // ── Helper factories ── async function createUser(overrides: { email?: string; name?: string; role?: 'USER' | 'ADMIN' } = {}) { return prisma.user.create({ data: { email: overrides.email ?? `test-${Date.now()}@example.com`, name: overrides.name ?? 'Test User', role: overrides.role ?? 'USER', }, }); } async function createServer(overrides: { name?: string; transport?: 'STDIO' | 'SSE' | 'STREAMABLE_HTTP' } = {}) { return prisma.mcpServer.create({ data: { name: overrides.name ?? `server-${Date.now()}`, description: 'Test server', packageName: '@test/mcp-server', transport: overrides.transport ?? 'STDIO', }, }); } // ── User model ── describe('User', () => { it('creates a user with defaults', async () => { const user = await createUser(); expect(user.id).toBeDefined(); expect(user.role).toBe('USER'); expect(user.version).toBe(1); expect(user.createdAt).toBeInstanceOf(Date); expect(user.updatedAt).toBeInstanceOf(Date); }); it('enforces unique email', async () => { await createUser({ email: 'dup@test.com' }); await expect(createUser({ email: 'dup@test.com' })).rejects.toThrow(); }); it('allows ADMIN role', async () => { const admin = await createUser({ role: 'ADMIN' }); expect(admin.role).toBe('ADMIN'); }); it('updates updatedAt on change', async () => { const user = await createUser(); const original = user.updatedAt; // Small delay to ensure different timestamp await new Promise((r) => setTimeout(r, 50)); const updated = await prisma.user.update({ where: { id: user.id }, data: { name: 'Updated' }, }); expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(original.getTime()); }); }); // ── Session model ── describe('Session', () => { it('creates a session linked to user', async () => { const user = await createUser(); const session = await prisma.session.create({ data: { token: 'test-token-123', userId: user.id, expiresAt: new Date(Date.now() + 86400_000), }, }); expect(session.token).toBe('test-token-123'); expect(session.userId).toBe(user.id); }); it('enforces unique token', async () => { const user = await createUser(); const data = { token: 'unique-token', userId: user.id, expiresAt: new Date(Date.now() + 86400_000), }; await prisma.session.create({ data }); await expect(prisma.session.create({ data })).rejects.toThrow(); }); it('cascades delete when user is deleted', async () => { const user = await createUser(); await prisma.session.create({ data: { token: 'cascade-token', userId: user.id, expiresAt: new Date(Date.now() + 86400_000), }, }); await prisma.user.delete({ where: { id: user.id } }); const sessions = await prisma.session.findMany({ where: { userId: user.id } }); expect(sessions).toHaveLength(0); }); }); // ── McpServer model ── describe('McpServer', () => { it('creates a server with defaults', async () => { const server = await createServer(); expect(server.transport).toBe('STDIO'); expect(server.version).toBe(1); expect(server.envTemplate).toEqual([]); }); it('enforces unique name', async () => { await createServer({ name: 'slack' }); await expect(createServer({ name: 'slack' })).rejects.toThrow(); }); it('stores envTemplate as JSON', async () => { const server = await prisma.mcpServer.create({ data: { name: 'with-env', envTemplate: [ { name: 'API_KEY', description: 'Key', isSecret: true }, ], }, }); const envTemplate = server.envTemplate as Array<{ name: string }>; expect(envTemplate).toHaveLength(1); expect(envTemplate[0].name).toBe('API_KEY'); }); it('supports SSE transport', async () => { const server = await createServer({ transport: 'SSE' }); expect(server.transport).toBe('SSE'); }); }); // ── McpProfile model ── describe('McpProfile', () => { it('creates a profile linked to server', async () => { const server = await createServer(); const profile = await prisma.mcpProfile.create({ data: { name: 'readonly', serverId: server.id, permissions: ['read'], }, }); expect(profile.name).toBe('readonly'); expect(profile.serverId).toBe(server.id); }); it('enforces unique name per server', async () => { const server = await createServer(); const data = { name: 'default', serverId: server.id }; await prisma.mcpProfile.create({ data }); await expect(prisma.mcpProfile.create({ data })).rejects.toThrow(); }); it('allows same profile name on different servers', async () => { const server1 = await createServer({ name: 'server-1' }); const server2 = await createServer({ name: 'server-2' }); await prisma.mcpProfile.create({ data: { name: 'default', serverId: server1.id } }); const profile2 = await prisma.mcpProfile.create({ data: { name: 'default', serverId: server2.id } }); expect(profile2.name).toBe('default'); }); it('cascades delete when server is deleted', async () => { const server = await createServer(); await prisma.mcpProfile.create({ data: { name: 'test', serverId: server.id } }); await prisma.mcpServer.delete({ where: { id: server.id } }); const profiles = await prisma.mcpProfile.findMany({ where: { serverId: server.id } }); expect(profiles).toHaveLength(0); }); }); // ── Project model ── describe('Project', () => { it('creates a project with owner', async () => { const user = await createUser(); const project = await prisma.project.create({ data: { name: 'weekly-reports', ownerId: user.id }, }); expect(project.name).toBe('weekly-reports'); expect(project.ownerId).toBe(user.id); }); it('enforces unique project name', async () => { const user = await createUser(); await prisma.project.create({ data: { name: 'dup', ownerId: user.id } }); await expect( prisma.project.create({ data: { name: 'dup', ownerId: user.id } }), ).rejects.toThrow(); }); it('cascades delete when owner is deleted', async () => { const user = await createUser(); await prisma.project.create({ data: { name: 'orphan', ownerId: user.id } }); await prisma.user.delete({ where: { id: user.id } }); const projects = await prisma.project.findMany({ where: { ownerId: user.id } }); expect(projects).toHaveLength(0); }); }); // ── ProjectMcpProfile (join table) ── describe('ProjectMcpProfile', () => { it('links project to profile', async () => { const user = await createUser(); const server = await createServer(); const profile = await prisma.mcpProfile.create({ data: { name: 'default', serverId: server.id }, }); const project = await prisma.project.create({ data: { name: 'test-project', ownerId: user.id }, }); const link = await prisma.projectMcpProfile.create({ data: { projectId: project.id, profileId: profile.id }, }); expect(link.projectId).toBe(project.id); expect(link.profileId).toBe(profile.id); }); it('enforces unique project+profile combination', async () => { const user = await createUser(); const server = await createServer(); const profile = await prisma.mcpProfile.create({ data: { name: 'default', serverId: server.id }, }); const project = await prisma.project.create({ data: { name: 'test-project', ownerId: user.id }, }); const data = { projectId: project.id, profileId: profile.id }; await prisma.projectMcpProfile.create({ data }); await expect(prisma.projectMcpProfile.create({ data })).rejects.toThrow(); }); it('loads profiles through project include', async () => { const user = await createUser(); const server = await createServer(); const profile = await prisma.mcpProfile.create({ data: { name: 'slack-ro', serverId: server.id }, }); const project = await prisma.project.create({ data: { name: 'reports', ownerId: user.id }, }); await prisma.projectMcpProfile.create({ data: { projectId: project.id, profileId: profile.id }, }); const loaded = await prisma.project.findUnique({ where: { id: project.id }, include: { profiles: { include: { profile: true } } }, }); expect(loaded!.profiles).toHaveLength(1); expect(loaded!.profiles[0].profile.name).toBe('slack-ro'); }); }); // ── McpInstance model ── describe('McpInstance', () => { it('creates an instance linked to server', async () => { const server = await createServer(); const instance = await prisma.mcpInstance.create({ data: { serverId: server.id }, }); expect(instance.status).toBe('STOPPED'); expect(instance.serverId).toBe(server.id); }); it('tracks instance status transitions', async () => { const server = await createServer(); const instance = await prisma.mcpInstance.create({ data: { serverId: server.id, status: 'STARTING' }, }); const running = await prisma.mcpInstance.update({ where: { id: instance.id }, data: { status: 'RUNNING', containerId: 'abc123', port: 8080 }, }); expect(running.status).toBe('RUNNING'); expect(running.containerId).toBe('abc123'); expect(running.port).toBe(8080); }); it('cascades delete when server is deleted', async () => { const server = await createServer(); await prisma.mcpInstance.create({ data: { serverId: server.id } }); await prisma.mcpServer.delete({ where: { id: server.id } }); const instances = await prisma.mcpInstance.findMany({ where: { serverId: server.id } }); expect(instances).toHaveLength(0); }); }); // ── AuditLog model ── describe('AuditLog', () => { it('creates an audit log entry', async () => { const user = await createUser(); const log = await prisma.auditLog.create({ data: { userId: user.id, action: 'CREATE', resource: 'McpServer', resourceId: 'server-123', details: { name: 'slack' }, }, }); expect(log.action).toBe('CREATE'); expect(log.resource).toBe('McpServer'); expect(log.createdAt).toBeInstanceOf(Date); }); it('supports querying by action and resource', async () => { const user = await createUser(); await prisma.auditLog.createMany({ data: [ { userId: user.id, action: 'CREATE', resource: 'McpServer' }, { userId: user.id, action: 'UPDATE', resource: 'McpServer' }, { userId: user.id, action: 'CREATE', resource: 'Project' }, ], }); const creates = await prisma.auditLog.findMany({ where: { action: 'CREATE' }, }); expect(creates).toHaveLength(2); const serverLogs = await prisma.auditLog.findMany({ where: { resource: 'McpServer' }, }); expect(serverLogs).toHaveLength(2); }); it('cascades delete when user is deleted', async () => { const user = await createUser(); await prisma.auditLog.create({ data: { userId: user.id, action: 'TEST', resource: 'Test' }, }); await prisma.user.delete({ where: { id: user.id } }); const logs = await prisma.auditLog.findMany({ where: { userId: user.id } }); expect(logs).toHaveLength(0); }); });