import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { BrowserRouter } from 'react-router-dom' import WafConfig from '../WafConfig' import * as securityApi from '../../api/security' import type { SecurityRuleSet, RuleSetsResponse } from '../../api/security' vi.mock('../../api/security') const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }) const renderWithProviders = (ui: React.ReactNode) => { const qc = createQueryClient() return render( {ui} ) } const mockRuleSet: SecurityRuleSet = { id: 1, uuid: 'uuid-1', name: 'OWASP CRS', source_url: '', mode: 'blocking', last_updated: '2024-01-15T10:00:00Z', content: 'SecRule REQUEST_URI "@contains /admin" "id:1000,phase:1,deny,status:403"', } describe('WafConfig page', () => { beforeEach(() => { vi.resetAllMocks() }) it('shows loading state while fetching rulesets', async () => { // Keep the promise pending to test loading state vi.mocked(securityApi.getRuleSets).mockReturnValue(new Promise(() => {})) renderWithProviders() expect(screen.getByTestId('waf-loading')).toBeInTheDocument() expect(screen.getByText('Loading WAF configuration...')).toBeInTheDocument() }) it('shows error state when fetch fails', async () => { vi.mocked(securityApi.getRuleSets).mockRejectedValue(new Error('Network error')) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('waf-error')).toBeInTheDocument() }) expect(screen.getByText(/Failed to load WAF configuration/)).toBeInTheDocument() expect(screen.getByText(/Network error/)).toBeInTheDocument() }) it('shows empty state when no rulesets exist', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('waf-empty-state')).toBeInTheDocument() }) expect(screen.getByText('No Rule Sets')).toBeInTheDocument() expect(screen.getByText(/Create your first WAF rule set/)).toBeInTheDocument() }) it('renders rulesets table when data exists', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) expect(screen.getByText('OWASP CRS')).toBeInTheDocument() expect(screen.getByText('Blocking')).toBeInTheDocument() expect(screen.getByText('Inline')).toBeInTheDocument() }) it('shows create form when Add Rule Set button is clicked', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) expect(screen.getByRole('heading', { name: 'Create Rule Set' })).toBeInTheDocument() expect(screen.getByTestId('ruleset-name-input')).toBeInTheDocument() expect(screen.getByTestId('ruleset-content-input')).toBeInTheDocument() }) it('submits new ruleset and closes form on success', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) vi.mocked(securityApi.upsertRuleSet).mockResolvedValue({ id: 1 }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Fill in the form await userEvent.type(screen.getByTestId('ruleset-name-input'), 'Test Rules') await userEvent.type( screen.getByTestId('ruleset-content-input'), 'SecRule ARGS "@contains test" "id:1,phase:1,deny"' ) // Submit const submitBtn = screen.getByRole('button', { name: 'Create Rule Set' }) await userEvent.click(submitBtn) await waitFor(() => { expect(securityApi.upsertRuleSet).toHaveBeenCalledWith({ id: undefined, name: 'Test Rules', source_url: undefined, content: 'SecRule ARGS "@contains test" "id:1,phase:1,deny"', mode: 'blocking', }) }) }) it('opens edit form when edit button is clicked', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('edit-ruleset-1')) expect(screen.getByText('Edit Rule Set')).toBeInTheDocument() expect(screen.getByDisplayValue('OWASP CRS')).toBeInTheDocument() }) it('opens delete confirmation dialog and deletes on confirm', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) vi.mocked(securityApi.deleteRuleSet).mockResolvedValue(undefined) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) // Click delete button await userEvent.click(screen.getByTestId('delete-ruleset-1')) // Confirm dialog should appear expect(screen.getByText('Delete Rule Set')).toBeInTheDocument() expect(screen.getByText(/Are you sure you want to delete "OWASP CRS"/)).toBeInTheDocument() // Confirm deletion await userEvent.click(screen.getByTestId('confirm-delete-btn')) await waitFor(() => { expect(securityApi.deleteRuleSet).toHaveBeenCalledWith(1) }) }) it('cancels delete when clicking cancel button', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) // Click delete button await userEvent.click(screen.getByTestId('delete-ruleset-1')) // Click cancel await userEvent.click(screen.getByText('Cancel')) // Dialog should be closed await waitFor(() => { expect(screen.queryByText('Delete Rule Set')).not.toBeInTheDocument() }) expect(securityApi.deleteRuleSet).not.toHaveBeenCalled() }) it('cancels delete when clicking backdrop', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) // Click delete button await userEvent.click(screen.getByTestId('delete-ruleset-1')) // Click backdrop await userEvent.click(screen.getByTestId('confirm-dialog-backdrop')) // Dialog should be closed await waitFor(() => { expect(screen.queryByText('Delete Rule Set')).not.toBeInTheDocument() }) }) it('displays mode correctly for detection-only rulesets', async () => { const detectionRuleset: SecurityRuleSet = { ...mockRuleSet, mode: 'detection', } const response: RuleSetsResponse = { rulesets: [detectionRuleset] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) expect(screen.getByText('Detection')).toBeInTheDocument() }) it('displays URL link when source_url is provided', async () => { const urlRuleset: SecurityRuleSet = { ...mockRuleSet, source_url: 'https://example.com/rules.conf', content: '', } const response: RuleSetsResponse = { rulesets: [urlRuleset] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) const urlLink = screen.getByText('URL') expect(urlLink).toHaveAttribute('href', 'https://example.com/rules.conf') expect(urlLink).toHaveAttribute('target', '_blank') }) it('validates form - submit disabled without name', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Only add content, no name await userEvent.type(screen.getByTestId('ruleset-content-input'), 'SecRule test') const submitBtn = screen.getByRole('button', { name: 'Create Rule Set' }) expect(submitBtn).toBeDisabled() }) it('validates form - submit disabled without content or URL', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Only add name, no content or URL await userEvent.type(screen.getByTestId('ruleset-name-input'), 'Test') const submitBtn = screen.getByRole('button', { name: 'Create Rule Set' }) expect(submitBtn).toBeDisabled() }) it('allows form submission with URL instead of content', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) vi.mocked(securityApi.upsertRuleSet).mockResolvedValue({ id: 1 }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Add name and URL, no content await userEvent.type(screen.getByTestId('ruleset-name-input'), 'Remote Rules') await userEvent.type(screen.getByTestId('ruleset-url-input'), 'https://example.com/rules.conf') const submitBtn = screen.getByRole('button', { name: 'Create Rule Set' }) expect(submitBtn).not.toBeDisabled() await userEvent.click(submitBtn) await waitFor(() => { expect(securityApi.upsertRuleSet).toHaveBeenCalledWith({ id: undefined, name: 'Remote Rules', source_url: 'https://example.com/rules.conf', content: undefined, mode: 'blocking', }) }) }) it('toggles between blocking and detection mode', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) vi.mocked(securityApi.upsertRuleSet).mockResolvedValue({ id: 1 }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Fill required fields await userEvent.type(screen.getByTestId('ruleset-name-input'), 'Test') await userEvent.type(screen.getByTestId('ruleset-content-input'), 'SecRule test') // Select detection mode await userEvent.click(screen.getByTestId('mode-detection')) // Verify mode description changed expect(screen.getByText(/Malicious requests will be logged but not blocked/)).toBeInTheDocument() await userEvent.click(screen.getByRole('button', { name: 'Create Rule Set' })) await waitFor(() => { expect(securityApi.upsertRuleSet).toHaveBeenCalledWith( expect.objectContaining({ mode: 'detection' }) ) }) }) it('hides form when cancel is clicked', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) expect(screen.getByRole('heading', { name: 'Create Rule Set' })).toBeInTheDocument() await userEvent.click(screen.getByRole('button', { name: 'Cancel' })) // Form should be hidden, empty state visible await waitFor(() => { expect(screen.getByTestId('waf-empty-state')).toBeInTheDocument() }) }) it('updates existing ruleset correctly', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) vi.mocked(securityApi.upsertRuleSet).mockResolvedValue({ id: 1 }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) // Open edit form await userEvent.click(screen.getByTestId('edit-ruleset-1')) // Update name const nameInput = screen.getByTestId('ruleset-name-input') await userEvent.clear(nameInput) await userEvent.type(nameInput, 'Updated CRS') // Submit await userEvent.click(screen.getByText('Update Rule Set')) await waitFor(() => { expect(securityApi.upsertRuleSet).toHaveBeenCalledWith( expect.objectContaining({ id: 1, name: 'Updated CRS', }) ) }) }) it('opens delete from edit form', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) // Open edit form await userEvent.click(screen.getByTestId('edit-ruleset-1')) // Click delete button in edit form header const deleteBtn = screen.getByRole('button', { name: /delete/i }) await userEvent.click(deleteBtn) // Confirm dialog should appear expect(screen.getByText('Delete Rule Set')).toBeInTheDocument() }) it('counts rules correctly in table', async () => { const multiRuleSet: SecurityRuleSet = { ...mockRuleSet, content: `SecRule ARGS "@contains test1" "id:1,phase:1,deny" SecRule ARGS "@contains test2" "id:2,phase:1,deny" SecRule ARGS "@contains test3" "id:3,phase:1,deny"`, } const response: RuleSetsResponse = { rulesets: [multiRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) expect(screen.getByText('3 rule(s)')).toBeInTheDocument() }) it('shows preset dropdown when creating new ruleset', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) expect(screen.getByTestId('preset-select')).toBeInTheDocument() expect(screen.getByText('Choose a preset...')).toBeInTheDocument() }) it('auto-fills form when preset is selected', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Select OWASP CRS preset const presetSelect = screen.getByTestId('preset-select') await userEvent.selectOptions(presetSelect, 'OWASP Core Rule Set') // Verify form is auto-filled expect(screen.getByTestId('ruleset-name-input')).toHaveValue('OWASP Core Rule Set') expect(screen.getByTestId('ruleset-url-input')).toHaveValue( 'https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.5.tar.gz' ) }) it('auto-fills content for inline preset', async () => { const response: RuleSetsResponse = { rulesets: [] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('create-ruleset-btn')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('create-ruleset-btn')) // Select SQL Injection preset (has inline content) const presetSelect = screen.getByTestId('preset-select') await userEvent.selectOptions(presetSelect, 'Basic SQL Injection Protection') // Verify content is auto-filled const contentInput = screen.getByTestId('ruleset-content-input') as HTMLTextAreaElement expect(contentInput.value).toContain('SecRule') expect(contentInput.value).toContain('SQLi') }) it('does not show preset dropdown when editing', async () => { const response: RuleSetsResponse = { rulesets: [mockRuleSet] } vi.mocked(securityApi.getRuleSets).mockResolvedValue(response) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('rulesets-table')).toBeInTheDocument() }) await userEvent.click(screen.getByTestId('edit-ruleset-1')) // Preset dropdown should not be visible when editing expect(screen.queryByTestId('preset-select')).not.toBeInTheDocument() }) })