import { describe, it, expect } from 'vitest'; import { listHandler, createHandler } from '@/app/api/v1/companies/route'; import { getHandler, patchHandler, deleteHandler } from '@/app/api/v1/companies/[id]/route'; import { autocompleteHandler } from '@/app/api/v1/companies/autocomplete/route'; import { db } from '@/lib/db'; import { companies } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import { makeMockCtx, makeMockRequest } from '../../helpers/route-tester'; import { makePort, makeCompany, makeFullPermissions } from '../../helpers/factories'; describe('POST /api/v1/companies', () => { it('creates a company and returns 201', async () => { const port = await makePort(); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const name = `Acme Corp ${Math.random().toString(36).slice(2, 8)}`; const req = makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: { name }, }); const res = await createHandler(req, ctx, {}); expect(res.status).toBe(201); const body = await res.json(); expect(body.data.name).toBe(name); expect(body.data.portId).toBe(port.id); expect(body.data.status).toBe('active'); }); it('returns 409 (ConflictError) on duplicate name case-insensitive in same port', async () => { const port = await makePort(); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const name = `DupeCo ${Math.random().toString(36).slice(2, 8)}`; const req1 = makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: { name }, }); const res1 = await createHandler(req1, ctx, {}); expect(res1.status).toBe(201); // Same name with different case — should still conflict. const req2 = makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: { name: name.toUpperCase() }, }); const res2 = await createHandler(req2, ctx, {}); expect(res2.status).toBe(409); }); it('returns 400 on missing name', async () => { const port = await makePort(); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: {}, }); const res = await createHandler(req, ctx, {}); expect(res.status).toBe(400); }); it('allows same name in different port', async () => { const portA = await makePort(); const portB = await makePort(); const name = `SharedName ${Math.random().toString(36).slice(2, 8)}`; const ctxA = makeMockCtx({ portId: portA.id, permissions: makeFullPermissions() }); const resA = await createHandler( makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: { name } }), ctxA, {}, ); expect(resA.status).toBe(201); const ctxB = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() }); const resB = await createHandler( makeMockRequest('POST', 'http://localhost/api/v1/companies', { body: { name } }), ctxB, {}, ); expect(resB.status).toBe(201); }); }); describe('GET /api/v1/companies', () => { it('returns tenant-scoped companies with pagination shape', async () => { const port = await makePort(); await makeCompany({ portId: port.id, overrides: { name: 'ListedCo' } }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest( 'GET', 'http://localhost/api/v1/companies?page=1&limit=20&order=desc', ); const res = await listHandler(req, ctx, {}); expect(res.status).toBe(200); const body = await res.json(); expect(body.data.some((c: { name: string }) => c.name === 'ListedCo')).toBe(true); expect(body.pagination.page).toBe(1); expect(body.pagination.pageSize).toBe(20); expect(typeof body.pagination.total).toBe('number'); expect(typeof body.pagination.totalPages).toBe('number'); expect(typeof body.pagination.hasNextPage).toBe('boolean'); expect(typeof body.pagination.hasPreviousPage).toBe('boolean'); }); it('filters by status', async () => { const port = await makePort(); await makeCompany({ portId: port.id, overrides: { name: 'ActiveCo', status: 'active' }, }); await makeCompany({ portId: port.id, overrides: { name: 'DissolvedCo', status: 'dissolved' }, }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest( 'GET', 'http://localhost/api/v1/companies?page=1&limit=20&order=desc&status=dissolved', ); const res = await listHandler(req, ctx, {}); expect(res.status).toBe(200); const body = await res.json(); expect(body.data.every((c: { status: string }) => c.status === 'dissolved')).toBe(true); expect(body.data.some((c: { name: string }) => c.name === 'DissolvedCo')).toBe(true); expect(body.data.some((c: { name: string }) => c.name === 'ActiveCo')).toBe(false); }); }); describe('GET /api/v1/companies/[id]', () => { it('returns company for valid id + port', async () => { const port = await makePort(); const company = await makeCompany({ portId: port.id, overrides: { name: 'DetailCo' }, }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest('GET', `http://localhost/api/v1/companies/${company.id}`); const res = await getHandler(req, ctx, { id: company.id }); expect(res.status).toBe(200); const body = await res.json(); expect(body.data.id).toBe(company.id); expect(body.data.name).toBe('DetailCo'); }); it('returns 404 for cross-tenant', async () => { const portA = await makePort(); const portB = await makePort(); const company = await makeCompany({ portId: portA.id }); const ctx = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() }); const req = makeMockRequest('GET', `http://localhost/api/v1/companies/${company.id}`); const res = await getHandler(req, ctx, { id: company.id }); expect(res.status).toBe(404); }); }); describe('PATCH /api/v1/companies/[id]', () => { it('updates allowed fields', async () => { const port = await makePort(); const company = await makeCompany({ portId: port.id, overrides: { name: 'BeforeCo' }, }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest('PATCH', `http://localhost/api/v1/companies/${company.id}`, { body: { name: 'AfterCo', notes: 'updated notes' }, }); const res = await patchHandler(req, ctx, { id: company.id }); expect(res.status).toBe(200); const body = await res.json(); expect(body.data.name).toBe('AfterCo'); expect(body.data.notes).toBe('updated notes'); }); it('returns 404 for cross-tenant', async () => { const portA = await makePort(); const portB = await makePort(); const company = await makeCompany({ portId: portA.id }); const ctx = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() }); const req = makeMockRequest('PATCH', `http://localhost/api/v1/companies/${company.id}`, { body: { name: 'hijack' }, }); const res = await patchHandler(req, ctx, { id: company.id }); expect(res.status).toBe(404); }); }); describe('DELETE /api/v1/companies/[id]', () => { it('archives the company (204)', async () => { const port = await makePort(); const company = await makeCompany({ portId: port.id }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest('DELETE', `http://localhost/api/v1/companies/${company.id}`); const res = await deleteHandler(req, ctx, { id: company.id }); expect(res.status).toBe(204); const [row] = await db.select().from(companies).where(eq(companies.id, company.id)); expect(row?.archivedAt).not.toBeNull(); }); }); describe('GET /api/v1/companies/autocomplete', () => { it('returns matching companies by name', async () => { const port = await makePort(); await makeCompany({ portId: port.id, overrides: { name: 'AutoCompleteCoMatch One' }, }); await makeCompany({ portId: port.id, overrides: { name: 'OtherCompanyName' }, }); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest( 'GET', 'http://localhost/api/v1/companies/autocomplete?q=AutoCompleteCoMatch', ); const res = await autocompleteHandler(req, ctx, {}); expect(res.status).toBe(200); const body = await res.json(); expect(body.data.length).toBeGreaterThanOrEqual(1); expect(body.data.every((c: { name: string }) => c.name.includes('AutoCompleteCoMatch'))).toBe( true, ); }); it('returns empty array when q missing', async () => { const port = await makePort(); const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() }); const req = makeMockRequest('GET', 'http://localhost/api/v1/companies/autocomplete'); const res = await autocompleteHandler(req, ctx, {}); expect(res.status).toBe(200); const body = await res.json(); expect(body.data).toEqual([]); }); });