Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
139 lines
4.2 KiB
TypeScript
139 lines
4.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { createMockRequest } from '@/tests/helpers'
|
|
|
|
vi.mock('@/lib/supabase/server', () => ({
|
|
createClient: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/core/audit/audit-service', () => ({
|
|
getAuditLog: vi.fn(),
|
|
}))
|
|
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { getAuditLog } from '@/lib/core/audit/audit-service'
|
|
import { GET } from '../route'
|
|
|
|
const mockCreateClient = vi.mocked(createClient)
|
|
const mockGetAuditLog = vi.mocked(getAuditLog)
|
|
|
|
function mockAuth(userId: string | null) {
|
|
mockCreateClient.mockResolvedValue({
|
|
auth: {
|
|
getUser: vi.fn().mockResolvedValue({
|
|
data: { user: userId ? { id: userId } : null },
|
|
}),
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any)
|
|
}
|
|
|
|
const sampleEntries = [
|
|
{
|
|
id: '1',
|
|
user_id: 'user-1',
|
|
action: 'INSERT' as const,
|
|
table_name: 'journal_entries',
|
|
record_id: 'rec-1',
|
|
actor_id: null,
|
|
old_state: null,
|
|
new_state: { description: 'Test entry' },
|
|
description: 'Created journal entry',
|
|
created_at: '2024-06-15T10:00:00Z',
|
|
},
|
|
{
|
|
id: '2',
|
|
user_id: 'user-1',
|
|
action: 'COMMIT' as const,
|
|
table_name: 'journal_entries',
|
|
record_id: 'rec-1',
|
|
actor_id: null,
|
|
old_state: { status: 'draft' },
|
|
new_state: { status: 'posted' },
|
|
description: 'Committed journal entry',
|
|
created_at: '2024-06-15T10:01:00Z',
|
|
},
|
|
]
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('GET /api/reports/audit-trail', () => {
|
|
it('returns 401 when not authenticated', async () => {
|
|
mockAuth(null)
|
|
const req = createMockRequest('/api/reports/audit-trail')
|
|
const res = await GET(req)
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns CSV format with correct headers', async () => {
|
|
mockAuth('user-1')
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockGetAuditLog.mockResolvedValue({ data: sampleEntries as any, count: 2 })
|
|
|
|
const req = createMockRequest('/api/reports/audit-trail', {
|
|
searchParams: { format: 'csv' },
|
|
})
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('Content-Type')).toBe('text/csv; charset=utf-8')
|
|
expect(res.headers.get('Content-Disposition')).toBe('attachment; filename="audit-trail.csv"')
|
|
|
|
const text = await res.text()
|
|
const lines = text.split('\n')
|
|
expect(lines[0]).toBe('timestamp,action,table_name,record_id,description,old_state,new_state')
|
|
expect(lines).toHaveLength(3) // header + 2 entries
|
|
expect(lines[1]).toContain('INSERT')
|
|
expect(lines[1]).toContain('journal_entries')
|
|
})
|
|
|
|
it('returns JSON format as downloadable file', async () => {
|
|
mockAuth('user-1')
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockGetAuditLog.mockResolvedValue({ data: sampleEntries as any, count: 2 })
|
|
|
|
const req = createMockRequest('/api/reports/audit-trail', {
|
|
searchParams: { format: 'json' },
|
|
})
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('Content-Type')).toBe('application/json')
|
|
expect(res.headers.get('Content-Disposition')).toBe('attachment; filename="audit-trail.json"')
|
|
|
|
const body = await res.json()
|
|
expect(body.data).toHaveLength(2)
|
|
expect(body.count).toBe(2)
|
|
})
|
|
|
|
it('paginates through all entries', async () => {
|
|
mockAuth('user-1')
|
|
|
|
// First call returns 500 entries (full page), second returns 100 (last page)
|
|
const bigPage = Array.from({ length: 500 }, (_, i) => ({
|
|
...sampleEntries[0],
|
|
id: `entry-${i}`,
|
|
}))
|
|
const lastPage = Array.from({ length: 100 }, (_, i) => ({
|
|
...sampleEntries[0],
|
|
id: `entry-${500 + i}`,
|
|
}))
|
|
|
|
mockGetAuditLog
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
.mockResolvedValueOnce({ data: bigPage as any, count: 600 })
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
.mockResolvedValueOnce({ data: lastPage as any, count: 600 })
|
|
|
|
const req = createMockRequest('/api/reports/audit-trail', {
|
|
searchParams: { format: 'json' },
|
|
})
|
|
const res = await GET(req)
|
|
const body = await res.json()
|
|
|
|
expect(body.data).toHaveLength(600)
|
|
expect(mockGetAuditLog).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|