Files
accounted/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts
T
Jakob Wennberg 3f255ea201 refactor: consolidate AI analyzers into shared lib/ai module and add journal entry reversal columns
Extract shared vision/document analysis logic into lib/ai (vision-client, document-analyzer,
image preprocessing, validation helpers), simplifying invoice-analyzer, receipt-analyzer, and
document classifier. Add migration 046 for journal entry reversal/correction link columns
(reversed_by_id, reverses_id, correction_of_id) required by storno service. Update extension
components and shared UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:14:44 +01:00

159 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createQueuedMockSupabase, parseJsonResponse } from '@/tests/helpers'
// Mock server-only
vi.mock('server-only', () => ({}))
// Hoisted mocks to avoid reference-before-initialization
const { mockVerify, mockServiceClientFn } = vi.hoisted(() => {
return {
mockVerify: vi.fn(),
mockServiceClientFn: vi.fn(),
}
})
// Mock svix
vi.mock('svix', () => ({
Webhook: class MockWebhook {
verify = mockVerify
},
}))
// Mock Supabase SSR
vi.mock('@supabase/ssr', () => ({
createServerClient: (...args: unknown[]) => mockServiceClientFn(...args),
}))
// Mock email handler
vi.mock('@/extensions/general/invoice-inbox/lib/email-handler', () => ({
parseInboundPayload: vi.fn(),
extractAttachments: vi.fn(),
resolveUserFromEmail: vi.fn(),
}))
// Mock unified document analyzer
vi.mock('@/lib/ai/document-analyzer', () => ({
analyzeDocument: vi.fn(),
}))
// Mock supplier matcher
vi.mock('@/extensions/general/invoice-inbox/lib/supplier-matcher', () => ({
matchSupplier: vi.fn(),
}))
// Mock receipt pipeline
vi.mock('@/extensions/general/receipt-ocr/lib/receipt-pipeline', () => ({
processReceiptFromDocument: vi.fn(),
}))
import { POST } from '../route'
import { parseInboundPayload, extractAttachments, resolveUserFromEmail } from '@/extensions/general/invoice-inbox/lib/email-handler'
const mockParseInboundPayload = vi.mocked(parseInboundPayload)
const mockExtractAttachments = vi.mocked(extractAttachments)
const mockResolveUserFromEmail = vi.mocked(resolveUserFromEmail)
describe('Invoice Inbox Webhook Route', () => {
beforeEach(() => {
vi.clearAllMocks()
process.env.RESEND_WEBHOOK_SECRET = 'test-secret'
process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'
process.env.SUPABASE_SERVICE_ROLE_KEY = 'test-key'
})
function makeWebhookRequest(body: unknown = {}) {
const bodyStr = JSON.stringify(body)
return new Request('http://localhost:3000/api/extensions/invoice-inbox/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'svix-id': 'msg_test123',
'svix-timestamp': String(Math.floor(Date.now() / 1000)),
'svix-signature': 'v1,test-signature',
},
body: bodyStr,
})
}
it('returns 400 when webhook headers are missing', async () => {
const request = new Request('http://localhost:3000/api/extensions/invoice-inbox/webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
})
const response = await POST(request)
const { status } = await parseJsonResponse(response)
expect(status).toBe(400)
})
it('returns 401 when signature verification fails', async () => {
mockVerify.mockImplementation(() => {
throw new Error('Invalid signature')
})
const response = await POST(makeWebhookRequest({ from: 'test@test.com', to: 'inbox@co.com' }))
const { status } = await parseJsonResponse(response)
expect(status).toBe(401)
})
it('returns 400 when payload is invalid', async () => {
mockVerify.mockReturnValue(undefined)
mockParseInboundPayload.mockReturnValue(null)
const response = await POST(makeWebhookRequest({}))
const { status } = await parseJsonResponse(response)
expect(status).toBe(400)
})
it('returns 404 when user not found for email', async () => {
const { supabase } = createQueuedMockSupabase()
mockServiceClientFn.mockReturnValue(supabase)
mockVerify.mockReturnValue(undefined)
mockParseInboundPayload.mockReturnValue({
from: 'supplier@test.com',
to: 'unknown@inbox.com',
subject: 'Invoice',
html: null,
text: null,
attachments: [],
created_at: '2024-06-15T10:00:00Z',
})
mockResolveUserFromEmail.mockResolvedValue(null)
const response = await POST(makeWebhookRequest({ from: 'supplier@test.com', to: 'unknown@inbox.com' }))
const { status } = await parseJsonResponse(response)
expect(status).toBe(404)
})
it('returns success with 0 processed when no attachments', async () => {
const { supabase, enqueueMany } = createQueuedMockSupabase()
mockServiceClientFn.mockReturnValue(supabase)
mockVerify.mockReturnValue(undefined)
mockParseInboundPayload.mockReturnValue({
from: 'supplier@test.com',
to: 'inbox@myco.com',
subject: 'No attachments',
html: null,
text: null,
attachments: [],
created_at: '2024-06-15T10:00:00Z',
})
mockExtractAttachments.mockReturnValue([])
mockResolveUserFromEmail.mockResolvedValue('user-1')
// Insert inbox item with error status
enqueueMany([
{ data: { id: 'item-1' }, error: null },
])
const response = await POST(makeWebhookRequest({ from: 'supplier@test.com', to: 'inbox@myco.com' }))
const { status, body } = await parseJsonResponse<{ data: { processed: number } }>(response)
expect(status).toBe(200)
expect(body.data.processed).toBe(0)
})
})