diff --git a/frontend/src/api/auditLogs.test.ts b/frontend/src/api/auditLogs.test.ts new file mode 100644 index 00000000..0b23908e --- /dev/null +++ b/frontend/src/api/auditLogs.test.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import client from './client' +import { + getAuditLogs, + getAuditLog, + getAuditLogsByProvider, + exportAuditLogsCSV, + type AuditLog, + type AuditLogFilters, +} from './auditLogs' + +vi.mock('./client', () => ({ + default: { + get: vi.fn(), + }, +})) + +const mockedClient = client as unknown as { + get: ReturnType +} + +describe('auditLogs api', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getAuditLogs', () => { + it('fetches audit logs with default pagination', async () => { + const mockResponse = { + logs: [ + { + id: 1, + uuid: 'log-1', + actor: 'admin', + action: 'user_login', + event_category: 'user', + details: 'User logged in', + ip_address: '192.168.1.1', + created_at: '2024-01-01T00:00:00Z', + }, + ], + total: 1, + page: 1, + limit: 50, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + const result = await getAuditLogs() + + expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs?page=1&limit=50') + expect(result).toEqual(mockResponse) + expect(result.logs).toHaveLength(1) + expect(result.logs[0].uuid).toBe('log-1') + }) + + it('fetches audit logs with custom pagination', async () => { + const mockResponse = { + logs: [], + total: 100, + page: 3, + limit: 25, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + const result = await getAuditLogs(undefined, 3, 25) + + expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs?page=3&limit=25') + expect(result.page).toBe(3) + expect(result.limit).toBe(25) + }) + + it('fetches audit logs with all filters', async () => { + const filters: AuditLogFilters = { + event_category: 'dns_provider', + actor: 'admin', + action: 'dns_provider_create', + start_date: '2024-01-01', + end_date: '2024-12-31', + resource_uuid: 'resource-123', + } + const mockResponse = { + logs: [], + total: 0, + page: 1, + limit: 50, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + await getAuditLogs(filters) + + expect(mockedClient.get).toHaveBeenCalledWith( + '/audit-logs?page=1&limit=50&event_category=dns_provider&actor=admin&action=dns_provider_create&start_date=2024-01-01&end_date=2024-12-31&resource_uuid=resource-123' + ) + }) + + it('fetches audit logs with partial filters', async () => { + const filters: AuditLogFilters = { + event_category: 'certificate', + start_date: '2024-01-01', + } + const mockResponse = { + logs: [], + total: 5, + page: 1, + limit: 50, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + await getAuditLogs(filters, 1, 50) + + expect(mockedClient.get).toHaveBeenCalledWith( + '/audit-logs?page=1&limit=50&event_category=certificate&start_date=2024-01-01' + ) + }) + + it('handles errors when fetching audit logs', async () => { + const error = new Error('Network error') + mockedClient.get.mockRejectedValueOnce(error) + + await expect(getAuditLogs()).rejects.toThrow('Network error') + }) + }) + + describe('getAuditLog', () => { + it('fetches a single audit log by UUID', async () => { + const mockLog: AuditLog = { + id: 42, + uuid: 'log-uuid-123', + actor: 'admin', + action: 'certificate_issue', + event_category: 'certificate', + resource_id: 10, + resource_uuid: 'cert-uuid', + details: 'Certificate issued successfully', + ip_address: '10.0.0.1', + user_agent: 'Mozilla/5.0', + created_at: '2024-06-15T12:30:00Z', + } + mockedClient.get.mockResolvedValueOnce({ data: mockLog }) + + const result = await getAuditLog('log-uuid-123') + + expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs/log-uuid-123') + expect(result).toEqual(mockLog) + expect(result.uuid).toBe('log-uuid-123') + expect(result.action).toBe('certificate_issue') + }) + + it('handles 404 when audit log not found', async () => { + const error = new Error('Not found') + mockedClient.get.mockRejectedValueOnce(error) + + await expect(getAuditLog('nonexistent')).rejects.toThrow('Not found') + }) + }) + + describe('getAuditLogsByProvider', () => { + it('fetches audit logs for a specific DNS provider with default pagination', async () => { + const mockResponse = { + logs: [ + { + id: 5, + uuid: 'log-5', + actor: 'system', + action: 'dns_provider_update', + event_category: 'dns_provider', + resource_id: 123, + details: 'DNS provider updated', + created_at: '2024-03-15T10:00:00Z', + }, + ], + total: 10, + page: 1, + limit: 50, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + const result = await getAuditLogsByProvider(123) + + expect(mockedClient.get).toHaveBeenCalledWith('/dns-providers/123/audit-logs?page=1&limit=50') + expect(result.logs).toHaveLength(1) + expect(result.logs[0].action).toBe('dns_provider_update') + }) + + it('fetches audit logs for a provider with custom pagination', async () => { + const mockResponse = { + logs: [], + total: 25, + page: 2, + limit: 10, + } + mockedClient.get.mockResolvedValueOnce({ data: mockResponse }) + + const result = await getAuditLogsByProvider(456, 2, 10) + + expect(mockedClient.get).toHaveBeenCalledWith('/dns-providers/456/audit-logs?page=2&limit=10') + expect(result.page).toBe(2) + expect(result.limit).toBe(10) + }) + + it('handles errors when fetching provider audit logs', async () => { + const error = new Error('Provider not found') + mockedClient.get.mockRejectedValueOnce(error) + + await expect(getAuditLogsByProvider(999)).rejects.toThrow('Provider not found') + }) + }) + + describe('exportAuditLogsCSV', () => { + it('exports audit logs to CSV without filters', async () => { + const mockCSV = 'id,actor,action,created_at\n1,admin,user_login,2024-01-01' + mockedClient.get.mockResolvedValueOnce({ data: mockCSV }) + + const result = await exportAuditLogsCSV() + + expect(mockedClient.get).toHaveBeenCalledWith( + '/audit-logs/export?', + { headers: { Accept: 'text/csv' } } + ) + expect(result).toBe(mockCSV) + }) + + it('exports audit logs to CSV with all filters', async () => { + const filters: AuditLogFilters = { + event_category: 'proxy_host', + actor: 'operator', + action: 'proxy_host_delete', + start_date: '2024-01-01', + end_date: '2024-06-30', + resource_uuid: 'host-uuid-456', + } + const mockCSV = 'id,actor,action,created_at\n' + mockedClient.get.mockResolvedValueOnce({ data: mockCSV }) + + const result = await exportAuditLogsCSV(filters) + + expect(mockedClient.get).toHaveBeenCalledWith( + '/audit-logs/export?event_category=proxy_host&actor=operator&action=proxy_host_delete&start_date=2024-01-01&end_date=2024-06-30&resource_uuid=host-uuid-456', + { headers: { Accept: 'text/csv' } } + ) + expect(result).toBe(mockCSV) + }) + + it('exports audit logs with partial filters', async () => { + const filters: AuditLogFilters = { + action: 'settings_update', + end_date: '2024-12-31', + } + const mockCSV = 'header,data\n' + mockedClient.get.mockResolvedValueOnce({ data: mockCSV }) + + await exportAuditLogsCSV(filters) + + expect(mockedClient.get).toHaveBeenCalledWith( + '/audit-logs/export?action=settings_update&end_date=2024-12-31', + { headers: { Accept: 'text/csv' } } + ) + }) + + it('handles errors when exporting audit logs', async () => { + const error = new Error('Export failed') + mockedClient.get.mockRejectedValueOnce(error) + + await expect(exportAuditLogsCSV()).rejects.toThrow('Export failed') + }) + }) +})