test: add API route tests for 10 critical endpoints
Add test coverage for invoices, transactions, bookkeeping, and supplier invoice API routes. Includes test helpers (createMockRequest, parseJsonResponse, createMockRouteParams, createQueuedMockSupabase) and fixture factories (makeInvoice, makeCustomer, makeSupplier, makeSupplierInvoice, makeCompanySettings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
createMockRequest,
|
||||
parseJsonResponse,
|
||||
createMockRouteParams,
|
||||
makeJournalEntry,
|
||||
} from '@/tests/helpers'
|
||||
|
||||
// Mock dependencies before imports
|
||||
const mockCreateClient = vi.fn()
|
||||
vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: () => mockCreateClient(),
|
||||
}))
|
||||
|
||||
const mockReverseEntry = vi.fn()
|
||||
vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
reverseEntry: (...args: unknown[]) => mockReverseEntry(...args),
|
||||
}))
|
||||
|
||||
import { POST } from '../route'
|
||||
|
||||
describe('POST /api/bookkeeping/journal-entries/[id]/reverse', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCreateClient.mockResolvedValue({
|
||||
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }) },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
mockCreateClient.mockResolvedValue({
|
||||
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
|
||||
})
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries/entry-1/reverse', {
|
||||
method: 'POST',
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'entry-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(401)
|
||||
expect(body).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('returns reversed entry on success', async () => {
|
||||
const reversalEntry = makeJournalEntry({
|
||||
id: 'reversal-1',
|
||||
reverses_id: 'entry-1',
|
||||
source_type: 'storno',
|
||||
})
|
||||
mockReverseEntry.mockResolvedValue(reversalEntry)
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries/entry-1/reverse', {
|
||||
method: 'POST',
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'entry-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ data: unknown }>(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data).toEqual(reversalEntry)
|
||||
expect(mockReverseEntry).toHaveBeenCalledWith('user-1', 'entry-1')
|
||||
})
|
||||
|
||||
it('returns 400 when engine throws', async () => {
|
||||
mockReverseEntry.mockRejectedValue(new Error('Entry already reversed'))
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries/entry-1/reverse', {
|
||||
method: 'POST',
|
||||
})
|
||||
const response = await POST(request, createMockRouteParams({ id: 'entry-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('Entry already reversed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
createMockRequest,
|
||||
parseJsonResponse,
|
||||
createQueuedMockSupabase,
|
||||
makeJournalEntry,
|
||||
} from '@/tests/helpers'
|
||||
|
||||
// Mock dependencies
|
||||
const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase()
|
||||
vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: () => Promise.resolve(mockSupabase),
|
||||
}))
|
||||
|
||||
const mockCreateJournalEntry = vi.fn()
|
||||
vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
|
||||
}))
|
||||
|
||||
import { GET, POST } from '../route'
|
||||
|
||||
describe('GET /api/bookkeeping/journal-entries', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
reset()
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries')
|
||||
const response = await GET(request)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(401)
|
||||
expect(body).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('returns entries list', async () => {
|
||||
const entries = [makeJournalEntry(), makeJournalEntry()]
|
||||
enqueue({ data: entries, error: null, count: 2 })
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries')
|
||||
const response = await GET(request)
|
||||
const { status, body } = await parseJsonResponse<{ data: unknown[]; count: number }>(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data).toEqual(entries)
|
||||
expect(body.count).toBe(2)
|
||||
})
|
||||
|
||||
it('passes filters to query', async () => {
|
||||
enqueue({ data: [], error: null, count: 0 })
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries', {
|
||||
searchParams: {
|
||||
period_id: 'period-1',
|
||||
status: 'posted',
|
||||
date_from: '2024-01-01',
|
||||
date_to: '2024-12-31',
|
||||
limit: '10',
|
||||
offset: '5',
|
||||
},
|
||||
})
|
||||
const response = await GET(request)
|
||||
const { status } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(mockSupabase.from).toHaveBeenCalledWith('journal_entries')
|
||||
})
|
||||
|
||||
it('returns 500 on database error', async () => {
|
||||
enqueue({ data: null, error: { message: 'DB error' } })
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries')
|
||||
const response = await GET(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(500)
|
||||
expect(body.error).toBe('DB error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/bookkeeping/journal-entries', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
reset()
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries', {
|
||||
method: 'POST',
|
||||
body: {},
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(401)
|
||||
expect(body).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('creates journal entry and returns it', async () => {
|
||||
const entry = makeJournalEntry()
|
||||
mockCreateJournalEntry.mockResolvedValue(entry)
|
||||
|
||||
const input = {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2024-06-15',
|
||||
description: 'Test entry',
|
||||
source_type: 'manual',
|
||||
lines: [
|
||||
{ account_number: '1930', debit_amount: 1000, credit_amount: 0 },
|
||||
{ account_number: '3001', debit_amount: 0, credit_amount: 1000 },
|
||||
],
|
||||
}
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries', {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ data: unknown }>(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data).toEqual(entry)
|
||||
expect(mockCreateJournalEntry).toHaveBeenCalledWith('user-1', input)
|
||||
})
|
||||
|
||||
it('returns 400 when engine throws', async () => {
|
||||
mockCreateJournalEntry.mockRejectedValue(new Error('Unbalanced entry'))
|
||||
|
||||
const request = createMockRequest('/api/bookkeeping/journal-entries', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
fiscal_period_id: 'period-1',
|
||||
entry_date: '2024-06-15',
|
||||
description: 'Bad entry',
|
||||
source_type: 'manual',
|
||||
lines: [{ account_number: '1930', debit_amount: 1000, credit_amount: 0 }],
|
||||
},
|
||||
})
|
||||
const response = await POST(request)
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(400)
|
||||
expect(body.error).toBe('Unbalanced entry')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user