/** * Tests for registerProject CONFLICT error disambiguation. * Verifies that UNIQUE constraint failures on specific columns produce * column-specific error messages. */ import { describe, it, expect, vi } from 'vitest'; import { TRPCError } from '@trpc/server'; import { router, publicProcedure, createCallerFactory } from '../trpc.js'; import { projectProcedures } from './project.js'; import type { TRPCContext } from '../context.js'; import type { ProjectRepository } from '../../db/repositories/project-repository.js'; const testRouter = router({ ...projectProcedures(publicProcedure), }); const createCaller = createCallerFactory(testRouter); function makeCtx(mockCreate: () => Promise): TRPCContext { const projectRepository: ProjectRepository = { create: mockCreate as unknown as ProjectRepository['create'], findById: vi.fn().mockResolvedValue(null), findByName: vi.fn().mockResolvedValue(null), findAll: vi.fn().mockResolvedValue([]), update: vi.fn(), delete: vi.fn(), addProjectToInitiative: vi.fn(), removeProjectFromInitiative: vi.fn(), findProjectsByInitiativeId: vi.fn().mockResolvedValue([]), setInitiativeProjects: vi.fn(), }; return { eventBus: {} as TRPCContext['eventBus'], serverStartedAt: null, processCount: 0, projectRepository, // No workspaceRoot — prevents cloneProject from running }; } const INPUT = { name: 'my-project', url: 'https://github.com/example/repo' }; describe('registerProject — CONFLICT error disambiguation', () => { it('throws CONFLICT with name-specific message on projects.name UNIQUE violation', async () => { const caller = createCaller(makeCtx(() => { throw new Error('UNIQUE constraint failed: projects.name'); })); const err = await caller.registerProject(INPUT).catch(e => e); expect(err).toBeInstanceOf(TRPCError); expect(err.code).toBe('CONFLICT'); expect(err.message).toBe('A project with this name already exists'); }); it('throws CONFLICT with url-specific message on projects.url UNIQUE violation', async () => { const caller = createCaller(makeCtx(() => { throw new Error('UNIQUE constraint failed: projects.url'); })); const err = await caller.registerProject(INPUT).catch(e => e); expect(err).toBeInstanceOf(TRPCError); expect(err.code).toBe('CONFLICT'); expect(err.message).toBe('A project with this URL already exists'); }); it('throws CONFLICT with fallback message on unknown UNIQUE constraint violation', async () => { const caller = createCaller(makeCtx(() => { throw new Error('UNIQUE constraint failed: projects.unknown_col'); })); const err = await caller.registerProject(INPUT).catch(e => e); expect(err).toBeInstanceOf(TRPCError); expect(err.code).toBe('CONFLICT'); expect(err.message).toBe('A project with this name or URL already exists'); }); it('rethrows non-UNIQUE errors without wrapping in a CONFLICT', async () => { const originalError = new Error('SQLITE_BUSY'); const caller = createCaller(makeCtx(() => { throw originalError; })); const err = await caller.registerProject(INPUT).catch(e => e); expect(err).toBeDefined(); // Must not be surfaced as a CONFLICT — the catch block should re-throw as-is expect(err).not.toMatchObject({ code: 'CONFLICT' }); // The original error message must be preserved somewhere expect(err.message).toContain('SQLITE_BUSY'); }); });