/** * @vitest-environment node */ import { createMockRequest } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetSession, mockValidateEnterpriseAuditAccess, mockBuildOrgScopeCondition, mockGetOrgWorkspaceIds, mockQueryAuditLogs, mockBuildFilterConditions, } = vi.hoisted(() => ({ mockGetSession: vi.fn(), mockValidateEnterpriseAuditAccess: vi.fn(), mockBuildOrgScopeCondition: vi.fn(), mockGetOrgWorkspaceIds: vi.fn(), mockQueryAuditLogs: vi.fn(), mockBuildFilterConditions: vi.fn(), })) vi.mock('@/lib/auth', () => ({ auth: { api: { getSession: vi.fn() } }, getSession: mockGetSession, })) vi.mock('@/app/api/v1/audit-logs/auth', () => ({ validateEnterpriseAuditAccess: mockValidateEnterpriseAuditAccess, })) vi.mock('@/app/api/v1/audit-logs/query', () => ({ buildFilterConditions: mockBuildFilterConditions, buildOrgScopeCondition: mockBuildOrgScopeCondition, getOrgWorkspaceIds: mockGetOrgWorkspaceIds, queryAuditLogs: mockQueryAuditLogs, })) import { GET } from '@/app/api/audit-logs/export/route' const ORG_ID = 'org-1' const MEMBER_IDS = ['admin-1'] const SCOPE_SENTINEL = { type: 'org-scope-sentinel' } function makeRequest(query = '') { return createMockRequest( 'GET', undefined, {}, `http://localhost:3000/api/audit-logs/export${query}` ) } function auditLog(overrides: Partial> = {}) { return { id: 'log-1', workspaceId: null, actorId: 'admin-1', actorName: 'Ada Lovelace', actorEmail: 'ada@example.com', action: 'workflow.created', resourceType: 'workflow', resourceId: 'wf-1', resourceName: 'My workflow', description: null, metadata: null, createdAt: new Date('2026-07-01T00:00:00.000Z'), ...overrides, } } describe('GET /api/audit-logs/export', () => { beforeEach(() => { vi.clearAllMocks() mockGetSession.mockResolvedValue({ user: { id: 'admin-1' } }) mockValidateEnterpriseAuditAccess.mockResolvedValue({ success: true, context: { organizationId: ORG_ID, orgMemberIds: MEMBER_IDS }, }) mockGetOrgWorkspaceIds.mockResolvedValue([]) mockBuildOrgScopeCondition.mockReturnValue(SCOPE_SENTINEL) mockBuildFilterConditions.mockReturnValue([]) mockQueryAuditLogs.mockResolvedValue({ data: [], nextCursor: undefined }) }) it('returns 401 when unauthenticated', async () => { mockGetSession.mockResolvedValue(null) const response = await GET(makeRequest()) expect(response.status).toBe(401) }) it('returns the enterprise-access-check response when access is denied', async () => { mockValidateEnterpriseAuditAccess.mockResolvedValue({ success: false, response: new Response( JSON.stringify({ error: 'Organization admin or owner role required' }), { status: 403, } ), }) const response = await GET(makeRequest()) expect(response.status).toBe(403) expect(mockQueryAuditLogs).not.toHaveBeenCalled() }) it('returns a CSV with the header row and one line per log', async () => { mockQueryAuditLogs.mockResolvedValueOnce({ data: [auditLog()], nextCursor: undefined }) const response = await GET(makeRequest()) const csv = await response.text() const [header, row] = csv.split('\n') expect(response.headers.get('Content-Type')).toBe('text/csv; charset=utf-8') expect(response.headers.get('Content-Disposition')).toContain('attachment; filename=') expect(response.headers.get('X-Export-Truncated')).toBe('0') expect(header).toBe('Date,Action,Resource Type,Resource Name,Actor,Description') expect(row).toBe( '2026-07-01T00:00:00.000Z,workflow.created,workflow,My workflow,ada@example.com,' ) }) it('falls back to actorName, then "System", when actorEmail is absent', async () => { mockQueryAuditLogs.mockResolvedValueOnce({ data: [auditLog({ actorEmail: null, actorName: 'Ada Lovelace' })], nextCursor: undefined, }) const response = await GET(makeRequest()) const csv = await response.text() expect(csv).toContain('Ada Lovelace') }) it('paginates through queryAuditLogs until there is no nextCursor', async () => { mockQueryAuditLogs .mockResolvedValueOnce({ data: [auditLog({ id: 'log-1' })], nextCursor: 'cursor-1', }) .mockResolvedValueOnce({ data: [auditLog({ id: 'log-2' })], nextCursor: undefined, }) const response = await GET(makeRequest()) const csv = await response.text() expect(mockQueryAuditLogs).toHaveBeenCalledTimes(2) expect(mockQueryAuditLogs).toHaveBeenNthCalledWith( 2, expect.anything(), expect.any(Number), 'cursor-1' ) expect(csv.split('\n')).toHaveLength(3) }) it('rejects an actorId that is not a current org member', async () => { const response = await GET(makeRequest('?actorId=outsider-1')) expect(response.status).toBe(400) expect(mockQueryAuditLogs).not.toHaveBeenCalled() }) })