/** * @vitest-environment node */ import { hybridAuthMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TableDefinition } from '@/lib/table' const { mockCheckAccess, mockMarkTableJobRunning, mockRunTableExport } = vi.hoisted(() => ({ mockCheckAccess: vi.fn(), mockMarkTableJobRunning: vi.fn(), mockRunTableExport: vi.fn(), })) vi.mock('@sim/utils/id', () => ({ generateId: vi.fn().mockReturnValue('job-id-xyz'), generateShortId: vi.fn().mockReturnValue('short-id'), })) vi.mock('@/lib/table/jobs/service', () => ({ markTableJobRunning: mockMarkTableJobRunning })) vi.mock('@/lib/table/export-runner', () => ({ runTableExport: mockRunTableExport })) vi.mock('@/lib/core/utils/background', () => ({ runDetached: (_label: string, work: () => Promise) => { void work() }, })) vi.mock('@/app/api/table/utils', async () => { const { NextResponse } = await import('next/server') return { checkAccess: mockCheckAccess, accessError: (result: { status: number }) => NextResponse.json({ error: 'denied' }, { status: result.status }), } }) import { POST } from '@/app/api/table/[tableId]/export-async/route' function buildTable(overrides: Partial = {}): TableDefinition { return { id: 'tbl_1', name: 'People', description: null, schema: { columns: [{ name: 'name', type: 'string' }] }, metadata: null, rowCount: 50000, maxRows: 1_000_000, workspaceId: 'workspace-1', createdBy: 'user-1', archivedAt: null, createdAt: new Date(), updatedAt: new Date(), ...overrides, } } function makeRequest(body: unknown, tableId = 'tbl_1') { const req = new NextRequest(`http://localhost:3000/api/table/${tableId}/export-async`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }) return POST(req, { params: Promise.resolve({ tableId }) }) } const validBody = { workspaceId: 'workspace-1', format: 'csv' } describe('POST /api/table/[tableId]/export-async', () => { beforeEach(() => { vi.clearAllMocks() hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ success: true, userId: 'user-1', authType: 'session', }) mockCheckAccess.mockResolvedValue({ ok: true, table: buildTable() }) mockMarkTableJobRunning.mockResolvedValue(true) mockRunTableExport.mockResolvedValue(undefined) }) it('claims an export job and kicks off the worker', async () => { const response = await makeRequest(validBody) const data = await response.json() expect(response.status).toBe(200) expect(data.data).toEqual({ tableId: 'tbl_1', jobId: 'job-id-xyz' }) expect(mockMarkTableJobRunning).toHaveBeenCalledWith('tbl_1', 'job-id-xyz', 'export', { format: 'csv', }) expect(mockRunTableExport).toHaveBeenCalledWith({ jobId: 'job-id-xyz', tableId: 'tbl_1', workspaceId: 'workspace-1', format: 'csv', }) }) it('defaults the format to csv', async () => { const response = await makeRequest({ workspaceId: 'workspace-1' }) expect(response.status).toBe(200) expect(mockRunTableExport).toHaveBeenCalledWith(expect.objectContaining({ format: 'csv' })) }) it('returns 409 when the claim fails', async () => { mockMarkTableJobRunning.mockResolvedValue(false) const response = await makeRequest(validBody) expect(response.status).toBe(409) expect(mockRunTableExport).not.toHaveBeenCalled() }) it('returns 401 when unauthenticated', async () => { hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ success: false }) const response = await makeRequest(validBody) expect(response.status).toBe(401) expect(mockMarkTableJobRunning).not.toHaveBeenCalled() }) it('returns the access error status when access is denied', async () => { mockCheckAccess.mockResolvedValue({ ok: false, status: 403 }) const response = await makeRequest(validBody) expect(response.status).toBe(403) expect(mockRunTableExport).not.toHaveBeenCalled() }) it('returns 400 on workspace mismatch', async () => { const response = await makeRequest({ ...validBody, workspaceId: 'other-ws' }) expect(response.status).toBe(400) expect(mockMarkTableJobRunning).not.toHaveBeenCalled() }) })