import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { invoiceInboxExtension } from '@/extensions/general/invoice-inbox' import { ResendSignatureError } from '@/extensions/general/invoice-inbox/lib/resend-inbound' import { createQueuedMockSupabase, createMockRequest } from '@/tests/helpers' vi.mock('@/extensions/general/invoice-inbox/lib/resend-inbound', async () => { const actual = await vi.importActual( '@/extensions/general/invoice-inbox/lib/resend-inbound' ) return { ...actual, verifyInboundWebhook: vi.fn(), fetchReceivingEmail: vi.fn(), fetchInboundAttachment: vi.fn(), } }) vi.mock('@supabase/supabase-js', () => ({ createClient: vi.fn(), })) // Rate limiter is a thin RPC wrapper; bypass it so the queued-mock sequence // in each test doesn't have to account for the extra Supabase call. vi.mock('@/lib/rate-limits/inbox', () => ({ checkInboxUploadRateLimit: vi.fn().mockResolvedValue({ ok: true }), })) import { verifyInboundWebhook, fetchReceivingEmail, fetchInboundAttachment } from '@/extensions/general/invoice-inbox/lib/resend-inbound' import { createClient } from '@supabase/supabase-js' function findRoute(method: string, path: string) { return invoiceInboxExtension.apiRoutes!.find((r) => r.method === method && r.path === path)! } const webhookRoute = findRoute('POST', '/inbound') function mockReceivedEvent(overrides?: Record) { return { type: 'email.received' as const, created_at: '2026-04-20T10:00:00Z', data: { email_id: 'em_123', created_at: '2026-04-20T10:00:00Z', from: 'billing@supplier.com', to: ['acme-ab-x7f2@arcim.io'], cc: [], bcc: [], subject: 'Invoice #5678', message_id: '', attachments: [ { id: 'att_1', filename: 'invoice.pdf', size: 12345, content_type: 'application/pdf', content_id: 'cid1', content_disposition: 'attachment', }, ], ...overrides, }, } } describe('POST /inbound', () => { const originalEnv = { ...process.env } beforeEach(() => { vi.clearAllMocks() process.env.RESEND_INBOUND_DOMAIN = 'arcim.io' process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost' process.env.SUPABASE_SERVICE_ROLE_KEY = 'test-service-key' }) afterEach(() => { process.env = { ...originalEnv } }) it('returns 503 when RESEND_INBOUND_DOMAIN is not set', async () => { delete process.env.RESEND_INBOUND_DOMAIN const request = createMockRequest('/inbound', { method: 'POST', body: { type: 'email.received' } }) const res = await webhookRoute.handler(request) expect(res.status).toBe(503) }) it('returns 401 when signature verification fails', async () => { vi.mocked(verifyInboundWebhook).mockImplementation(() => { throw new ResendSignatureError('bad sig') }) const request = createMockRequest('/inbound', { method: 'POST', body: { type: 'email.received' } }) const res = await webhookRoute.handler(request) expect(res.status).toBe(401) }) it('ignores non-received events with 200', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue({ type: 'email.sent', created_at: '', data: {}, } as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) expect(res.status).toBe(200) }) it('returns 404 when no recipient matches our domain', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue( mockReceivedEvent({ to: ['random@contoso.com'] }) as never ) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) expect(res.status).toBe(404) }) it('returns 404 when the address is not in company_inboxes', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue(mockReceivedEvent() as never) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: null }) // company_inboxes lookup returns nothing vi.mocked(createClient).mockReturnValue(supabase as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) expect(res.status).toBe(404) }) it('returns 410 when the address is deprecated', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue(mockReceivedEvent() as never) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'inbox-1', company_id: 'company-1', status: 'deprecated' } }) vi.mocked(createClient).mockReturnValue(supabase as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) expect(res.status).toBe(410) }) it('skips already-processed attachments (per-attachment idempotency)', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue(mockReceivedEvent() as never) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'inbox-1', company_id: 'company-1', status: 'active' } }) // inbox lookup enqueue({ data: { created_by: 'user-owner-1' } }) // company owner enqueue({ data: { id: 'existing-item-1' } }) // per-attachment dup check finds existing row vi.mocked(createClient).mockReturnValue(supabase as never) vi.mocked(fetchReceivingEmail).mockResolvedValue({ object: 'email', id: 'em_123', to: ['acme-ab-x7f2@arcim.io'], from: 'billing@supplier.com', created_at: '2026-04-20T10:00:00Z', subject: 'Invoice #5678', bcc: null, cc: null, reply_to: null, html: null, text: 'Body', headers: {}, message_id: '', raw: null, attachments: [ { id: 'att_1', filename: 'invoice.pdf', size: 100, content_type: 'application/pdf', content_id: 'cid', content_disposition: 'attachment' }, ], } as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) const body = await res.json() expect(res.status).toBe(200) expect(body.data.results[0].duplicate).toBe(true) expect(body.data.results[0].inbox_item_id).toBe('existing-item-1') expect(fetchInboundAttachment).not.toHaveBeenCalled() }) it('returns 500 when the company has no created_by owner', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue(mockReceivedEvent() as never) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'inbox-1', company_id: 'company-1', status: 'active' } }) enqueue({ data: { created_by: null } }) // company with no owner vi.mocked(createClient).mockReturnValue(supabase as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) expect(res.status).toBe(500) }) it('logs an error inbox item when email has no attachments', async () => { vi.mocked(verifyInboundWebhook).mockReturnValue( mockReceivedEvent({ attachments: [] }) as never ) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'inbox-1', company_id: 'company-1', status: 'active' } }) enqueue({ data: { created_by: 'user-owner-1' } }) enqueue({ data: null }) // insert succeeds vi.mocked(createClient).mockReturnValue(supabase as never) vi.mocked(fetchReceivingEmail).mockResolvedValue({ object: 'email', id: 'em_123', to: ['acme-ab-x7f2@arcim.io'], from: 'billing@supplier.com', created_at: '2026-04-20T10:00:00Z', subject: 'No attachments here', bcc: null, cc: null, reply_to: null, html: null, text: 'Body only', headers: {}, message_id: '', raw: null, attachments: [], } as never) const request = createMockRequest('/inbound', { method: 'POST', body: {} }) const res = await webhookRoute.handler(request) const body = await res.json() expect(res.status).toBe(200) expect(body.data.reason).toBe('no_attachments') expect(fetchInboundAttachment).not.toHaveBeenCalled() }) })