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.env).toEqual([]); }); it('enforces unique name', async () => { await createServer({ name: 'slack' }); await expect(createServer({ name: 'slack' })).rejects.toThrow(); }); it('stores env as JSON', async () => { const server = await prisma.mcpServer.create({ data: { name: 'with-env', env: [ { name: 'API_KEY', value: 'test-key' }, ], }, }); const env = server.env as Array<{ name: string }>; expect(env).toHaveLength(1); expect(env[0].name).toBe('API_KEY'); }); it('supports SSE transport', async () => { const server = await createServer({ transport: 'SSE' }); expect(server.transport).toBe('SSE'); }); }); // ── Secret model ── describe('Secret', () => { it('creates a secret with defaults', async () => { const secret = await prisma.secret.create({ data: { name: 'my-secret' }, }); expect(secret.name).toBe('my-secret'); expect(secret.data).toEqual({}); expect(secret.version).toBe(1); }); it('stores key-value data as JSON', async () => { const secret = await prisma.secret.create({ data: { name: 'api-keys', data: { API_KEY: 'test-key', API_SECRET: 'test-secret' }, }, }); const data = secret.data as Record; expect(data['API_KEY']).toBe('test-key'); expect(data['API_SECRET']).toBe('test-secret'); }); it('enforces unique name', async () => { await prisma.secret.create({ data: { name: 'dup-secret' } }); await expect(prisma.secret.create({ data: { name: 'dup-secret' } })).rejects.toThrow(); }); it('updates data', async () => { const secret = await prisma.secret.create({ data: { name: 'updatable', data: { KEY: 'old' } }, }); const updated = await prisma.secret.update({ where: { id: secret.id }, data: { data: { KEY: 'new', EXTRA: 'added' } }, }); const data = updated.data as Record; expect(data['KEY']).toBe('new'); expect(data['EXTRA']).toBe('added'); }); }); // ── 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); }); }); // ── 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); }); });